From a634d652bb14f4247f3c0f3d74a1de6d3aedd1b3 Mon Sep 17 00:00:00 2001 From: "Alejandro J. Cura" Date: Tue, 13 Oct 2026 10:24:34 -0300 Subject: [PATCH 1/3] Give Dream Garden separate planted, flying and HUD layers --- games/demos/dream_garden/README.md | 33 +++-- .../code/checkpoints/05-flowers.py | 16 ++- .../dream_garden/code/checkpoints/06-tiles.py | 18 ++- .../code/checkpoints/07-scroll.py | 18 ++- .../dream_garden/code/checkpoints/08-hud.py | 22 +++- .../code/checkpoints/09-scenes.py | 30 +++-- .../code/checkpoints/10-shared.py | 83 ++++++++---- .../dream_garden/code/checkpoints/11-audio.py | 94 +++++++++---- games/demos/dream_garden/code/dream_garden.py | 94 +++++++++---- games/demos/dream_garden/images/garden.png | Bin 2129 -> 1084 bytes games/demos/dream_garden/images/seed.png | Bin 662 -> 343 bytes games/demos/dream_garden/sources/README.md | 4 +- .../dream_garden/sources/prepare-atlas.cjs | 17 ++- tests/test_dream_garden.py | 123 ++++++++++++++++-- 14 files changed, 414 insertions(+), 138 deletions(-) diff --git a/games/demos/dream_garden/README.md b/games/demos/dream_garden/README.md index f4bdb325..77b9692f 100644 --- a/games/demos/dream_garden/README.md +++ b/games/demos/dream_garden/README.md @@ -17,20 +17,25 @@ Windows uses `vs-emu.bat --game demos.dream_garden`. | Action | Controller | Player 1 keyboard | Player 2 keyboard | | --- | --- | --- | --- | -| Begin / join | A | Space | Z | -| Drift around the rim | Left / Right | Left / Right arrows | H / L | -| Slow everyone's garden | Hold A | Hold Space | Hold Z | -| Rest and keep the flowers on screen | B | O | X | -| Toggle the quiet lullaby and bloom sound | X | P | C | -| Return to the launcher | Y / Back | Y / Page Down | V / End | - -Catch a golden seed to plant a flower. Missing a seed has no penalty. A second +| Begin / join | A | Space | Numpad 0 | +| Drift around the rim | Left / Right | A / D | Left / Right arrows | +| Slow everyone's garden | Hold A | Hold Space | Hold Numpad 0 | +| Rest and keep the flowers on screen | B | B | Numpad 1 | +| Toggle the quiet lullaby and bloom sound | X | N | Numpad 2 | +| Return to the launcher | Y / Back / Home | M / 1 / Escape | Numpad 3 / + / Escape | + +The garden starts empty. Catch a golden seed to plant a flower at the catching +player’s depth, alternating sides at a centre offset of 75% of the player’s width. +Flowers drift inward at 8% of seed speed and disappear at the centre. +Smaller, subdued golden seeds occasionally pulse with a randomly triggered glow. +Missing a seed has no penalty. A second player joins by steering or pressing A, using an already reserved mint dreamer. Either player can slow seed movement, the drifting background and flower animation. New spawn intervals also lengthen, after the pending seed deadline. Flowers gradually replace the oldest ones when the garden fills up. -There is no death, score, countdown or difficulty ramp. Watching is welcome. Sound starts off. B stops the music and keeps the current flowers in a still garden. +There is no death, score, countdown or difficulty ramp. Watching is welcome. Music and bloom sounds start on. X toggles both, showing SOUND ON or SOUND OFF +for two seconds; another toggle restarts that notice. B stops the music and keeps the current flowers in a still garden. A starts a new garden from the Rest scene. ## How the example works @@ -41,10 +46,12 @@ objects in `update()`. A collision grows a flower instead of ending a run. Pools cap seeds at 16 and flowers at 24, with explicit recycling for flowers. The second player costs one reserved sprite even before joining. -The Garden graph uses **42 sprites, 2 tilemaps, 2 layers and 5 image strips**. +The Garden graph uses **42 sprites, 3 tilemaps, 3 layers and 5 image strips**. The flower pool counts toward that cost even when its slots are hidden. The -ground uses sparse, mostly transparent 16-pixel tiles. The HUD says DRIFT or -SLOW near the top rim, with both flips so the letters read upright. +lower layer draws flowers first and players above them. The middle layer draws +smaller, darker flying accents in 16-pixel tiles, followed by 9-pixel seeds. +The top HUD shows RELAX only while A is held, plus temporary audio feedback. +Both flips keep the top text upright. The full CP437 font comes from the existing tutorial example. PNGs and YAML compile into a generated image ROM. MP3s stay on the emulator host or console @@ -62,4 +69,4 @@ mpy-cross games/demos/dream_garden/code/dream_garden.py -o /tmp/dream_garden.mpy ``` The tests exercise seam collisions, both controllers, shared slow time, safe -pool exhaustion, long play, opt-in audio, voluntary rest and the exit path. +pool exhaustion, long play, default-on audio and timed toggle feedback, voluntary rest and the exit path. diff --git a/games/demos/dream_garden/code/checkpoints/05-flowers.py b/games/demos/dream_garden/code/checkpoints/05-flowers.py index 50332bbb..bc450887 100644 --- a/games/demos/dream_garden/code/checkpoints/05-flowers.py +++ b/games/demos/dream_garden/code/checkpoints/05-flowers.py @@ -6,10 +6,12 @@ class Game(vs2.Scene): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.air = self.layer("air", projection=vs2.TUNNEL) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) + self.plant_side = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -22,6 +24,10 @@ def update(self): 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 frame = int(self.dream_time // 3) % self.image("seed.png").frames + for flower in self.flowers: + flower.y += self.speed * 0.08 + if flower.y >= 255: + self.flowers.despawn(flower) for seed in self.seeds: seed.y -= self.speed seed.frame = frame @@ -35,9 +41,11 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.image("flowers.png").width // 2, - y=24, frame=randrange(4) * 2) + centre = self.dreamer.x + self.dreamer.width // 2 + centre += self.plant_side * self.dreamer.width * 0.75 + self.plant_side = -self.plant_side + self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, + y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) def main(): diff --git a/games/demos/dream_garden/code/checkpoints/06-tiles.py b/games/demos/dream_garden/code/checkpoints/06-tiles.py index 41a01a28..522e6939 100644 --- a/games/demos/dream_garden/code/checkpoints/06-tiles.py +++ b/games/demos/dream_garden/code/checkpoints/06-tiles.py @@ -6,17 +6,19 @@ class Game(vs2.Scene): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) - self.ground = self.world.tilemap("garden.png", columns=16, rows=16, + self.air = self.layer("air", projection=vs2.TUNNEL) + self.ground = self.air.tilemap("garden.png", columns=16, rows=16, view_width=256, view_height=160) for row in range(16): for col in range(16): band = row % 6 self.ground[col, row] = ((col + band) % 8 if (col + band * 3) % 5 == 0 else vs2.EMPTY_TILE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) + self.plant_side = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -29,6 +31,10 @@ def update(self): 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 frame = int(self.dream_time // 3) % self.image("seed.png").frames + for flower in self.flowers: + flower.y += self.speed * 0.08 + if flower.y >= 255: + self.flowers.despawn(flower) for seed in self.seeds: seed.y -= self.speed seed.frame = frame @@ -42,9 +48,11 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.image("flowers.png").width // 2, - y=24, frame=randrange(4) * 2) + centre = self.dreamer.x + self.dreamer.width // 2 + centre += self.plant_side * self.dreamer.width * 0.75 + self.plant_side = -self.plant_side + self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, + y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) def main(): diff --git a/games/demos/dream_garden/code/checkpoints/07-scroll.py b/games/demos/dream_garden/code/checkpoints/07-scroll.py index 5238e729..d67a27fa 100644 --- a/games/demos/dream_garden/code/checkpoints/07-scroll.py +++ b/games/demos/dream_garden/code/checkpoints/07-scroll.py @@ -6,17 +6,19 @@ class Game(vs2.Scene): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) - self.ground = self.world.tilemap("garden.png", columns=16, rows=16, + self.air = self.layer("air", projection=vs2.TUNNEL) + self.ground = self.air.tilemap("garden.png", columns=16, rows=16, view_width=256, view_height=160) for row in range(16): for col in range(16): band = row % 6 self.ground[col, row] = ((col + band) % 8 if (col + band * 3) % 5 == 0 else vs2.EMPTY_TILE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) + self.plant_side = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -32,6 +34,10 @@ def update(self): frame = int(self.dream_time // 3) % self.image("seed.png").frames self.scroll = (self.scroll + self.speed) % (6 * self.ground.tile_height) self.ground.view_y = self.scroll + for flower in self.flowers: + flower.y += self.speed * 0.08 + if flower.y >= 255: + self.flowers.despawn(flower) for seed in self.seeds: seed.y -= self.speed seed.frame = frame @@ -45,9 +51,11 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.image("flowers.png").width // 2, - y=24, frame=randrange(4) * 2) + centre = self.dreamer.x + self.dreamer.width // 2 + centre += self.plant_side * self.dreamer.width * 0.75 + self.plant_side = -self.plant_side + self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, + y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) def main(): diff --git a/games/demos/dream_garden/code/checkpoints/08-hud.py b/games/demos/dream_garden/code/checkpoints/08-hud.py index 83673bd2..a14c3e77 100644 --- a/games/demos/dream_garden/code/checkpoints/08-hud.py +++ b/games/demos/dream_garden/code/checkpoints/08-hud.py @@ -6,17 +6,19 @@ class Game(vs2.Scene): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) - self.ground = self.world.tilemap("garden.png", columns=16, rows=16, + self.air = self.layer("air", projection=vs2.TUNNEL) + self.ground = self.air.tilemap("garden.png", columns=16, rows=16, view_width=256, view_height=160) for row in range(16): for col in range(16): band = row % 6 self.ground[col, row] = ((col + band) % 8 if (col + band * 3) % 5 == 0 else vs2.EMPTY_TILE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) + self.plant_side = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -24,7 +26,7 @@ def build(self): self.scroll = 0 hud = self.layer("hud", projection=vs2.HUD) self.mood = hud.label("steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="DRIFT") + flip_x=True, flip_y=True, text="RELAX", visible=False) self.slow = False self.slow_ready = not joy1.held(A) @@ -34,7 +36,7 @@ def update(self): slow = self.slow_ready and joy1.held(A) if slow != self.slow: self.slow = slow - self.mood.text = "SLOW" if slow else "DRIFT" + self.mood.visible = slow self.speed = 0.12 if slow else 0.45 self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) @@ -45,6 +47,10 @@ def update(self): frame = int(self.dream_time // 3) % self.image("seed.png").frames self.scroll = (self.scroll + self.speed) % (6 * self.ground.tile_height) self.ground.view_y = self.scroll + for flower in self.flowers: + flower.y += self.speed * 0.08 + if flower.y >= 255: + self.flowers.despawn(flower) for seed in self.seeds: seed.y -= self.speed seed.frame = frame @@ -58,9 +64,11 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.image("flowers.png").width // 2, - y=24, frame=randrange(4) * 2) + centre = self.dreamer.x + self.dreamer.width // 2 + centre += self.plant_side * self.dreamer.width * 0.75 + self.plant_side = -self.plant_side + self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, + y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) def main(): diff --git a/games/demos/dream_garden/code/checkpoints/09-scenes.py b/games/demos/dream_garden/code/checkpoints/09-scenes.py index 143f544a..9375baac 100644 --- a/games/demos/dream_garden/code/checkpoints/09-scenes.py +++ b/games/demos/dream_garden/code/checkpoints/09-scenes.py @@ -6,17 +6,19 @@ class Game(vs2.Scene): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) - self.ground = self.world.tilemap("garden.png", columns=16, rows=16, + self.air = self.layer("air", projection=vs2.TUNNEL) + self.ground = self.air.tilemap("garden.png", columns=16, rows=16, view_width=256, view_height=160) for row in range(16): for col in range(16): band = row % 6 self.ground[col, row] = ((col + band) % 8 if (col + band * 3) % 5 == 0 else vs2.EMPTY_TILE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) + self.plant_side = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -24,19 +26,19 @@ def build(self): self.scroll = 0 hud = self.layer("hud", projection=vs2.HUD) self.mood = hud.label("steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="DRIFT") + flip_x=True, flip_y=True, text="RELAX", visible=False) self.slow = False self.slow_ready = not joy1.held(A) def update(self): if joy1.just_pressed(B): - return self.switch(Rest([(flower.x, flower.frame) for flower in self.flowers])) + return self.switch(Rest([(flower.x, flower.y, flower.frame) for flower in self.flowers])) if not joy1.held(A): self.slow_ready = True slow = self.slow_ready and joy1.held(A) if slow != self.slow: self.slow = slow - self.mood.text = "SLOW" if slow else "DRIFT" + self.mood.visible = slow self.speed = 0.12 if slow else 0.45 self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) @@ -47,6 +49,10 @@ def update(self): frame = int(self.dream_time // 3) % self.image("seed.png").frames self.scroll = (self.scroll + self.speed) % (6 * self.ground.tile_height) self.ground.view_y = self.scroll + for flower in self.flowers: + flower.y += self.speed * 0.08 + if flower.y >= 255: + self.flowers.despawn(flower) for seed in self.seeds: seed.y -= self.speed seed.frame = frame @@ -60,9 +66,11 @@ def spawn_seed(self): self.call_later(self.spawn_ms, self.spawn_seed) def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.image("flowers.png").width // 2, - y=24, frame=randrange(4) * 2) + centre = self.dreamer.x + self.dreamer.width // 2 + centre += self.plant_side * self.dreamer.width * 0.75 + self.plant_side = -self.plant_side + self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, + y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) def label(hud, text, y): @@ -73,7 +81,7 @@ class Title(vs2.Scene): def build(self): hud = self.layer("hud", projection=vs2.HUD) label(hud, "DREAM GARDEN", 1) - label(hud, "A TO DRIFT", 13) + label(hud, "A TO BEGIN", 13) def update(self): if joy1.just_pressed(A): @@ -87,8 +95,8 @@ def __init__(self, flowers): def build(self): world = self.layer("world", projection=vs2.TUNNEL) flowers = world.sprite_pool("flowers.png", count=24) - for x, frame in self.saved_flowers: - flowers.spawn(x=x, y=48, frame=frame) + for x, y, frame in self.saved_flowers: + flowers.spawn(x=x, y=y, frame=frame) hud = self.layer("hud", projection=vs2.HUD) label(hud, "REST A WHILE", 1) label(hud, "A: NEW DREAM", 14) diff --git a/games/demos/dream_garden/code/checkpoints/10-shared.py b/games/demos/dream_garden/code/checkpoints/10-shared.py index a9e5ffff..4c1b1795 100644 --- a/games/demos/dream_garden/code/checkpoints/10-shared.py +++ b/games/demos/dream_garden/code/checkpoints/10-shared.py @@ -1,13 +1,13 @@ """Dream Garden: a shared, unhurried circular garden for Overkill 2026. -LEFT/RIGHT drift, hold A to slow the whole garden, B rests, X toggles audio. +LEFT/RIGHT drift, hold A to relax the whole garden, B rests. A second controller can join at any time. Watching is also a way to play. """ from urandom import randrange import vs2 -from vs2.controls import A, B, X, LEFT, RIGHT, joy1, joy2 +from vs2.controls import A, B, LEFT, RIGHT, joy1, joy2 AWAKE, LEFT_POSE, RIGHT_POSE, ASLEEP = range(4) DRIFT_SPEED = 0.45 @@ -17,6 +17,10 @@ SEED_DEPTH = 160 FLOWER_COUNT = 24 PATTERN_ROWS = 6 +FLOWER_DRIFT_RATIO = 0.08 +FLOWER_DEPTH = 255 +GLOW_FRAMES = (1, 2, 3, 2, 1, 0) +GLOW_FRAME_TICKS = 6 def garden_tiles(world): @@ -46,26 +50,28 @@ def __init__(self, joined=False): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) + self.air = self.layer("air", projection=vs2.TUNNEL) self.hud = self.layer("hud", projection=vs2.HUD) - self.ground = garden_tiles(self.world) + self.ground = garden_tiles(self.air) self.flowers = self.world.sprite_pool( "flowers.png", count=FLOWER_COUNT, on_empty=vs2.RECYCLE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) self.friend = self.world.sprite("dreamer.png", x=64 - self.dreamer.width // 2, y=0, frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="DRIFT") + flip_x=True, flip_y=True, text="RELAX", visible=False) self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 self.dream_time = 0 self.slow = False + self.plant_side1 = 1 + self.plant_side2 = 1 + self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS - for index in range(4): - self.flowers.spawn(x=32 + index * 64 - self.flower_half, y=24, frame=index * 2) self.call_later(self.spawn_ms, self.spawn_seed) def steer(self, dreamer, controller, base_frame, sleeping): @@ -78,7 +84,7 @@ def steer(self, dreamer, controller, base_frame, sleeping): def update(self): if joy1.just_pressed(B) or joy2.just_pressed(B): - flowers = [(flower.x, flower.frame) for flower in self.flowers] + flowers = [(flower.x, flower.y, flower.frame) for flower in self.flowers] return self.switch(Rest(flowers)) if not self.joined and (joy2.held(LEFT) or joy2.held(RIGHT) @@ -98,7 +104,7 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.text = "SLOW" if slow else "DRIFT" + self.mood.visible = slow self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 @@ -110,25 +116,53 @@ def update(self): self.flower_phase = phase for flower in self.flowers: flower.frame = (flower.frame // 2) * 2 + phase + for flower in self.flowers: + flower.y += self.speed * FLOWER_DRIFT_RATIO + if flower.y >= FLOWER_DEPTH: + self.flowers.despawn(flower) self.move_seeds() - - def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.flower_half, y=24, + def bloom(self, seed, player): + side = self.plant_side2 if player is self.friend else self.plant_side1 + centre = player.x + player.width // 2 + self.flowers.spawn(x=(centre + side * player.width * 0.75 + - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - self.seeds.despawn(seed) + if player is self.friend: + self.plant_side2 = -side + else: + self.plant_side1 = -side + self.retire_seed(seed) def move_seeds(self): - frame = int(self.dream_time // 3) % self.image("seed.png").frames for seed in self.seeds: seed.y -= self.speed - seed.frame = frame - if (seed.overlaps(self.dreamer) - or (self.joined and seed.overlaps(self.friend))): - self.bloom(seed) + self.glow_seed(seed) + if seed.overlaps(self.dreamer): + self.bloom(seed, self.dreamer) + elif self.joined and seed.overlaps(self.friend): + self.bloom(seed, self.friend) elif seed.y < -seed.image.height: - self.seeds.despawn(seed) # passing seeds cause no harm + self.retire_seed(seed) # passing seeds cause no harm + + def glow_seed(self, seed): + state = self.seed_glows.get(seed) + if state is None: + state = [randrange(90, 241), 0] + self.seed_glows[seed] = state + if state[0] > 0: + state[0] -= 1 + seed.frame = 0 + return + seed.frame = GLOW_FRAMES[state[1] // GLOW_FRAME_TICKS] + state[1] += 1 + if state[1] == len(GLOW_FRAMES) * GLOW_FRAME_TICKS: + state[0] = randrange(90, 241) + state[1] = 0 + + def retire_seed(self, seed): + self.seed_glows.pop(seed, None) + self.seeds.despawn(seed) def spawn_seed(self): self.seeds.spawn(x=randrange(vs2.display.width), y=SEED_DEPTH) @@ -140,8 +174,8 @@ class Title(vs2.Scene): def build(self): hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "DREAM GARDEN", y=1) - centred_label(hud, "A TO DRIFT", y=13) - centred_label(hud, "HOLD A: SLOW", y=25) + centred_label(hud, "A TO BEGIN", y=13) + centred_label(hud, "HOLD A: RELAX", y=25) def update(self): if joy1.just_pressed(A) or joy2.just_pressed(A): @@ -156,9 +190,8 @@ def __init__(self, flowers): def build(self): world = self.layer("world", projection=vs2.TUNNEL) flowers = world.sprite_pool("flowers.png", count=FLOWER_COUNT) - for x, frame in self.saved_flowers: - # Settle inward, leaving the rim clear for the invitation. - flowers.spawn(x=x, y=48, frame=frame) + for x, y, frame in self.saved_flowers: + flowers.spawn(x=x, y=y, frame=frame) hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "REST A WHILE", y=1) centred_label(hud, "A: NEW DREAM", y=14) diff --git a/games/demos/dream_garden/code/checkpoints/11-audio.py b/games/demos/dream_garden/code/checkpoints/11-audio.py index fb262510..52247101 100644 --- a/games/demos/dream_garden/code/checkpoints/11-audio.py +++ b/games/demos/dream_garden/code/checkpoints/11-audio.py @@ -5,6 +5,7 @@ """ from urandom import randrange +from utime import ticks_ms, ticks_add, ticks_diff import vs2 from vs2.controls import A, B, X, LEFT, RIGHT, joy1, joy2 @@ -17,6 +18,10 @@ SEED_DEPTH = 160 FLOWER_COUNT = 24 PATTERN_ROWS = 6 +FLOWER_DRIFT_RATIO = 0.08 +FLOWER_DEPTH = 255 +GLOW_FRAMES = (1, 2, 3, 2, 1, 0) +GLOW_FRAME_TICKS = 6 def garden_tiles(world): @@ -46,27 +51,33 @@ def __init__(self, joined=False): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) + self.air = self.layer("air", projection=vs2.TUNNEL) self.hud = self.layer("hud", projection=vs2.HUD) - self.ground = garden_tiles(self.world) + self.ground = garden_tiles(self.air) self.flowers = self.world.sprite_pool( "flowers.png", count=FLOWER_COUNT, on_empty=vs2.RECYCLE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) self.friend = self.world.sprite("dreamer.png", x=64 - self.dreamer.width // 2, y=0, frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="DRIFT") + flip_x=True, flip_y=True, text="RELAX", visible=False) + self.sound_notice = self.hud.label("steel8x8.png", columns=9, x=92, y=11, + flip_x=True, flip_y=True, visible=False) + self.sound_notice_until = None self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 self.dream_time = 0 self.slow = False - self.sound_on = False + self.sound_on = True + vs2.audio.music("lullaby", loop=True) + self.plant_side1 = 1 + self.plant_side2 = 1 + self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS - for index in range(4): - self.flowers.spawn(x=32 + index * 64 - self.flower_half, y=24, frame=index * 2) self.call_later(self.spawn_ms, self.spawn_seed) def steer(self, dreamer, controller, base_frame, sleeping): @@ -79,7 +90,7 @@ def steer(self, dreamer, controller, base_frame, sleeping): def update(self): if joy1.just_pressed(B) or joy2.just_pressed(B): - flowers = [(flower.x, flower.frame) for flower in self.flowers] + flowers = [(flower.x, flower.y, flower.frame) for flower in self.flowers] vs2.audio.stop_music() return self.switch(Rest(flowers)) @@ -100,7 +111,7 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.text = "SLOW" if slow else "DRIFT" + self.mood.visible = slow self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 @@ -112,33 +123,69 @@ def update(self): self.flower_phase = phase for flower in self.flowers: flower.frame = (flower.frame // 2) * 2 + phase + for flower in self.flowers: + flower.y += self.speed * FLOWER_DRIFT_RATIO + if flower.y >= FLOWER_DEPTH: + self.flowers.despawn(flower) self.move_seeds() + if (self.sound_notice_until is not None + and ticks_diff(ticks_ms(), self.sound_notice_until) >= 0): + self.sound_notice.hide() + self.sound_notice_until = None if joy1.just_pressed(X) or joy2.just_pressed(X): self.sound_on = not self.sound_on if self.sound_on: vs2.audio.music("lullaby", loop=True) else: vs2.audio.stop_music() - - def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.flower_half, y=24, + self.sound_notice.text = "SOUND ON" if self.sound_on else "SOUND OFF" + self.sound_notice.show() + self.sound_notice_until = ticks_add(ticks_ms(), 2000) + + def bloom(self, seed, player): + side = self.plant_side2 if player is self.friend else self.plant_side1 + centre = player.x + player.width // 2 + self.flowers.spawn(x=(centre + side * player.width * 0.75 + - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - self.seeds.despawn(seed) + if player is self.friend: + self.plant_side2 = -side + else: + self.plant_side1 = -side + self.retire_seed(seed) if self.sound_on: vs2.audio.sound("bloom") def move_seeds(self): - frame = int(self.dream_time // 3) % self.image("seed.png").frames for seed in self.seeds: seed.y -= self.speed - seed.frame = frame - if (seed.overlaps(self.dreamer) - or (self.joined and seed.overlaps(self.friend))): - self.bloom(seed) + self.glow_seed(seed) + if seed.overlaps(self.dreamer): + self.bloom(seed, self.dreamer) + elif self.joined and seed.overlaps(self.friend): + self.bloom(seed, self.friend) elif seed.y < -seed.image.height: - self.seeds.despawn(seed) # passing seeds cause no harm + self.retire_seed(seed) # passing seeds cause no harm + + def glow_seed(self, seed): + state = self.seed_glows.get(seed) + if state is None: + state = [randrange(90, 241), 0] + self.seed_glows[seed] = state + if state[0] > 0: + state[0] -= 1 + seed.frame = 0 + return + seed.frame = GLOW_FRAMES[state[1] // GLOW_FRAME_TICKS] + state[1] += 1 + if state[1] == len(GLOW_FRAMES) * GLOW_FRAME_TICKS: + state[0] = randrange(90, 241) + state[1] = 0 + + def retire_seed(self, seed): + self.seed_glows.pop(seed, None) + self.seeds.despawn(seed) def spawn_seed(self): self.seeds.spawn(x=randrange(vs2.display.width), y=SEED_DEPTH) @@ -150,8 +197,8 @@ class Title(vs2.Scene): def build(self): hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "DREAM GARDEN", y=1) - centred_label(hud, "A TO DRIFT", y=13) - centred_label(hud, "HOLD A: SLOW", y=25) + centred_label(hud, "A TO BEGIN", y=13) + centred_label(hud, "HOLD A: RELAX", y=25) def update(self): if joy1.just_pressed(A) or joy2.just_pressed(A): @@ -166,9 +213,8 @@ def __init__(self, flowers): def build(self): world = self.layer("world", projection=vs2.TUNNEL) flowers = world.sprite_pool("flowers.png", count=FLOWER_COUNT) - for x, frame in self.saved_flowers: - # Settle inward, leaving the rim clear for the invitation. - flowers.spawn(x=x, y=48, frame=frame) + for x, y, frame in self.saved_flowers: + flowers.spawn(x=x, y=y, frame=frame) hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "REST A WHILE", y=1) centred_label(hud, "A: NEW DREAM", y=14) diff --git a/games/demos/dream_garden/code/dream_garden.py b/games/demos/dream_garden/code/dream_garden.py index fb262510..52247101 100644 --- a/games/demos/dream_garden/code/dream_garden.py +++ b/games/demos/dream_garden/code/dream_garden.py @@ -5,6 +5,7 @@ """ from urandom import randrange +from utime import ticks_ms, ticks_add, ticks_diff import vs2 from vs2.controls import A, B, X, LEFT, RIGHT, joy1, joy2 @@ -17,6 +18,10 @@ SEED_DEPTH = 160 FLOWER_COUNT = 24 PATTERN_ROWS = 6 +FLOWER_DRIFT_RATIO = 0.08 +FLOWER_DEPTH = 255 +GLOW_FRAMES = (1, 2, 3, 2, 1, 0) +GLOW_FRAME_TICKS = 6 def garden_tiles(world): @@ -46,27 +51,33 @@ def __init__(self, joined=False): def build(self): self.world = self.layer("world", projection=vs2.TUNNEL) + self.air = self.layer("air", projection=vs2.TUNNEL) self.hud = self.layer("hud", projection=vs2.HUD) - self.ground = garden_tiles(self.world) + self.ground = garden_tiles(self.air) self.flowers = self.world.sprite_pool( "flowers.png", count=FLOWER_COUNT, on_empty=vs2.RECYCLE) - self.seeds = self.world.sprite_pool("seed.png", count=16) + self.seeds = self.air.sprite_pool("seed.png", count=16) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) self.friend = self.world.sprite("dreamer.png", x=64 - self.dreamer.width // 2, y=0, frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, - flip_x=True, flip_y=True, text="DRIFT") + flip_x=True, flip_y=True, text="RELAX", visible=False) + self.sound_notice = self.hud.label("steel8x8.png", columns=9, x=92, y=11, + flip_x=True, flip_y=True, visible=False) + self.sound_notice_until = None self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 self.dream_time = 0 self.slow = False - self.sound_on = False + self.sound_on = True + vs2.audio.music("lullaby", loop=True) + self.plant_side1 = 1 + self.plant_side2 = 1 + self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS - for index in range(4): - self.flowers.spawn(x=32 + index * 64 - self.flower_half, y=24, frame=index * 2) self.call_later(self.spawn_ms, self.spawn_seed) def steer(self, dreamer, controller, base_frame, sleeping): @@ -79,7 +90,7 @@ def steer(self, dreamer, controller, base_frame, sleeping): def update(self): if joy1.just_pressed(B) or joy2.just_pressed(B): - flowers = [(flower.x, flower.frame) for flower in self.flowers] + flowers = [(flower.x, flower.y, flower.frame) for flower in self.flowers] vs2.audio.stop_music() return self.switch(Rest(flowers)) @@ -100,7 +111,7 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.text = "SLOW" if slow else "DRIFT" + self.mood.visible = slow self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 @@ -112,33 +123,69 @@ def update(self): self.flower_phase = phase for flower in self.flowers: flower.frame = (flower.frame // 2) * 2 + phase + for flower in self.flowers: + flower.y += self.speed * FLOWER_DRIFT_RATIO + if flower.y >= FLOWER_DEPTH: + self.flowers.despawn(flower) self.move_seeds() + if (self.sound_notice_until is not None + and ticks_diff(ticks_ms(), self.sound_notice_until) >= 0): + self.sound_notice.hide() + self.sound_notice_until = None if joy1.just_pressed(X) or joy2.just_pressed(X): self.sound_on = not self.sound_on if self.sound_on: vs2.audio.music("lullaby", loop=True) else: vs2.audio.stop_music() - - def bloom(self, seed): - centre = seed.x + seed.width // 2 - self.flowers.spawn(x=centre - self.flower_half, y=24, + self.sound_notice.text = "SOUND ON" if self.sound_on else "SOUND OFF" + self.sound_notice.show() + self.sound_notice_until = ticks_add(ticks_ms(), 2000) + + def bloom(self, seed, player): + side = self.plant_side2 if player is self.friend else self.plant_side1 + centre = player.x + player.width // 2 + self.flowers.spawn(x=(centre + side * player.width * 0.75 + - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - self.seeds.despawn(seed) + if player is self.friend: + self.plant_side2 = -side + else: + self.plant_side1 = -side + self.retire_seed(seed) if self.sound_on: vs2.audio.sound("bloom") def move_seeds(self): - frame = int(self.dream_time // 3) % self.image("seed.png").frames for seed in self.seeds: seed.y -= self.speed - seed.frame = frame - if (seed.overlaps(self.dreamer) - or (self.joined and seed.overlaps(self.friend))): - self.bloom(seed) + self.glow_seed(seed) + if seed.overlaps(self.dreamer): + self.bloom(seed, self.dreamer) + elif self.joined and seed.overlaps(self.friend): + self.bloom(seed, self.friend) elif seed.y < -seed.image.height: - self.seeds.despawn(seed) # passing seeds cause no harm + self.retire_seed(seed) # passing seeds cause no harm + + def glow_seed(self, seed): + state = self.seed_glows.get(seed) + if state is None: + state = [randrange(90, 241), 0] + self.seed_glows[seed] = state + if state[0] > 0: + state[0] -= 1 + seed.frame = 0 + return + seed.frame = GLOW_FRAMES[state[1] // GLOW_FRAME_TICKS] + state[1] += 1 + if state[1] == len(GLOW_FRAMES) * GLOW_FRAME_TICKS: + state[0] = randrange(90, 241) + state[1] = 0 + + def retire_seed(self, seed): + self.seed_glows.pop(seed, None) + self.seeds.despawn(seed) def spawn_seed(self): self.seeds.spawn(x=randrange(vs2.display.width), y=SEED_DEPTH) @@ -150,8 +197,8 @@ class Title(vs2.Scene): def build(self): hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "DREAM GARDEN", y=1) - centred_label(hud, "A TO DRIFT", y=13) - centred_label(hud, "HOLD A: SLOW", y=25) + centred_label(hud, "A TO BEGIN", y=13) + centred_label(hud, "HOLD A: RELAX", y=25) def update(self): if joy1.just_pressed(A) or joy2.just_pressed(A): @@ -166,9 +213,8 @@ def __init__(self, flowers): def build(self): world = self.layer("world", projection=vs2.TUNNEL) flowers = world.sprite_pool("flowers.png", count=FLOWER_COUNT) - for x, frame in self.saved_flowers: - # Settle inward, leaving the rim clear for the invitation. - flowers.spawn(x=x, y=48, frame=frame) + for x, y, frame in self.saved_flowers: + flowers.spawn(x=x, y=y, frame=frame) hud = self.layer("hud", projection=vs2.HUD) centred_label(hud, "REST A WHILE", y=1) centred_label(hud, "A: NEW DREAM", y=14) diff --git a/games/demos/dream_garden/images/garden.png b/games/demos/dream_garden/images/garden.png index 8e8adc44a2200c81e77c69053bf9082af1a0f152..e7723de77fd9e3e1ab6b559a1213b046c1b5e41c 100644 GIT binary patch delta 1043 zcmV+u1nm3K5WEPGIDZ4~Nkl%_x1}!S#)!Tk&R6k!vxu^LpLgI z1(bC*WRZ-K%x#TM-4R+6GaE3Z*S6D+P^~UyPa{0vm2|i1iASP*U343d{bJ zXKAZAmu%c~HCwXW&(G(1-gDk_-uIkCh>D7eii(Pg%HNRvihmXVMRN36yA;ROEi)2w?W0?5!|BK<4$TasV83Tkx1#VfMRVb}a!_3fzP{}R zFR1WGj=#~(^7{{92PnhHJv;@-Nr-+Kz5&Q872q|Xr&_8d+k?C7#h}^>AiE*s5 z9K`y%Zq(kn0YbLR_Tu>sXb?NlGUbI!?83hBGoaKUTYs)Q9G ziehQzo4wG}ZO9SIFz4{H@7cyP^%)VZ0H-SfAqA+tb`C3R4l|qSsR`1_0Ftf!cr9n? zcDAwTtMulFd*jH@`}m3RUzCfo;!|xgX7AsIMv&jJ{*3meT3A9usPF_}Zukk#nGx8+ zqo73IwSPdV1%XHqPO%%dF%Mj#A7}6NFwf;XdkmfXB=8*DI%1&^lDm(kHBzS^+`|rh z-}Jb!|B+NcZFUg|DFxM(GX-C_`Xj`wr;zaWV%T8ilKam26K*$)F>4U4@EJbyJZX%F7?p?T7Wk}K!XBzkctG6>g{ zpUH#&%e5)cw-p`cg!eo+QYv4o%PEI%CVSF8(ARsw^w!%8p{e>Vg!y z3@xpc>9MjLL}LJAC~3He~kIRjA6j> z0EA|8r<7>j9tdPd#)GRwlVJ~=-}*qN`U|>R|MhR+8*5UpZX?Rx%A95vG}H*5rmisM zaMXwmd)t{kD8Ch&7avvbv!TX#fjtLYF?gcv3#p=_qN1XrLjL#s0jsX`MfVPf9}EBh N002ovPDHLkV1n0-9GUJH;ciX3^IzOeL5j-R2s3eOi}|I)6pOs}Er)V-&RWcI z>6n%Vv%S}RdOSU~>H7%COJT>IEIbX{7!?(F8-wlwR?8AHJBH2SYtN89ex zaap{Amw)48h_5-==!4<)JydvYrlmZjLGJS%7EpFn;nqUxOv7TOsAKjmgl10 z&c%5CZwSIbO**33kHGRmwp0~Tqh7`T|0gM;b7$+Z}BS^AJMS4 z?MqEsI)cz0KktR;1Q*Pkz78|&_< zr+-(V=J;b{-LFx;cJf%sigN`lyH>zQmyROqh9K+RSbqIF1!^{a75Pj`I@h44q<19z zP8zZ0>4cUgvAZk{zi$%=FHIz@ED?Lx>6&~BUs=w1gTnD(6XA<1v2O9l26?tN&eY7^ zNX?p1%2#i%vmNCy2_+#7QE;jIeR%x)a zTI<{}ZQYNueLrLzLH#rpNpyNd$F$P5+kLjs~Ys9CUpi? zkq`I%Zph*&i~c|7wg5>Qz@n_HwD01}d&hI}PB_5ezs90(U3>k%(4fY%V|<9XoqxhrxpZ)SZF%kPIT(tPMo+l?f}YaQx4(Wu+f zxG{fQ9aomz7}?$%H8u++>M(gz9)G<>ocg|v>9!f=ZZ)^ERPtkZ?7Dul_p|1WQx<%x z-Gk$O8>tWGYnkK+)=u~RwY3D@XlAa@Va{ggjJ+6Hlt;@J4R88EgsjP<>by!xwwcmwGgnj15f_189ob#F5f;|GzjGK8uLpP-~1p{G0s zQE1?e<5#^V(Do1Gv486{hOX0B(XyQfgTr>>85N>A2zsj#5SosfaRenklUBChRk2~J z<>5n;T)AMVCND;z^b0dLjw;+ZXyRYdDmGS@EAbbtypV(r7!NPy+4l95rDqrIm0Szw zmp;aGK@{Rc$$E>seVCJDno`3qSr$wD(8!EY+sc?9U zk-yLV<`H{`mR_2Dxw~cq)w6@p3xg2_X9UrU$|YMdeHvz2qu;!D*=76^z1{=8-WlgD zpCO3adgl+Y(ho;3->?3vx5B8N8Dvqz-fk*qcACkLSILV~$&FMvve~FLRQy^n)^ z8z-`Y?y&x~%h=hCZnFPgU8YevJpf6p|Jf}Ht`HDvS+~`5-bnJ`pY`eYVaU3H$g(>Q z_KkbV8t}X)j$O4^?PDPh!l9Gpq3=P+IyYpwai5)y!7044`!s9M|I@7h>De`-8O;#> aPw*dUkm1=j*0iBL{Q4GJ0x0000DNk~Le0000a000092nGNE09rDRaFHPwe*p2ZtL$j&Fm?vZ|5uJL|9^F! z_5T@(mN+f@e>3U-)5&4DwO(SLiRAi|yzXYv1kb$7x4Ym06EYbrY8 z)bRK2^#6bEPWfNsgx%5q55)iffB%2ae5GTg^Z)ve0^I6E*gXE9sIew0q5j0+t0KiBL{Q4GJ0x0000DNk~Le0000m0000C2nGNE03^CTQIR1Re*#EJ zL_t(YiQSc5NK;`L$LE?eKhUDPF6<(&`kgMPSeeK|&w|wo;o#qeWWYOgPS+eYl--9{%BW73>^Ob`kc!Jp9i8dH>J* zyzi?}{BPm{{ZOoDC4Gl80g?kIf28jiP_}tL!h?o?5-R91t*vgi+w@@Wn&C| zD0m&hm>ng>mQ@S(w4cc|X2?-1$)=y4f-rR+6mlUxYDa#-E}6Tcw1FS9gMV=n;XbS6 zPc(TB6rTen7eJpo;qPt~OYk_zY<$E$vD)?JPl$VE8br|A-j;OupZ2S5u7h~h^c#Z mAi`h0C(DnT*7qZuOTPeKi(MD?(=R&!0000({input, left:index*size,top:0}))).png().toFile(path.join(directory,'images',name)); @@ -43,9 +46,11 @@ async function strip(name, row, columns, size, padding=1) { atlas = await sharp(source).ensureAlpha().raw().toBuffer({resolveWithObject: true}); // Generated lean poses face right then left, so swap them in each set. await strip('dreamer.png',0,[0,2,1,3,4,6,5,7],19); - await strip('seed.png',1,[0,1,2,3],12); + // Subdued at rest; the game occasionally plays a brightening pulse. + await strip('seed.png',1,[0,1,2,3],9,1,[0.85,1,1.15,1.3]); await strip('flowers.png',2,[0,1,2,3,4,5,6,7],17); - await strip('garden.png',3,[0,1,2,3,4,5,6,7],16,2); + // Keep 16-pixel tile spacing while shrinking and dimming the flying accents. + await strip('garden.png',3,[0,1,2,3,4,5,6,7],16,4,0.85); const icon = await frame(0,0,29,1); await sharp({create:{width:64,height:30,channels:4,background:'#00000000'}}) .composite([{input:icon,left:18,top:0}]).png() diff --git a/tests/test_dream_garden.py b/tests/test_dream_garden.py index 01fd80f0..698191a3 100644 --- a/tests/test_dream_garden.py +++ b/tests/test_dream_garden.py @@ -27,7 +27,7 @@ class _Utime: STRIPS = { "dreamer.png": (19, 19, 8, None), - "seed.png": (12, 12, 4, None), + "seed.png": (9, 9, 4, None), "flowers.png": (17, 17, 8, None), "garden.png": (16, 16, 8, None), "steel8x8.png": (8, 8, 256, None), @@ -97,6 +97,93 @@ def test_steering_wraps_and_opposites_cancel(self): self.step(LEFT | RIGHT) self.assertEqual(garden.dreamer.x, 255) + def test_empty_garden_and_three_ordered_layers(self): + garden = self.start() + self.assertEqual(len(garden.flowers), 0) + self.assertEqual([layer.name for layer in garden.layers], ["world", "air", "hud"]) + self.assertIs(garden.dreamer.layer, garden.world) + self.assertIs(garden.friend.layer, garden.world) + self.assertTrue(all(sprite.layer is garden.world for sprite in garden.flowers._free)) + self.assertIs(garden.ground.layer, garden.air) + self.assertTrue(all(sprite.layer is garden.air for sprite in garden.seeds._free)) + # All reserved flowers precede both dreamers in the lower layer. + self.assertEqual(garden.world.sprites[-2:], [garden.dreamer, garden.friend]) + self.assertEqual(garden.hud.tilemaps, [garden.mood, garden.sound_notice]) + self.assertFalse(garden.mood.visible) + self.assertFalse(garden.sound_notice.visible) + + def test_planting_alternates_per_player_at_their_depth_and_wraps(self): + garden = self.start(player2=A) + garden.dreamer.y = 12 + garden.friend.y = 20 + for player in (garden.dreamer, garden.friend, garden.dreamer, garden.friend): + side = garden.plant_side2 if player is garden.friend else garden.plant_side1 + seed = garden.seeds.spawn(x=100, y=100) + garden.bloom(seed, player) + flower = garden.flowers._newest + self.assertEqual(flower.y, player.y) + expected = (player.x + player.width // 2 + side * player.width * 0.75 + - garden.flower_half) % vs2.display.width + self.assertAlmostEqual(flower.x, expected) + self.assertEqual((garden.plant_side1, garden.plant_side2), (1, 1)) + + def test_flowers_drift_inward_much_slower_than_seeds_and_retire(self): + from games.demos.dream_garden.code.dream_garden import FLOWER_DRIFT_RATIO + garden = self.start() + flower = garden.flowers.spawn(x=100, y=20) + seed = garden.seeds.spawn(x=150, y=100) + for buttons in (0, A): + before_flower, before_seed = flower.y, seed.y + self.step(buttons) + self.assertAlmostEqual(flower.y - before_flower, garden.speed * FLOWER_DRIFT_RATIO) + self.assertAlmostEqual(before_seed - seed.y, garden.speed) + flower.y = 254.999 + self.step() + self.assertEqual(len(garden.flowers), 0) + + def test_seed_glow_is_random_independent_and_cleared_on_retirement(self): + from unittest.mock import patch + garden = self.start() + first = garden.seeds.spawn(x=100, y=100) + second = garden.seeds.spawn(x=150, y=100) + with patch.dict(garden.glow_seed.__globals__, {"randrange": lambda *args: 90}): + garden.glow_seed(first) + garden.glow_seed(second) + self.assertEqual(first.frame, 0) + garden.seed_glows[first][0] = 0 + frames = [] + for _ in range(36): + garden.glow_seed(first) + garden.glow_seed(second) + frames.append(first.frame) + self.assertEqual(second.frame, 0) + self.assertEqual(frames, [frame for frame in (1, 2, 3, 2, 1, 0) for _ in range(6)]) + self.assertEqual(garden.seed_glows[first], [90, 0]) + garden.retire_seed(first) + second.y = -10 + self.step() + self.assertEqual(garden.seed_glows, {}) + + def test_sound_toggle_notice_lasts_two_seconds_and_restarts_on_retoggling(self): + from unittest.mock import patch + garden = self.start() + now = [1000] + with patch.dict(garden.update.__globals__, {"ticks_ms": lambda: now[0]}): + self.step(X) + self.assertTrue(garden.sound_notice.visible) + self.assertEqual(garden.sound_notice_until, 3000) + now[0] = 2500 + self.step() + self.step(0, X) + self.assertEqual(garden.sound_notice.text, "SOUND ON") + self.assertEqual(garden.sound_notice_until, 4500) + now[0] = 4499 + self.step() + self.assertTrue(garden.sound_notice.visible) + now[0] = 4500 + self.step() + self.assertFalse(garden.sound_notice.visible) + self.assertIsNone(garden.sound_notice_until) def test_second_player_joins_and_moves_independently(self): garden = self.start() self.assertFalse(garden.friend.visible) @@ -122,10 +209,11 @@ def test_either_player_slows_every_seed_and_the_background(self): self.assertAlmostEqual(seed.y, 100 - SLOW_SPEED, places=2) self.assertAlmostEqual(garden.scroll - previous_scroll, SLOW_SPEED, places=2) self.assertEqual(garden.spawn_ms, SLOW_SPAWN_MS) - self.assertEqual(garden.mood.text, "SLOW") + self.assertEqual(garden.mood.text, "RELAX") + self.assertTrue(garden.mood.visible) self.assertEqual(garden.friend.frame, 7) self.step() - self.assertEqual(garden.mood.text, "DRIFT") + self.assertFalse(garden.mood.visible) self.step(A) self.assertTrue(garden.slow) self.assertEqual(garden.dreamer.frame, 3) @@ -149,7 +237,7 @@ def test_player_two_also_grows_flowers(self): def test_missing_a_seed_has_no_penalty(self): garden = self.start() before = len(list(garden.flowers)) - garden.seeds.spawn(x=100, y=-12) + garden.seeds.spawn(x=100, y=-9) self.step() self.assertEqual(len(list(garden.seeds)), 0) self.assertEqual(len(list(garden.flowers)), before) @@ -166,22 +254,26 @@ def test_full_pools_skip_seeds_and_recycle_flowers(self): garden.seeds.despawn_all() starting = [(flower.x, flower.frame) for flower in garden.flowers] for index in range(80): - garden.bloom(garden.seeds.spawn(x=130, y=10)) + garden.bloom(garden.seeds.spawn(x=130, y=10), garden.dreamer) self.assertTrue(all((flower.x, flower.frame) not in starting for flower in garden.flowers)) self.assertEqual(len(list(garden.flowers)), 24) self.assertEqual(garden._sprite_count, 42) - self.assertEqual(garden._tilemap_count, 2) + self.assertEqual(garden._tilemap_count, 3) vs2.export_scene_payload(garden) def test_rest_preserves_the_garden_and_a_begins_a_new_dream(self): from games.demos.dream_garden.code.dream_garden import Garden, Rest garden = self.start(keep_timer=True) self.assertEqual(len(garden.pending_calls), 1) - flowers = [(flower.x, flower.frame) for flower in garden.flowers] + garden.bloom(garden.seeds.spawn(x=130, y=10), garden.dreamer) + self.step(LEFT, count=10) + flowers = [(flower.x, flower.y, flower.frame) for flower in garden.flowers] self.step(B) rest = director.scene_stack[-1] self.assertIsInstance(rest, Rest) self.assertEqual(rest.saved_flowers, flowers) + self.assertEqual([(flower.x, flower.y, flower.frame) + for flower in rest.layers[0].sprites if flower.visible], flowers) self.assertEqual(garden.pending_calls, []) self.step() self.step(0, A) @@ -276,17 +368,20 @@ def test_packed_art_has_binary_alpha_and_host_tools_are_not_runtime_files(self): with Image.open(Path(ROOT) / "games/demos/dream_garden/images" / name) as image: self.assertLessEqual(set(image.convert("RGBA").getchannel("A").tobytes()), {0, 255}) - def test_audio_is_opt_in_and_stops_on_rest(self): + def test_audio_starts_on_toggles_and_stops_on_rest(self): garden = self.start() - self.assertFalse(garden.sound_on) - self.step(X) self.assertTrue(garden.sound_on) self.assertTrue(any(b"lullaby" in line for line, _ in director.platform.comms.sent)) - self.step() - self.step(X) + garden.bloom(garden.seeds.spawn(x=130, y=10), garden.dreamer) + self.assertTrue(any(b"bloom" in line for line, _ in director.platform.comms.sent)) + self.step(0, X) self.assertFalse(garden.sound_on) + self.assertEqual(garden.sound_notice.text, "SOUND OFF") + self.assertEqual(director.platform.comms.sent[-1][0], b"music off") self.step() self.step(X) + self.assertTrue(garden.sound_on) + self.assertEqual(garden.sound_notice.text, "SOUND ON") self.step(B) self.assertEqual(director.platform.comms.sent[-1][0], b"music off") @@ -300,9 +395,11 @@ def test_long_drift_keeps_resources_and_time_bounded(self): RIGHT if tick % 50 < 25 else 0) self.assertIs(director.scene_stack[-1], garden) self.assertEqual(garden._sprite_count, 42) - self.assertEqual(garden._tilemap_count, 2) + self.assertEqual(garden._tilemap_count, 3) self.assertLessEqual(len(list(garden.seeds)), 16) self.assertLessEqual(len(list(garden.flowers)), 24) + self.assertLessEqual(len(garden.seed_glows), 16) + self.assertTrue(all(seed in list(garden.seeds) for seed in garden.seed_glows)) self.assertLess(garden.dream_time, 96) self.assertLess(garden.scroll, 96) vs2.export_scene_payload(garden) From 550f76b917515955dfddd716cfa301efab797ac3 Mon Sep 17 00:00:00 2001 From: "Alejandro J. Cura" Date: Tue, 13 Oct 2026 10:42:21 -0300 Subject: [PATCH 2/3] Slide QUIET, NOISY and RELAX notices outward past the rim --- games/demos/dream_garden/README.md | 8 +-- .../dream_garden/code/checkpoints/08-hud.py | 23 ++++++++- .../code/checkpoints/09-scenes.py | 23 ++++++++- .../code/checkpoints/10-shared.py | 22 ++++++++- .../dream_garden/code/checkpoints/11-audio.py | 32 ++++++++++-- games/demos/dream_garden/code/dream_garden.py | 32 ++++++++++-- tests/test_dream_garden.py | 49 +++++++++++++++++-- 7 files changed, 170 insertions(+), 19 deletions(-) diff --git a/games/demos/dream_garden/README.md b/games/demos/dream_garden/README.md index 77b9692f..d201ce6a 100644 --- a/games/demos/dream_garden/README.md +++ b/games/demos/dream_garden/README.md @@ -34,8 +34,9 @@ Either player can slow seed movement, the drifting background and flower animation. New spawn intervals also lengthen, after the pending seed deadline. Flowers gradually replace the oldest ones when the garden fills up. -There is no death, score, countdown or difficulty ramp. Watching is welcome. Music and bloom sounds start on. X toggles both, showing SOUND ON or SOUND OFF -for two seconds; another toggle restarts that notice. B stops the music and keeps the current flowers in a still garden. +There is no death, score, countdown or difficulty ramp. Watching is welcome. Music and bloom sounds start on. X toggles both, showing NOISY or QUIET +for two seconds before the text slides outward past the rim over 450 ms. +Another toggle resets the position and restarts the notice. B stops the music and keeps the current flowers in a still garden. A starts a new garden from the Rest scene. ## How the example works @@ -50,7 +51,8 @@ The Garden graph uses **42 sprites, 3 tilemaps, 3 layers and 5 image strips**. The flower pool counts toward that cost even when its slots are hidden. The lower layer draws flowers first and players above them. The middle layer draws smaller, darker flying accents in 16-pixel tiles, followed by 9-pixel seeds. -The top HUD shows RELAX only while A is held, plus temporary audio feedback. +The top HUD shows RELAX while A is held, then slides it out on release, +plus temporary audio feedback. Both flips keep the top text upright. The full CP437 font comes from the existing tutorial example. PNGs and YAML diff --git a/games/demos/dream_garden/code/checkpoints/08-hud.py b/games/demos/dream_garden/code/checkpoints/08-hud.py index a14c3e77..092cb72f 100644 --- a/games/demos/dream_garden/code/checkpoints/08-hud.py +++ b/games/demos/dream_garden/code/checkpoints/08-hud.py @@ -2,6 +2,19 @@ import vs2 from vs2.controls import A, B, LEFT, RIGHT, joy1 from urandom import randrange +from utime import ticks_ms, ticks_diff + +HUD_EXIT_MS = 450 + + +def slide_text_out(label, start_y, started): + elapsed = ticks_diff(ticks_ms(), started) + if elapsed >= HUD_EXIT_MS: + label.hide() + return None + label.y = start_y - (start_y + label.tile_height) * elapsed / HUD_EXIT_MS + return started + class Game(vs2.Scene): def build(self): @@ -27,6 +40,7 @@ def build(self): hud = self.layer("hud", projection=vs2.HUD) self.mood = hud.label("steel8x8.png", columns=5, x=108, y=1, flip_x=True, flip_y=True, text="RELAX", visible=False) + self.mood_exit = None self.slow = False self.slow_ready = not joy1.held(A) @@ -36,7 +50,14 @@ def update(self): slow = self.slow_ready and joy1.held(A) if slow != self.slow: self.slow = slow - self.mood.visible = slow + if slow: + self.mood.y = 1 + self.mood.show() + self.mood_exit = None + else: + self.mood_exit = ticks_ms() + if self.mood_exit is not None: + self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = 0.12 if slow else 0.45 self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) diff --git a/games/demos/dream_garden/code/checkpoints/09-scenes.py b/games/demos/dream_garden/code/checkpoints/09-scenes.py index 9375baac..ecc072f5 100644 --- a/games/demos/dream_garden/code/checkpoints/09-scenes.py +++ b/games/demos/dream_garden/code/checkpoints/09-scenes.py @@ -2,6 +2,19 @@ import vs2 from vs2.controls import A, B, LEFT, RIGHT, joy1 from urandom import randrange +from utime import ticks_ms, ticks_diff + +HUD_EXIT_MS = 450 + + +def slide_text_out(label, start_y, started): + elapsed = ticks_diff(ticks_ms(), started) + if elapsed >= HUD_EXIT_MS: + label.hide() + return None + label.y = start_y - (start_y + label.tile_height) * elapsed / HUD_EXIT_MS + return started + class Game(vs2.Scene): def build(self): @@ -27,6 +40,7 @@ def build(self): hud = self.layer("hud", projection=vs2.HUD) self.mood = hud.label("steel8x8.png", columns=5, x=108, y=1, flip_x=True, flip_y=True, text="RELAX", visible=False) + self.mood_exit = None self.slow = False self.slow_ready = not joy1.held(A) @@ -38,7 +52,14 @@ def update(self): slow = self.slow_ready and joy1.held(A) if slow != self.slow: self.slow = slow - self.mood.visible = slow + if slow: + self.mood.y = 1 + self.mood.show() + self.mood_exit = None + else: + self.mood_exit = ticks_ms() + if self.mood_exit is not None: + self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = 0.12 if slow else 0.45 self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) diff --git a/games/demos/dream_garden/code/checkpoints/10-shared.py b/games/demos/dream_garden/code/checkpoints/10-shared.py index 4c1b1795..1fb21e34 100644 --- a/games/demos/dream_garden/code/checkpoints/10-shared.py +++ b/games/demos/dream_garden/code/checkpoints/10-shared.py @@ -5,6 +5,7 @@ """ from urandom import randrange +from utime import ticks_ms, ticks_diff import vs2 from vs2.controls import A, B, LEFT, RIGHT, joy1, joy2 @@ -21,6 +22,17 @@ FLOWER_DEPTH = 255 GLOW_FRAMES = (1, 2, 3, 2, 1, 0) GLOW_FRAME_TICKS = 6 +HUD_EXIT_MS = 450 + + +def slide_text_out(label, start_y, started): + elapsed = ticks_diff(ticks_ms(), started) + if elapsed >= HUD_EXIT_MS: + label.hide() + return None + label.y = start_y - (start_y + label.tile_height) * elapsed / HUD_EXIT_MS + return started + def garden_tiles(world): @@ -62,6 +74,7 @@ def build(self): frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, flip_x=True, flip_y=True, text="RELAX", visible=False) + self.mood_exit = None self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 @@ -104,7 +117,14 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.visible = slow + if slow: + self.mood.y = 1 + self.mood.show() + self.mood_exit = None + else: + self.mood_exit = ticks_ms() + if self.mood_exit is not None: + self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 diff --git a/games/demos/dream_garden/code/checkpoints/11-audio.py b/games/demos/dream_garden/code/checkpoints/11-audio.py index 52247101..95425bfa 100644 --- a/games/demos/dream_garden/code/checkpoints/11-audio.py +++ b/games/demos/dream_garden/code/checkpoints/11-audio.py @@ -22,6 +22,16 @@ FLOWER_DEPTH = 255 GLOW_FRAMES = (1, 2, 3, 2, 1, 0) GLOW_FRAME_TICKS = 6 +HUD_EXIT_MS = 450 + + +def slide_text_out(label, start_y, started): + elapsed = ticks_diff(ticks_ms(), started) + if elapsed >= HUD_EXIT_MS: + label.hide() + return None + label.y = start_y - (start_y + label.tile_height) * elapsed / HUD_EXIT_MS + return started def garden_tiles(world): @@ -63,9 +73,11 @@ def build(self): frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, flip_x=True, flip_y=True, text="RELAX", visible=False) - self.sound_notice = self.hud.label("steel8x8.png", columns=9, x=92, y=11, + self.mood_exit = None + self.sound_notice = self.hud.label("steel8x8.png", columns=5, x=108, y=11, flip_x=True, flip_y=True, visible=False) self.sound_notice_until = None + self.sound_notice_exit = None self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 @@ -111,7 +123,14 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.visible = slow + if slow: + self.mood.y = 1 + self.mood.show() + self.mood_exit = None + else: + self.mood_exit = ticks_ms() + if self.mood_exit is not None: + self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 @@ -131,16 +150,21 @@ def update(self): if (self.sound_notice_until is not None and ticks_diff(ticks_ms(), self.sound_notice_until) >= 0): - self.sound_notice.hide() + self.sound_notice_exit = self.sound_notice_until self.sound_notice_until = None + if self.sound_notice_exit is not None: + self.sound_notice_exit = slide_text_out(self.sound_notice, 11, + self.sound_notice_exit) if joy1.just_pressed(X) or joy2.just_pressed(X): self.sound_on = not self.sound_on if self.sound_on: vs2.audio.music("lullaby", loop=True) else: vs2.audio.stop_music() - self.sound_notice.text = "SOUND ON" if self.sound_on else "SOUND OFF" + self.sound_notice.text = "NOISY" if self.sound_on else "QUIET" + self.sound_notice.y = 11 self.sound_notice.show() + self.sound_notice_exit = None self.sound_notice_until = ticks_add(ticks_ms(), 2000) def bloom(self, seed, player): diff --git a/games/demos/dream_garden/code/dream_garden.py b/games/demos/dream_garden/code/dream_garden.py index 52247101..95425bfa 100644 --- a/games/demos/dream_garden/code/dream_garden.py +++ b/games/demos/dream_garden/code/dream_garden.py @@ -22,6 +22,16 @@ FLOWER_DEPTH = 255 GLOW_FRAMES = (1, 2, 3, 2, 1, 0) GLOW_FRAME_TICKS = 6 +HUD_EXIT_MS = 450 + + +def slide_text_out(label, start_y, started): + elapsed = ticks_diff(ticks_ms(), started) + if elapsed >= HUD_EXIT_MS: + label.hide() + return None + label.y = start_y - (start_y + label.tile_height) * elapsed / HUD_EXIT_MS + return started def garden_tiles(world): @@ -63,9 +73,11 @@ def build(self): frame=4, visible=self.joined) self.mood = self.hud.label("steel8x8.png", columns=5, x=108, y=1, flip_x=True, flip_y=True, text="RELAX", visible=False) - self.sound_notice = self.hud.label("steel8x8.png", columns=9, x=92, y=11, + self.mood_exit = None + self.sound_notice = self.hud.label("steel8x8.png", columns=5, x=108, y=11, flip_x=True, flip_y=True, visible=False) self.sound_notice_until = None + self.sound_notice_exit = None self.flower_half = self.image("flowers.png").width // 2 self.flower_phase = 0 self.scroll = 0 @@ -111,7 +123,14 @@ def update(self): slow = slow1 or slow2 if slow != self.slow: self.slow = slow - self.mood.visible = slow + if slow: + self.mood.y = 1 + self.mood.show() + self.mood_exit = None + else: + self.mood_exit = ticks_ms() + if self.mood_exit is not None: + self.mood_exit = slide_text_out(self.mood, 1, self.mood_exit) self.speed = SLOW_SPEED if slow else DRIFT_SPEED self.spawn_ms = SLOW_SPAWN_MS if slow else DRIFT_SPAWN_MS self.dream_time = (self.dream_time + self.speed) % 96 @@ -131,16 +150,21 @@ def update(self): if (self.sound_notice_until is not None and ticks_diff(ticks_ms(), self.sound_notice_until) >= 0): - self.sound_notice.hide() + self.sound_notice_exit = self.sound_notice_until self.sound_notice_until = None + if self.sound_notice_exit is not None: + self.sound_notice_exit = slide_text_out(self.sound_notice, 11, + self.sound_notice_exit) if joy1.just_pressed(X) or joy2.just_pressed(X): self.sound_on = not self.sound_on if self.sound_on: vs2.audio.music("lullaby", loop=True) else: vs2.audio.stop_music() - self.sound_notice.text = "SOUND ON" if self.sound_on else "SOUND OFF" + self.sound_notice.text = "NOISY" if self.sound_on else "QUIET" + self.sound_notice.y = 11 self.sound_notice.show() + self.sound_notice_exit = None self.sound_notice_until = ticks_add(ticks_ms(), 2000) def bloom(self, seed, player): diff --git a/tests/test_dream_garden.py b/tests/test_dream_garden.py index 698191a3..5cb7b3d2 100644 --- a/tests/test_dream_garden.py +++ b/tests/test_dream_garden.py @@ -175,15 +175,53 @@ def test_sound_toggle_notice_lasts_two_seconds_and_restarts_on_retoggling(self): now[0] = 2500 self.step() self.step(0, X) - self.assertEqual(garden.sound_notice.text, "SOUND ON") + self.assertEqual(garden.sound_notice.text, "NOISY") self.assertEqual(garden.sound_notice_until, 4500) now[0] = 4499 self.step() self.assertTrue(garden.sound_notice.visible) now[0] = 4500 self.step() - self.assertFalse(garden.sound_notice.visible) + self.assertTrue(garden.sound_notice.visible) self.assertIsNone(garden.sound_notice_until) + self.assertEqual(garden.sound_notice.y, 11) + now[0] = 4725 + self.step() + self.assertAlmostEqual(garden.sound_notice.y, 1.5) + now[0] = 4800 + self.step(X) + self.assertEqual(garden.sound_notice.text, "QUIET") + self.assertEqual(garden.sound_notice.y, 11) + self.assertIsNone(garden.sound_notice_exit) + now[0] = 7025 + self.step() + self.assertAlmostEqual(garden.sound_notice.y, 1.5) + now[0] = 7250 + self.step() + self.assertFalse(garden.sound_notice.visible) + self.assertIsNone(garden.sound_notice_exit) + + def test_relax_slides_out_and_a_new_request_cancels_the_exit(self): + from unittest.mock import patch + garden = self.start() + now = [1000] + with patch.dict(garden.update.__globals__, {"ticks_ms": lambda: now[0]}): + self.step(A) + self.step() + self.assertTrue(garden.mood.visible) + now[0] = 1225 + self.step() + self.assertAlmostEqual(garden.mood.y, -3.5) + self.assertTrue(garden.mood.visible) + self.step(0, A) + self.assertEqual(garden.mood.y, 1) + self.assertIsNone(garden.mood_exit) + self.step() + now[0] = 1675 + self.step() + self.assertFalse(garden.mood.visible) + self.assertIsNone(garden.mood_exit) + def test_second_player_joins_and_moves_independently(self): garden = self.start() self.assertFalse(garden.friend.visible) @@ -213,7 +251,8 @@ def test_either_player_slows_every_seed_and_the_background(self): self.assertTrue(garden.mood.visible) self.assertEqual(garden.friend.frame, 7) self.step() - self.assertFalse(garden.mood.visible) + self.assertTrue(garden.mood.visible) + self.assertIsNotNone(garden.mood_exit) self.step(A) self.assertTrue(garden.slow) self.assertEqual(garden.dreamer.frame, 3) @@ -376,12 +415,12 @@ def test_audio_starts_on_toggles_and_stops_on_rest(self): self.assertTrue(any(b"bloom" in line for line, _ in director.platform.comms.sent)) self.step(0, X) self.assertFalse(garden.sound_on) - self.assertEqual(garden.sound_notice.text, "SOUND OFF") + self.assertEqual(garden.sound_notice.text, "QUIET") self.assertEqual(director.platform.comms.sent[-1][0], b"music off") self.step() self.step(X) self.assertTrue(garden.sound_on) - self.assertEqual(garden.sound_notice.text, "SOUND ON") + self.assertEqual(garden.sound_notice.text, "NOISY") self.step(B) self.assertEqual(director.platform.comms.sent[-1][0], b"music off") From ed1f0dff650681eefaadfb76700f1044f11c3af3 Mon Sep 17 00:00:00 2001 From: "Alejandro J. Cura" Date: Tue, 13 Oct 2026 10:53:53 -0300 Subject: [PATCH 3/3] Plant flowers in each player's last steering direction --- games/demos/dream_garden/README.md | 5 +- .../code/checkpoints/05-flowers.py | 7 ++- .../dream_garden/code/checkpoints/06-tiles.py | 7 ++- .../code/checkpoints/07-scroll.py | 7 ++- .../dream_garden/code/checkpoints/08-hud.py | 7 ++- .../code/checkpoints/09-scenes.py | 7 ++- .../code/checkpoints/10-shared.py | 16 ++--- .../dream_garden/code/checkpoints/11-audio.py | 16 ++--- games/demos/dream_garden/code/dream_garden.py | 16 ++--- tests/test_dream_garden.py | 59 +++++++++++++++---- 10 files changed, 99 insertions(+), 48 deletions(-) diff --git a/games/demos/dream_garden/README.md b/games/demos/dream_garden/README.md index d201ce6a..5b84ebf6 100644 --- a/games/demos/dream_garden/README.md +++ b/games/demos/dream_garden/README.md @@ -25,7 +25,10 @@ Windows uses `vs-emu.bat --game demos.dream_garden`. | Return to the launcher | Y / Back / Home | M / 1 / Escape | Numpad 3 / + / Escape | The garden starts empty. Catch a golden seed to plant a flower at the catching -player’s depth, alternating sides at a centre offset of 75% of the player’s width. +player’s depth, on the side they last moved toward, with a centre offset of +75% of the player’s width. Each player remembers their own direction while +stopped; opposite inputs cancel without changing it. Before the first movement, +planting defaults to the left. Flowers drift inward at 8% of seed speed and disappear at the centre. Smaller, subdued golden seeds occasionally pulse with a randomly triggered glow. Missing a seed has no penalty. A second diff --git a/games/demos/dream_garden/code/checkpoints/05-flowers.py b/games/demos/dream_garden/code/checkpoints/05-flowers.py index bc450887..5fc956ab 100644 --- a/games/demos/dream_garden/code/checkpoints/05-flowers.py +++ b/games/demos/dream_garden/code/checkpoints/05-flowers.py @@ -11,7 +11,7 @@ def build(self): self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) - self.plant_side = 1 + self.last_direction = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -20,6 +20,8 @@ def build(self): def update(self): steer = joy1.held(LEFT) - joy1.held(RIGHT) self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width + if steer: + self.last_direction = steer self.dreamer.frame = (3 if joy1.held(A) else 1 if steer > 0 else 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 @@ -42,8 +44,7 @@ def spawn_seed(self): def bloom(self, seed): centre = self.dreamer.x + self.dreamer.width // 2 - centre += self.plant_side * self.dreamer.width * 0.75 - self.plant_side = -self.plant_side + centre += self.last_direction * self.dreamer.width * 0.75 self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) diff --git a/games/demos/dream_garden/code/checkpoints/06-tiles.py b/games/demos/dream_garden/code/checkpoints/06-tiles.py index 522e6939..d1ecd74b 100644 --- a/games/demos/dream_garden/code/checkpoints/06-tiles.py +++ b/games/demos/dream_garden/code/checkpoints/06-tiles.py @@ -18,7 +18,7 @@ def build(self): self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) - self.plant_side = 1 + self.last_direction = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -27,6 +27,8 @@ def build(self): def update(self): steer = joy1.held(LEFT) - joy1.held(RIGHT) self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width + if steer: + self.last_direction = steer self.dreamer.frame = (3 if joy1.held(A) else 1 if steer > 0 else 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 @@ -49,8 +51,7 @@ def spawn_seed(self): def bloom(self, seed): centre = self.dreamer.x + self.dreamer.width // 2 - centre += self.plant_side * self.dreamer.width * 0.75 - self.plant_side = -self.plant_side + centre += self.last_direction * self.dreamer.width * 0.75 self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) diff --git a/games/demos/dream_garden/code/checkpoints/07-scroll.py b/games/demos/dream_garden/code/checkpoints/07-scroll.py index d67a27fa..9646e1b5 100644 --- a/games/demos/dream_garden/code/checkpoints/07-scroll.py +++ b/games/demos/dream_garden/code/checkpoints/07-scroll.py @@ -18,7 +18,7 @@ def build(self): self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) - self.plant_side = 1 + self.last_direction = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -28,6 +28,8 @@ def build(self): def update(self): steer = joy1.held(LEFT) - joy1.held(RIGHT) self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width + if steer: + self.last_direction = steer self.dreamer.frame = (3 if joy1.held(A) else 1 if steer > 0 else 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 @@ -52,8 +54,7 @@ def spawn_seed(self): def bloom(self, seed): centre = self.dreamer.x + self.dreamer.width // 2 - centre += self.plant_side * self.dreamer.width * 0.75 - self.plant_side = -self.plant_side + centre += self.last_direction * self.dreamer.width * 0.75 self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) diff --git a/games/demos/dream_garden/code/checkpoints/08-hud.py b/games/demos/dream_garden/code/checkpoints/08-hud.py index 092cb72f..b52cdbbd 100644 --- a/games/demos/dream_garden/code/checkpoints/08-hud.py +++ b/games/demos/dream_garden/code/checkpoints/08-hud.py @@ -31,7 +31,7 @@ def build(self): self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) - self.plant_side = 1 + self.last_direction = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -62,6 +62,8 @@ def update(self): self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width + if steer: + self.last_direction = steer self.dreamer.frame = (3 if slow else 1 if steer > 0 else 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 @@ -86,8 +88,7 @@ def spawn_seed(self): def bloom(self, seed): centre = self.dreamer.x + self.dreamer.width // 2 - centre += self.plant_side * self.dreamer.width * 0.75 - self.plant_side = -self.plant_side + centre += self.last_direction * self.dreamer.width * 0.75 self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) diff --git a/games/demos/dream_garden/code/checkpoints/09-scenes.py b/games/demos/dream_garden/code/checkpoints/09-scenes.py index ecc072f5..099a898a 100644 --- a/games/demos/dream_garden/code/checkpoints/09-scenes.py +++ b/games/demos/dream_garden/code/checkpoints/09-scenes.py @@ -31,7 +31,7 @@ def build(self): self.flowers = self.world.sprite_pool("flowers.png", count=24, on_empty=vs2.RECYCLE) self.dreamer = self.world.sprite("dreamer.png", y=0) self.dreamer.x = -(self.dreamer.width // 2) - self.plant_side = 1 + self.last_direction = 1 self.speed = 0.45 self.spawn_ms = 1800 self.dream_time = 0 @@ -64,6 +64,8 @@ def update(self): self.spawn_ms = 4000 if slow else 1800 steer = joy1.held(LEFT) - joy1.held(RIGHT) self.dreamer.x = (self.dreamer.x + steer) % vs2.display.width + if steer: + self.last_direction = steer self.dreamer.frame = (3 if slow else 1 if steer > 0 else 2 if steer < 0 else 0) self.dream_time = (self.dream_time + self.speed) % 96 @@ -88,8 +90,7 @@ def spawn_seed(self): def bloom(self, seed): centre = self.dreamer.x + self.dreamer.width // 2 - centre += self.plant_side * self.dreamer.width * 0.75 - self.plant_side = -self.plant_side + centre += self.last_direction * self.dreamer.width * 0.75 self.flowers.spawn(x=(centre - self.image("flowers.png").width // 2) % vs2.display.width, y=self.dreamer.y, frame=randrange(4) * 2) self.seeds.despawn(seed) diff --git a/games/demos/dream_garden/code/checkpoints/10-shared.py b/games/demos/dream_garden/code/checkpoints/10-shared.py index 1fb21e34..5eb46326 100644 --- a/games/demos/dream_garden/code/checkpoints/10-shared.py +++ b/games/demos/dream_garden/code/checkpoints/10-shared.py @@ -80,8 +80,9 @@ def build(self): self.scroll = 0 self.dream_time = 0 self.slow = False - self.plant_side1 = 1 - self.plant_side2 = 1 + # Start toward LEFT; stopping keeps each player’s last steering direction. + self.last_direction1 = 1 + self.last_direction2 = 1 self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS @@ -90,6 +91,11 @@ def build(self): def steer(self, dreamer, controller, base_frame, sleeping): direction = controller.held(LEFT) - controller.held(RIGHT) dreamer.x = (dreamer.x + direction) % vs2.display.width + if direction: + if dreamer is self.friend: + self.last_direction2 = direction + else: + self.last_direction1 = direction pose = (ASLEEP if sleeping else LEFT_POSE if direction > 0 else RIGHT_POSE if direction < 0 else AWAKE) @@ -143,15 +149,11 @@ def update(self): self.move_seeds() def bloom(self, seed, player): - side = self.plant_side2 if player is self.friend else self.plant_side1 + side = self.last_direction2 if player is self.friend else self.last_direction1 centre = player.x + player.width // 2 self.flowers.spawn(x=(centre + side * player.width * 0.75 - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - if player is self.friend: - self.plant_side2 = -side - else: - self.plant_side1 = -side self.retire_seed(seed) def move_seeds(self): diff --git a/games/demos/dream_garden/code/checkpoints/11-audio.py b/games/demos/dream_garden/code/checkpoints/11-audio.py index 95425bfa..94ce62cf 100644 --- a/games/demos/dream_garden/code/checkpoints/11-audio.py +++ b/games/demos/dream_garden/code/checkpoints/11-audio.py @@ -85,8 +85,9 @@ def build(self): self.slow = False self.sound_on = True vs2.audio.music("lullaby", loop=True) - self.plant_side1 = 1 - self.plant_side2 = 1 + # Start toward LEFT; stopping keeps each player’s last steering direction. + self.last_direction1 = 1 + self.last_direction2 = 1 self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS @@ -95,6 +96,11 @@ def build(self): def steer(self, dreamer, controller, base_frame, sleeping): direction = controller.held(LEFT) - controller.held(RIGHT) dreamer.x = (dreamer.x + direction) % vs2.display.width + if direction: + if dreamer is self.friend: + self.last_direction2 = direction + else: + self.last_direction1 = direction pose = (ASLEEP if sleeping else LEFT_POSE if direction > 0 else RIGHT_POSE if direction < 0 else AWAKE) @@ -168,15 +174,11 @@ def update(self): self.sound_notice_until = ticks_add(ticks_ms(), 2000) def bloom(self, seed, player): - side = self.plant_side2 if player is self.friend else self.plant_side1 + side = self.last_direction2 if player is self.friend else self.last_direction1 centre = player.x + player.width // 2 self.flowers.spawn(x=(centre + side * player.width * 0.75 - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - if player is self.friend: - self.plant_side2 = -side - else: - self.plant_side1 = -side self.retire_seed(seed) if self.sound_on: vs2.audio.sound("bloom") diff --git a/games/demos/dream_garden/code/dream_garden.py b/games/demos/dream_garden/code/dream_garden.py index 95425bfa..94ce62cf 100644 --- a/games/demos/dream_garden/code/dream_garden.py +++ b/games/demos/dream_garden/code/dream_garden.py @@ -85,8 +85,9 @@ def build(self): self.slow = False self.sound_on = True vs2.audio.music("lullaby", loop=True) - self.plant_side1 = 1 - self.plant_side2 = 1 + # Start toward LEFT; stopping keeps each player’s last steering direction. + self.last_direction1 = 1 + self.last_direction2 = 1 self.seed_glows = {} self.speed = DRIFT_SPEED self.spawn_ms = DRIFT_SPAWN_MS @@ -95,6 +96,11 @@ def build(self): def steer(self, dreamer, controller, base_frame, sleeping): direction = controller.held(LEFT) - controller.held(RIGHT) dreamer.x = (dreamer.x + direction) % vs2.display.width + if direction: + if dreamer is self.friend: + self.last_direction2 = direction + else: + self.last_direction1 = direction pose = (ASLEEP if sleeping else LEFT_POSE if direction > 0 else RIGHT_POSE if direction < 0 else AWAKE) @@ -168,15 +174,11 @@ def update(self): self.sound_notice_until = ticks_add(ticks_ms(), 2000) def bloom(self, seed, player): - side = self.plant_side2 if player is self.friend else self.plant_side1 + side = self.last_direction2 if player is self.friend else self.last_direction1 centre = player.x + player.width // 2 self.flowers.spawn(x=(centre + side * player.width * 0.75 - self.flower_half) % vs2.display.width, y=player.y, frame=randrange(4) * 2 + self.flower_phase) - if player is self.friend: - self.plant_side2 = -side - else: - self.plant_side1 = -side self.retire_seed(seed) if self.sound_on: vs2.audio.sound("bloom") diff --git a/tests/test_dream_garden.py b/tests/test_dream_garden.py index 5cb7b3d2..9a6d378c 100644 --- a/tests/test_dream_garden.py +++ b/tests/test_dream_garden.py @@ -112,20 +112,52 @@ def test_empty_garden_and_three_ordered_layers(self): self.assertFalse(garden.mood.visible) self.assertFalse(garden.sound_notice.visible) - def test_planting_alternates_per_player_at_their_depth_and_wraps(self): + def test_planting_follows_last_motion_per_player_at_their_depth_and_wraps(self): garden = self.start(player2=A) garden.dreamer.y = 12 garden.friend.y = 20 - for player in (garden.dreamer, garden.friend, garden.dreamer, garden.friend): - side = garden.plant_side2 if player is garden.friend else garden.plant_side1 - seed = garden.seeds.spawn(x=100, y=100) - garden.bloom(seed, player) - flower = garden.flowers._newest - self.assertEqual(flower.y, player.y) - expected = (player.x + player.width // 2 + side * player.width * 0.75 - - garden.flower_half) % vs2.display.width - self.assertAlmostEqual(flower.x, expected) - self.assertEqual((garden.plant_side1, garden.plant_side2), (1, 1)) + for first, second, directions in ( + (0, 0, (1, 1)), + (LEFT, RIGHT, (1, -1)), + (0, 0, (1, -1)), + (LEFT | RIGHT, LEFT | RIGHT, (1, -1)), + (RIGHT | A, LEFT | A, (-1, 1)), + (0, 0, (-1, 1))): + self.step(first, second) + self.assertEqual((garden.last_direction1, garden.last_direction2), directions) + for player, side in zip((garden.dreamer, garden.friend), directions): + for _ in range(2): + seed = garden.seeds.spawn(x=100, y=100) + garden.bloom(seed, player) + flower = garden.flowers._newest + self.assertEqual(flower.y, player.y) + expected = (player.x + player.width // 2 + side * player.width * 0.75 + - garden.flower_half) % vs2.display.width + self.assertAlmostEqual(flower.x, expected) + self.assertEqual((garden.last_direction1, garden.last_direction2), directions) + + def test_catch_after_stopping_uses_heading_across_the_angular_seam(self): + garden = self.start() + self.assertEqual(garden.last_direction1, 1) + self.step(LEFT, count=10) + self.assertEqual(garden.dreamer.x, 1) + self.step() + garden.seeds.spawn(x=254, y=10) + self.step() + flower = garden.flowers._newest + expected = (garden.dreamer.x + garden.dreamer.width // 2 + + garden.dreamer.width * 0.75 - garden.flower_half) % 256 + self.assertAlmostEqual(flower.x, expected) + self.assertEqual(flower.y, garden.dreamer.y) + self.step(RIGHT, count=2) + self.assertEqual(garden.dreamer.x, 255) + self.step() + garden.seeds.spawn(x=254, y=10) + self.step() + flower = garden.flowers._newest + expected = (garden.dreamer.x + garden.dreamer.width // 2 + - garden.dreamer.width * 0.75 - garden.flower_half) % 256 + self.assertAlmostEqual(flower.x, expected) def test_flowers_drift_inward_much_slower_than_seeds_and_retire(self): from games.demos.dream_garden.code.dream_garden import FLOWER_DRIFT_RATIO @@ -388,6 +420,11 @@ def test_every_presentation_checkpoint_runs_and_has_only_its_stage_features(self scene.seeds.spawn(x=scene.dreamer.x, y=10) self.step() self.assertGreater(len(scene.flowers), 0) + flower = scene.flowers._newest + expected = (scene.dreamer.x + scene.dreamer.width // 2 + + scene.dreamer.width * 0.75 + - scene.image("flowers.png").width // 2) % 256 + self.assertAlmostEqual(flower.x, expected) vs2.export_scene_payload(scene) self.step(extra=8) self.step()