Random Generation for Games
Creating Variety and Surprise
Randomness is the spice of gaming! It creates variety, replayability, and surprise. From loot drops to procedural worlds, understanding controlled randomness is essential for creating engaging games. Let's explore how to harness chaos for fun! ๐ฒ๐
Understanding Randomness in Games
๐ฐ The Casino Analogy
Think of randomness in games like a casino:
- True Random: Like a perfectly fair dice - unpredictable
- Pseudo-Random: Like a shuffled deck - appears random but deterministic
- Weighted Random: Like loaded dice - biased outcomes
- Controlled Random: Like card counting - managing randomness
- Seeded Random: Like using the same deck - reproducible results
Interactive Random Visualizer
Visualize different random distributions and patterns!
Mode: Uniform
Basic Random Number Generation
import random
# Basic random functions
random_float = random.random() # 0.0 to 1.0
random_int = random.randint(1, 6) # 1 to 6 (inclusive)
random_range = random.randrange(0, 100, 5) # 0, 5, 10, ..., 95
random_choice = random.choice(['sword', 'shield', 'potion'])
# Set seed for reproducibility
random.seed(12345) # Same seed = same sequence
# Random with probability
def weighted_choice(choices):
"""choices = [(item, weight), ...]"""
total = sum(weight for item, weight in choices)
r = random.uniform(0, total)
upto = 0
for item, weight in choices:
upto += weight
if r <= upto:
return item
return choices[-1][0]
# Example: Loot table
loot_table = [
('common', 60),
('uncommon', 30),
('rare', 9),
('legendary', 1)
]
rarity = weighted_choice(loot_table)
Random Distributions
import random
import math
class RandomDistributions:
@staticmethod
def uniform(min_val, max_val):
"""Equal probability for all values"""
return random.uniform(min_val, max_val)
@staticmethod
def gaussian(mean, std_dev):
"""Bell curve distribution"""
return random.gauss(mean, std_dev)
@staticmethod
def exponential(rate=1.0):
"""Decay distribution - good for spawn timers"""
return random.expovariate(rate)
@staticmethod
def triangular(low, high, peak):
"""Triangle-shaped distribution"""
return random.triangular(low, high, peak)
@staticmethod
def beta_distribution(alpha, beta):
"""Flexible distribution between 0 and 1"""
return random.betavariate(alpha, beta)
@staticmethod
def pareto(alpha):
"""Power law - few large values, many small"""
return random.paretovariate(alpha)
# Custom distributions
def biased_random(bias=2):
"""Returns 0-1 biased towards 0 or 1"""
r = random.random()
if bias > 1:
# Bias towards 1
return r ** (1/bias)
else:
# Bias towards 0
return 1 - (1 - r) ** bias
def random_in_circle(radius):
"""Uniform random point in circle"""
# Simple but wrong: clustering in center
# angle = random.uniform(0, 2 * math.pi)
# r = random.uniform(0, radius)
# Correct: uniform distribution
angle = random.uniform(0, 2 * math.pi)
r = math.sqrt(random.random()) * radius
x = r * math.cos(angle)
y = r * math.sin(angle)
return (x, y)
def random_on_circle(radius):
"""Random point on circle perimeter"""
angle = random.uniform(0, 2 * math.pi)
x = radius * math.cos(angle)
y = radius * math.sin(angle)
return (x, y)
Controlled Randomness
class DiceRoller:
"""Dice with guaranteed distribution over time"""
def __init__(self, sides=6):
self.sides = sides
self.history = []
self.max_history = sides * 3
def roll(self):
"""Roll with bias against recent results"""
weights = [1.0] * self.sides
# Reduce weight of recent results
for past_roll in self.history[-self.sides:]:
weights[past_roll - 1] *= 0.5
# Weighted random choice
total = sum(weights)
r = random.uniform(0, total)
cumsum = 0
for i, weight in enumerate(weights):
cumsum += weight
if r <= cumsum:
result = i + 1
self.history.append(result)
if len(self.history) > self.max_history:
self.history.pop(0)
return result
return self.sides
class LootBag:
"""Guaranteed drops without repetition"""
def __init__(self, items):
self.items = items[:]
self.available = items[:]
def get_item(self):
if not self.available:
self.available = self.items[:]
random.shuffle(self.available)
return self.available.pop()
class PitySystem:
"""Increase chance until success"""
def __init__(self, base_chance=0.01, increase=0.01):
self.base_chance = base_chance
self.increase = increase
self.current_chance = base_chance
def try_drop(self):
if random.random() < self.current_chance:
self.current_chance = self.base_chance
return True
else:
self.current_chance += self.increase
return False
Noise Functions
import math
import random
class NoiseGenerator:
def __init__(self, seed=None):
if seed:
random.seed(seed)
# Create permutation table
self.perm = list(range(256))
random.shuffle(self.perm)
self.perm = self.perm + self.perm # Duplicate for wrapping
def fade(self, t):
"""Fade function for smooth interpolation"""
return t * t * t * (t * (t * 6 - 15) + 10)
def lerp(self, t, a, b):
"""Linear interpolation"""
return a + t * (b - a)
def grad1d(self, hash_val, x):
"""1D gradient function"""
return x if hash_val & 1 else -x
def noise1d(self, x):
"""1D Perlin noise"""
# Find unit interval
xi = int(math.floor(x)) & 255
xf = x - math.floor(x)
# Fade for smoothing
u = self.fade(xf)
# Hash and interpolate
a = self.perm[xi]
b = self.perm[xi + 1]
return self.lerp(u, self.grad1d(a, xf),
self.grad1d(b, xf - 1))
def octave_noise1d(self, x, octaves=4, persistence=0.5):
"""Layered noise for more detail"""
total = 0
amplitude = 1
max_value = 0
frequency = 1
for _ in range(octaves):
total += self.noise1d(x * frequency) * amplitude
max_value += amplitude
amplitude *= persistence
frequency *= 2
return total / max_value
class ValueNoise2D:
"""Simple 2D value noise"""
def __init__(self, width, height, scale=20):
self.width = width
self.height = height
self.scale = scale
self.values = {}
def get_noise(self, x, y):
"""Get noise value at integer coordinates"""
key = (x, y)
if key not in self.values:
random.seed(hash(key))
self.values[key] = random.random()
return self.values[key]
def smooth_noise(self, x, y):
"""Interpolated noise value"""
int_x = int(x)
int_y = int(y)
frac_x = x - int_x
frac_y = y - int_y
# Get corner values
v1 = self.get_noise(int_x, int_y)
v2 = self.get_noise(int_x + 1, int_y)
v3 = self.get_noise(int_x, int_y + 1)
v4 = self.get_noise(int_x + 1, int_y + 1)
# Interpolate
i1 = self.cosine_interp(v1, v2, frac_x)
i2 = self.cosine_interp(v3, v4, frac_x)
return self.cosine_interp(i1, i2, frac_y)
def cosine_interp(self, a, b, x):
"""Smooth interpolation"""
ft = x * math.pi
f = (1 - math.cos(ft)) * 0.5
return a * (1 - f) + b * f
def generate_map(self):
"""Generate full noise map"""
noise_map = []
for y in range(self.height):
row = []
for x in range(self.width):
noise_val = self.smooth_noise(x / self.scale,
y / self.scale)
row.append(noise_val)
noise_map.append(row)
return noise_map
Procedural Generation
class ProceduralDungeon:
def __init__(self, width, height, seed=None):
self.width = width
self.height = height
if seed:
random.seed(seed)
# Initialize with walls
self.tiles = [['#' for _ in range(width)] for _ in range(height)]
self.rooms = []
def generate(self, num_rooms=10, min_size=4, max_size=10):
"""Generate dungeon with rooms and corridors"""
for _ in range(num_rooms):
# Random room size and position
w = random.randint(min_size, max_size)
h = random.randint(min_size, max_size)
x = random.randint(1, self.width - w - 1)
y = random.randint(1, self.height - h - 1)
room = {'x': x, 'y': y, 'w': w, 'h': h}
# Check for overlaps
if not any(self.rooms_overlap(room, other) for other in self.rooms):
self.carve_room(room)
# Connect to previous room
if self.rooms:
prev = self.rooms[-1]
self.carve_corridor(
room['x'] + room['w'] // 2,
room['y'] + room['h'] // 2,
prev['x'] + prev['w'] // 2,
prev['y'] + prev['h'] // 2
)
self.rooms.append(room)
def rooms_overlap(self, r1, r2):
"""Check if two rooms overlap"""
return (r1['x'] < r2['x'] + r2['w'] and
r1['x'] + r1['w'] > r2['x'] and
r1['y'] < r2['y'] + r2['h'] and
r1['y'] + r1['h'] > r2['y'])
def carve_room(self, room):
"""Carve out a room"""
for y in range(room['y'], room['y'] + room['h']):
for x in range(room['x'], room['x'] + room['w']):
self.tiles[y][x] = '.'
def carve_corridor(self, x1, y1, x2, y2):
"""Carve corridor between two points"""
# Horizontal first, then vertical
if random.random() < 0.5:
self.carve_h_tunnel(x1, x2, y1)
self.carve_v_tunnel(y1, y2, x2)
else:
self.carve_v_tunnel(y1, y2, x1)
self.carve_h_tunnel(x1, x2, y2)
def carve_h_tunnel(self, x1, x2, y):
"""Carve horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.tiles[y][x] = '.'
def carve_v_tunnel(self, y1, y2, x):
"""Carve vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.tiles[y][x] = '.'
class WaveFunction Collapse:
"""Simple WFC for tile-based generation"""
def __init__(self, tile_rules):
self.tile_rules = tile_rules # What can be adjacent
def generate(self, width, height):
# Simplified WFC implementation
grid = [[None for _ in range(width)] for _ in range(height)]
# Start with random tile
start_x = width // 2
start_y = height // 2
grid[start_y][start_x] = random.choice(list(self.tile_rules.keys()))
# Propagate constraints
# (Full WFC is complex - this is simplified)
for y in range(height):
for x in range(width):
if grid[y][x] is None:
possible = self.get_valid_tiles(grid, x, y)
if possible:
grid[y][x] = random.choice(possible)
return grid
def get_valid_tiles(self, grid, x, y):
"""Get tiles that can be placed at position"""
possible = set(self.tile_rules.keys())
# Check neighbors
neighbors = [
(x-1, y), (x+1, y),
(x, y-1), (x, y+1)
]
for nx, ny in neighbors:
if 0 <= nx < len(grid[0]) and 0 <= ny < len(grid):
neighbor_tile = grid[ny][nx]
if neighbor_tile:
# Filter based on rules
valid = self.tile_rules[neighbor_tile]
possible = possible.intersection(valid)
return list(possible)
Random in Game Systems
class CriticalHitSystem:
"""Critical hits with pseudo-random distribution"""
def __init__(self, base_chance=0.25):
self.base_chance = base_chance
self.c = self._calculate_c()
self.counter = 0
def _calculate_c(self):
"""Calculate PRD constant"""
# Approximation for PRD
p = self.base_chance
if p <= 0:
return 0
return p * 0.75 # Simplified
def check_crit(self):
"""Check if attack is critical"""
self.counter += 1
chance = min(1.0, self.c * self.counter)
if random.random() < chance:
self.counter = 0
return True
return False
class RandomEncounter:
"""Random encounters with increasing probability"""
def __init__(self, base_steps=20):
self.base_steps = base_steps
self.steps_since_encounter = 0
def step(self):
"""Take a step, check for encounter"""
self.steps_since_encounter += 1
# Probability increases with steps
chance = self.steps_since_encounter / self.base_steps
if random.random() < chance:
self.steps_since_encounter = 0
return True
return False
class RandomSpawner:
"""Spawn enemies with controlled randomness"""
def __init__(self, spawn_points):
self.spawn_points = spawn_points
self.last_spawn = None
self.spawn_cooldown = {}
def get_spawn_point(self):
"""Get random spawn point, avoiding recent ones"""
available = [sp for sp in self.spawn_points
if sp not in self.spawn_cooldown]
if not available:
# All on cooldown, use oldest
oldest = min(self.spawn_cooldown,
key=self.spawn_cooldown.get)
del self.spawn_cooldown[oldest]
available = [oldest]
spawn = random.choice(available)
# Add to cooldown
self.spawn_cooldown[spawn] = 3 # 3 spawns before reuse
# Decrease all cooldowns
for sp in list(self.spawn_cooldown.keys()):
self.spawn_cooldown[sp] -= 1
if self.spawn_cooldown[sp] <= 0:
del self.spawn_cooldown[sp]
return spawn
Complete Random Generation Demo
import pygame
import random
import math
class RandomDemo:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Random Generation Demo")
self.clock = pygame.time.Clock()
# Initialize systems
self.seed = random.randint(0, 999999)
random.seed(self.seed)
# Generate terrain with noise
self.terrain = self.generate_terrain()
# Generate dungeon
self.dungeon = ProceduralDungeon(40, 30, self.seed)
self.dungeon.generate()
# Particle system with random properties
self.particles = []
# Loot system
self.loot_bag = LootBag(['sword', 'shield', 'potion',
'gold', 'gem', 'scroll'])
# Random walker
self.walker_x = 400
self.walker_y = 300
self.walker_trail = []
# Spawn timer
self.spawn_timer = 0
self.spawn_interval = RandomDistributions.exponential(0.5)
# Demo state
self.show_terrain = True
self.show_dungeon = False
def generate_terrain(self):
"""Generate terrain using layered noise"""
noise = ValueNoise2D(80, 60, scale=10)
terrain_map = noise.generate_map()
# Convert to tiles
terrain = []
for row in terrain_map:
terrain_row = []
for value in row:
if value < 0.3:
terrain_row.append('water')
elif value < 0.5:
terrain_row.append('sand')
elif value < 0.7:
terrain_row.append('grass')
else:
terrain_row.append('mountain')
terrain.append(terrain_row)
return terrain
def spawn_particle(self):
"""Spawn particle with random properties"""
# Random position on edge
edge = random.choice(['top', 'bottom', 'left', 'right'])
if edge == 'top':
x = random.randint(0, 800)
y = 0
elif edge == 'bottom':
x = random.randint(0, 800)
y = 600
elif edge == 'left':
x = 0
y = random.randint(0, 600)
else:
x = 800
y = random.randint(0, 600)
# Random velocity towards center with variation
center_x = 400 + random.gauss(0, 100)
center_y = 300 + random.gauss(0, 100)
dx = center_x - x
dy = center_y - y
dist = math.sqrt(dx*dx + dy*dy)
if dist > 0:
speed = random.uniform(1, 4)
vx = (dx / dist) * speed
vy = (dy / dist) * speed
else:
vx = vy = 0
# Random color
hue = random.randint(0, 360)
color = self.hsv_to_rgb(hue, 0.8, 1.0)
# Random lifetime
lifetime = random.triangular(0.5, 3, 1.5)
self.particles.append({
'x': x, 'y': y,
'vx': vx, 'vy': vy,
'color': color,
'lifetime': lifetime,
'max_lifetime': lifetime,
'size': random.randint(2, 8)
})
def hsv_to_rgb(self, h, s, v):
"""Convert HSV to RGB"""
h = h / 360.0
i = int(h * 6)
f = h * 6 - i
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
i = i % 6
if i == 0:
r, g, b = v, t, p
elif i == 1:
r, g, b = q, v, p
elif i == 2:
r, g, b = p, v, t
elif i == 3:
r, g, b = p, q, v
elif i == 4:
r, g, b = t, p, v
else:
r, g, b = v, p, q
return (int(r * 255), int(g * 255), int(b * 255))
def update_walker(self):
"""Update random walker"""
# Biased random walk
dx = random.gauss(0, 2)
dy = random.gauss(0, 2)
self.walker_x += dx
self.walker_y += dy
# Keep on screen
self.walker_x = max(10, min(790, self.walker_x))
self.walker_y = max(10, min(590, self.walker_y))
# Add to trail
self.walker_trail.append((self.walker_x, self.walker_y))
if len(self.walker_trail) > 100:
self.walker_trail.pop(0)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
# Get random loot
loot = self.loot_bag.get_item()
print(f"Got loot: {loot}")
elif event.key == pygame.K_r:
# New seed
self.seed = random.randint(0, 999999)
random.seed(self.seed)
self.terrain = self.generate_terrain()
self.dungeon = ProceduralDungeon(40, 30, self.seed)
self.dungeon.generate()
elif event.key == pygame.K_t:
self.show_terrain = not self.show_terrain
elif event.key == pygame.K_d:
self.show_dungeon = not self.show_dungeon
return True
def update(self, dt):
# Update particles
for particle in self.particles[:]:
particle['x'] += particle['vx']
particle['y'] += particle['vy']
particle['lifetime'] -= dt
if particle['lifetime'] <= 0:
self.particles.remove(particle)
# Spawn new particles
self.spawn_timer -= dt
if self.spawn_timer <= 0:
self.spawn_particle()
self.spawn_timer = random.expovariate(2) # Average 0.5 seconds
# Update walker
self.update_walker()
def draw(self):
self.screen.fill((20, 20, 20))
# Draw terrain
if self.show_terrain:
tile_size = 10
colors = {
'water': (50, 100, 200),
'sand': (238, 203, 173),
'grass': (50, 200, 50),
'mountain': (139, 90, 43)
}
for y, row in enumerate(self.terrain):
for x, tile in enumerate(row):
color = colors[tile]
pygame.draw.rect(self.screen, color,
(x * tile_size, y * tile_size,
tile_size, tile_size))
# Draw dungeon overlay
if self.show_dungeon:
tile_size = 15
for y, row in enumerate(self.dungeon.tiles):
for x, tile in enumerate(row):
if tile == '#':
color = (100, 100, 100)
else:
color = (200, 180, 140)
pygame.draw.rect(self.screen, color,
(x * tile_size + 200,
y * tile_size + 150,
tile_size - 1, tile_size - 1))
# Draw random walker trail
if len(self.walker_trail) > 1:
for i in range(1, len(self.walker_trail)):
alpha = i / len(self.walker_trail)
color = (int(255 * alpha), int(100 * alpha), int(100 * alpha))
pygame.draw.line(self.screen, color,
self.walker_trail[i-1],
self.walker_trail[i], 2)
# Draw walker
pygame.draw.circle(self.screen, (255, 100, 100),
(int(self.walker_x), int(self.walker_y)), 5)
# Draw particles
for particle in self.particles:
alpha = particle['lifetime'] / particle['max_lifetime']
size = int(particle['size'] * alpha)
if size > 0:
pygame.draw.circle(self.screen, particle['color'],
(int(particle['x']), int(particle['y'])),
size)
# Draw info
font = pygame.font.Font(None, 24)
info = [
f"Seed: {self.seed}",
f"Particles: {len(self.particles)}",
"R: New Seed | T: Terrain | D: Dungeon | Space: Loot"
]
for i, text in enumerate(info):
rendered = font.render(text, True, (255, 255, 255))
self.screen.blit(rendered, (10, 10 + i * 25))
def run(self):
running = True
dt = 0
while running:
running = self.handle_events()
self.update(dt)
self.draw()
pygame.display.flip()
dt = self.clock.tick(60) / 1000.0
pygame.quit()
if __name__ == "__main__":
demo = RandomDemo()
demo.run()
Best Practices
โก Random Generation Tips
- Always Use Seeds: For debugging and reproducibility
- Avoid True Random: Use pseudo-random for consistency
- Control Distribution: Don't use uniform for everything
- Test Edge Cases: Min/max values, empty sets
- Cache When Possible: Don't regenerate unchanged content
- Player Psychology: Fair โ Fun (use controlled random)
Practice Exercises
๐ฏ Random Generation Challenges!
- Infinite Runner: Procedurally generate endless level
- Loot System: Tiered drops with rarity and affixes
- Name Generator: Random names from syllable parts
- Galaxy Generator: Star systems with planets
- Music Generator: Procedural melodies with rules
- Roguelike Dungeon: Complete dungeon with rooms, items, enemies
Key Takeaways
- ๐ฒ Pseudo-random is predictable with seeds
- ๐ Different distributions for different needs
- ๐ฏ Controlled random feels more fair
- ๐ Noise functions create natural patterns
- ๐๏ธ Procedural generation saves content creation
- โ๏ธ Balance true random with player satisfaction
- ๐ง Always test with multiple seeds
๐๏ธโโ๏ธ Practice Exercise: Loot Drops with a Loaded Deck
๐๏ธโโ๏ธ Exercise 1: Loot Drop Generator โ Weighted Choices on a Seeded RNG
Objective: Build a small Pygame loot-drop demo that exercises three core randomness ideas in one program: (1) seeded pseudo-random via random.seed(42) at startup so the same SPACE-press sequence reproduces the same drop sequence on every run โ reproducibility for debugging and replays, the lesson's Best Practice #1 'Always Use Seeds'; (2) weighted distribution via random.choices(items, weights=[70, 20, 8, 2]) for a Diablo-style rarity table (Common 70% / Uncommon 20% / Rare 8% / Epic 2%) โ lesson Best Practice 'Control Distribution: Don't use uniform for everything'; (3) running tally displayed on screen so the empirical distribution converges visibly toward the declared weights as the player presses SPACE many times โ makes the abstract weight values concrete. Press R to re-seed with a different value (e.g. 7) to verify the seed actually matters โ different seed โ different sequence; same seed โ same sequence every run.
Instructions:
- Open an 800ร600 window and start a clean game loop. Import
randomand callrandom.seed(42)ONCE at startup before any other random calls. - Define rarity tiers as parallel lists:
RARITIES = ["Common", "Uncommon", "Rare", "Epic"]andWEIGHTS = [70, 20, 8, 2](the weights don't have to sum to 100; they're relative). - Maintain a running tally dict
tally = {r: 0 for r in RARITIES}and a list of recent drops (the last ~8 to display visually, oldest fading out). - On
KEYDOWNforK_SPACE: roll one drop withrarity = random.choices(RARITIES, weights=WEIGHTS, k=1)[0]; incrementtally[rarity]and append to the recent-drops list (drop the head if >8 items). - On
KEYDOWNforK_R: re-seed with a different value (e.g.random.seed(7)), reset the tally and recent-drops list. The next press of SPACE should produce a DIFFERENT sequence than the seed-42 sequence โ visible proof that the seed drives the output. - Render the recent drops as a horizontal row of colored chips (Common gray, Uncommon green, Rare blue, Epic purple) along with the item name; render the tally as a bar chart on the right side of the screen so the empirical distribution converges visibly toward 70/20/8/2 over many presses.
- Render a HUD with the current seed value and total drop count so the seeded-reproducibility claim is verifiable: close the program, restart it with seed 42, press SPACE the same number of times โ you should see the IDENTICAL sequence of drops.
๐ก Hint
The two big traps: (1) random.seed() seeds the global random module, but if you instead do rng = random.Random(); rng.seed(42) you get an isolated generator โ either is fine but DON'T mix them, and DON'T re-seed mid-loop unless you intentionally want the sequence to restart. The lesson's framing is to seed ONCE at startup. (2) random.choices (with the 's', plural) returns a LIST of size k, even when k=1. Don't confuse with random.choice (singular, no weights, returns the item directly). The canonical idiom for one weighted pick is random.choices(items, weights=W, k=1)[0] โ the [0] unwraps the single-item list. The weights are RELATIVE, not absolute probabilities, so [70, 20, 8, 2] and [7, 2, 0.8, 0.2] produce identical sampling.
โ Example Solution
import pygame, random
pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Loot Drops")
font = pygame.font.SysFont(None, 28)
clock = pygame.time.Clock()
RARITIES = ["Common", "Uncommon", "Rare", "Epic"]
WEIGHTS = [70, 20, 8, 2]
COLORS = {
"Common": (180, 180, 180),
"Uncommon": (60, 200, 60),
"Rare": (60, 120, 230),
"Epic": (180, 80, 220),
}
seed = 42
random.seed(seed)
tally = {r: 0 for r in RARITIES}
recent = []
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
rarity = random.choices(RARITIES, weights=WEIGHTS, k=1)[0]
tally[rarity] += 1
recent.append(rarity)
if len(recent) > 8:
recent.pop(0)
elif event.key == pygame.K_r:
seed = 7 if seed == 42 else 42
random.seed(seed)
tally = {r: 0 for r in RARITIES}
recent = []
screen.fill((25, 25, 35))
total = sum(tally.values())
screen.blit(font.render(f"Seed: {seed} Drops: {total} (SPACE drop, R reseed)",
True, (240, 240, 240)), (10, 10))
for i, r in enumerate(recent):
pygame.draw.rect(screen, COLORS[r], (40 + i * 70, 100, 60, 60))
for i, r in enumerate(RARITIES):
bar_w = tally[r] * 4
pygame.draw.rect(screen, COLORS[r], (450, 220 + i * 50, bar_w, 36))
screen.blit(font.render(f"{r}: {tally[r]}", True, COLORS[r]), (460, 226 + i * 50))
pygame.display.flip()
clock.tick(60)
pygame.quit()
๐ฏ Quick Quiz
Question 1: What does calling `random.seed(42)` at the start of your game accomplish, and why does the lesson recommend always seeding?
Question 2: You want to pick a loot rarity from `["Common", "Uncommon", "Rare", "Epic"]` with rarity weights `[70, 20, 8, 2]`. Which call does this correctly?
Question 3: The lesson's Best Practices include 'Player Psychology: Fair โ Fun (use controlled random).' What does this mean in practice for game design?
What's Next?
Congratulations! You've completed the Game Mathematics section! You now have the mathematical tools to create smooth movement, realistic physics, and infinite variety. Next, we'll move into the Intermediate Module where you'll learn about sprite animation, scrolling backgrounds, and more!