fuzzyworld

herd / files / plan.md

plan.md

Blame: the commit that last changed each line · file · history

1a83062c 2026-05-17 Pavel Safronov1# Herd — Implementation Plan
2
3## Overview
4
5A 2D top-down browser game. The player taps/clicks on the canvas to direct a border collie, which exerts pressure on a flock of sheep. The objective is to guide the entire flock into a marked pen area before time runs out.
6
7Rendering: HTML5 Canvas (no heavy game engine).
8Physics/AI: hand-rolled boids flocking + dog steering.
9Stack: TypeScript · Vite · npm · GitLab Pages.
10
11---
12
13## Phase 1 — Project Scaffold
14
15- `npm create vite@latest herd -- --template vanilla-ts`
16- Add `.gitlab-ci.yml` for GitLab Pages deployment (build → `dist/`, artifact on `public/` path).
17- Configure `vite.config.ts` with `base: '/herd/'` (adjust to match the repo name / Pages path).
18- Set up `tsconfig.json` with `strict: true`.
19- Add `eslint` + `prettier` configs (optional but recommended for the long run).
20- Copy `plant.png` into the project root and reference it as the favicon in `index.html`:
21 `<link rel="icon" type="image/png" href="/plant.png" />`
22- Verify `npm run dev` and `npm run build` both work before writing any game code.
23
24Deliverable: blank Vite page with favicon, deployed to GitLab Pages.
25
26---
27
28## Phase 2 — Game Loop & Canvas
29
30Files: `src/main.ts`, `src/game/GameLoop.ts`, `src/game/Canvas.ts`
31
32- Create a `<canvas>` element that fills the viewport, scaling with `devicePixelRatio` for crisp rendering on retina/HiDPI screens.
33- Handle `resize` events — recalculate canvas size and reposition entities proportionally.
34- Implement a fixed-timestep game loop using `requestAnimationFrame`:
35 - Accumulate `dt` (delta time in ms).
36 - Run `update(dt)` in fixed 16 ms steps (≈60 Hz) to keep physics deterministic.
37 - Run `render(interpolationAlpha)` every frame.
38- Expose `GameLoop.start()` / `GameLoop.stop()`.
39
40Deliverable: blank canvas with a running loop that logs FPS to console.
41
42---
43
44## Phase 3 — Entity System
45
46Files: `src/entities/Entity.ts`, `src/entities/Sheep.ts`, `src/entities/Dog.ts`, `src/entities/Pen.ts`
47
48### Entity base
49Each entity has:
50- `position: Vec2`
51- `velocity: Vec2`
52- `radius: number` (for collision/influence zones)
53- `update(dt: number): void`
54- `draw(ctx: CanvasRenderingContext2D): void`
55
56### Vec2 math helper
57File: `src/math/Vec2.ts`
58Implement: `add`, `sub`, `scale`, `normalize`, `length`, `distance`, `limit`, `lerp`.
59Keep it immutable — return new `Vec2` instances.
60
61### Sheep
62- Circular sprite (or a simple polygon) with a facing direction.
63- Carries boids state: a list of currently visible neighbors (updated each tick).
64- Has a `fearLevel` scalar that rises when the dog is close, decays over time.
65
66### Dog
67- Distinct visual so it's easy to tell apart from sheep.
68- Has a `target: Vec2 | null` — the position the player last tapped.
69- Moves toward target at a fixed speed; stops when within ~8 px.
70
71### Pen
72- An axis-aligned rectangle defined by `x, y, width, height`.
73- `contains(pos: Vec2): boolean` — used for win-condition checking.
74- Drawn as a fenced rectangle with an open side (the "gate").
75
76Deliverable: entities visible on canvas, nothing moving yet.
77
78---
79
80## Phase 4 — Sheep Flocking (Boids)
81
82File: `src/ai/Boids.ts`
83
84Classic boids with sheep-specific extensions. Each frame, for each sheep, compute a weighted sum of steering forces:
85
86| Force | Weight | Description |
87| --- | --- | --- |
88| Separation | 1.8 | Steer away from neighbors within `separationRadius` (~30 px) |
89| Alignment | 1.0 | Match average velocity of neighbors within `alignRadius` (~80 px) |
90| Cohesion | 1.0 | Steer toward centroid of neighbors within `cohesionRadius` (~100 px) |
91| Dog flee | 3.5 | Strong repulsion when dog is within `fleeRadius` (~150 px); magnitude scales with `1/distance²` |
92| Boundary | 2.0 | Smooth repulsion from canvas edges (starts ~60 px from edge) |
93| Pen attraction | 0.3 | Weak pull toward pen center (simulates sheep "settling" once near the pen) |
94
95Implementation notes:
96
97- Neighbor lookup is O(n²) — fine for ≤40 sheep.
98- Cap final velocity with `Vec2.limit(maxSpeed)`.
99- `maxSpeed` for sheep: ~90 px/s. `maxForce` (acceleration cap per frame): ~6 px/s².
100- When `fearLevel` is high, multiply dog-flee weight and max speed by a multiplier (panicked sheep move faster).
101
102Deliverable: sheep flock together and scatter when the dog approaches.
103
104---
105
106## Phase 5 — Dog Behaviour & Player Input
107
108File: `src/input/InputHandler.ts`, update `Dog`
109
110### Input
111- Listen for `pointerdown` on the canvas (covers mouse, touch, stylus).
112- Convert from screen coordinates to canvas/world coordinates (account for `devicePixelRatio` and canvas CSS size).
113- Set `dog.target` to the tapped world position.
114
115### Dog movement
116- Each tick: compute vector from `dog.position` to `dog.target`.
117- Steer toward target with a simple seek behaviour (`Vec2.normalize(diff).scale(speed)`).
118- Dog speed: ~160 px/s — faster than sheep so it can actually herd.
119- Stop when distance to target < arrival threshold (~10 px); set `target = null`.
120
121### Dog influence on sheep
122- Each sheep tick, compute distance to dog.
123- If within `fleeRadius`: increase sheep's `fearLevel`; apply flee force.
124- `fearLevel` decays exponentially when dog is far: `fearLevel *= 0.95` per tick.
125
126Deliverable: tapping moves the dog; sheep scatter away from it.
127
128---
129
130## Phase 6 — Game State & Win Condition
131
132File: `src/game/GameState.ts`
133
134States: `IDLE` → `PLAYING` → `WIN` | `FAIL`
135
136### Win condition
137- Check every N ticks (e.g., every 30 frames).
138- Count sheep whose `position` is `pen.contains(sheep.position)`.
139- Win when ≥ threshold (e.g., all sheep, or configurable percentage like 80%).
140- Brief celebration (all sheep in pen, show "You Win!" overlay).
141
142### Fail condition (optional, adds pressure)
143- A countdown timer. If it hits zero before the win condition, show "Time's Up!" overlay.
144
145### UI overlays
146- Render directly on canvas (no DOM elements during gameplay for performance).
147- `IDLE`: "Tap to start" message.
148- `PLAYING`: sheep-in-pen counter (e.g., "3 / 8 🐑") + timer bar.
149- `WIN` / `FAIL`: message + "Play Again" prompt.
150
151Deliverable: complete game loop from start to end state, restartable.
152
153---
154
155## Phase 7 — Visual Polish
156
157Keep it readable and charming without requiring art assets (use canvas primitives).
158
159### Sheep
160- White filled circle, darker ellipse for the "wool" texture, two small dots for eyes, tiny legs.
161- Or: import simple SVG sprite at a later pass.
162
163### Dog
164
165- Brown/black filled circle with pointed ears, different color from sheep.
166
167### Pen
168- Dashed or solid rectangle, green fill at low opacity, fence posts along three sides, open gate on one side with a visual indicator.
169
170### Logo / favicon
171
172- `plant.png` (grass icon) is already in the project root and wired up in `index.html`.
173- Render it in the top-left corner of the canvas as a small watermark during gameplay (e.g. 32×32 px at low opacity).
174- Can also appear on the level-select / idle screens at full size as the game's title graphic.
175
176### Environment
177- Green fill for the field.
178- Optional: a few static obstacle circles (rocks / trees) that sheep path around — add boundary repulsion force for each.
179
180### Feedback
181- Footprints that fade out behind the dog.
182- Sheep briefly turn red/orange when at high `fearLevel`.
183- Pen fill color shifts from transparent → light green as more sheep enter.
184
185---
186
187## Phase 8 — Levels & Configuration
188
189File: `src/levels/LevelConfig.ts`
190
191All coordinates are fractional (0.0–1.0) relative to canvas size so levels are resolution-independent.
192
193### LevelConfig type
194
195```ts
196interface SheepOverride {
197 index: number;
198 stubborn?: boolean; // higher flee radius, lower cohesion pull
199 leader?: boolean; // larger cohesion broadcast radius — others weight this sheep more
200 subFlock?: number; // group ID; sheep strongly prefer neighbors in the same group
201 startFear?: number; // 0–1 initial fearLevel (default 0)
202}
203
204interface DynamicObstacle {
205 x: number; y: number; r: number;
206 appearsAtSeconds: number; // obstacle materializes partway through the level
207}
208
209interface LevelConfig {
210 id: number;
211 title: string;
212 subtitle: string; // one-line real-world resonance note shown on level card
213
214 sheepCount: number;
215 penRect: { x: number; y: number; w: number; h: number };
216 penGateSide: 'top' | 'right' | 'bottom' | 'left';
217 timeLimitSeconds: number | null; // null = no timer
218 winThreshold: number; // fraction of flock that must be penned, e.g. 1.0 = all
219
220 spawnClusters: Array<{ cx: number; cy: number; r: number; count: number }>;
221 // where sheep start — allows scattered vs. grouped starts
222
223 obstacles: Array<{ x: number; y: number; r: number }>;
224 dynamicObstacles: DynamicObstacle[];
225
226 sheepOverrides: SheepOverride[];
227
228 darkMode: boolean; // canvas is dark; only the dog's vicinity is lit
229 penShrinksEverySeconds: number | null; // pen width/height reduce by 10% on each interval
230}
231```
232
233---
234
235### The 10 Levels
236
237---
238
239#### Level 1 — "First Lesson"
240
241*No real-world parallel — pure tutorial.*
242
243| Property | Value |
244|---|---|
245| Sheep | 6 |
246| Win threshold | 80% (5 of 6) |
247| Timer | 120 s |
248| Pen | Large (0.25 × 0.25), top-right corner |
249| Spawn | One cluster, center of field |
250| Obstacles | None |
251| Special | None |
252
253The player learns that tapping moves the dog, the dog scares sheep, and sheep flock together. The generous threshold and timer mean almost any approach works. A tooltip appears on first tap.
254
255---
256
257#### Level 2 — "The Scatter"
258
259*Real-world parallel: rural depopulation. Communities spread too thin across a landscape to self-organize. Someone has to physically go out and bring them back together before cohesion can happen at all.*
260
261| Property | Value |
262|---|---|
263| Sheep | 12 |
264| Win threshold | 100% |
265| Timer | 150 s |
266| Pen | Medium (0.20 × 0.20), bottom-center |
267| Spawn | 4 clusters of 3, one in each corner of the field |
268| Obstacles | None |
269| Special | Clusters start too far apart to naturally flock — boids cohesion radius won't reach across the gap. Player must push sub-groups together before the flock coheres. |
270
271The mechanical lesson: cohesion only kicks in at range. A scattered flock can't self-organize; it needs active bridging.
272
273---
274
275#### Level 3 — "The Holdouts"
276
277*Real-world parallel: herd immunity. You need a population-wide threshold, but a small minority requires dramatically more effort than the rest. The burden of 100% is not evenly distributed.*
278
279| Property | Value |
280|---|---|
281| Sheep | 15 |
282| Win threshold | 100% |
283| Timer | 180 s |
284| Pen | Medium, left side |
285| Spawn | One main cluster (12) + 3 holdouts spread across far corners |
286| Obstacles | None |
287| Special | 3 `stubborn` sheep: flee radius 2× normal, cohesion pull 0.3× normal. Marked with a small visual indicator. |
288
289The main flock pens easily. Then the player discovers that the three holdouts need individual, sustained pressure — they can't be herded efficiently as a group and will break away from the main flock if spooked.
290
291---
292
293#### Level 4 — "Two Tribes"
294
295*Real-world parallel: political polarization. Two groups have strong internal cohesion but actively repel each other. Getting both into the same space requires patient, impartial pressure — heavy-handed intervention on one tribe panics the other.*
296
297| Property | Value |
298|---|---|
299| Sheep | 20 (10 per tribe, visually distinct — e.g. white vs. grey) |
300| Win threshold | 100% |
301| Timer | 180 s |
302| Pen | Large, center of field |
303| Spawn | Two clusters on opposite sides of the field |
304| Obstacles | None |
305| Special | `subFlock` IDs 0 and 1. Within a flock: normal flocking. Between flocks: separation force is 3× higher than within-flock separation. They will actively avoid mixing. |
306
307The player quickly learns that chasing one group into the pen scatters the other. Gentle, alternating pressure from behind both groups is the only solution. The pen is large enough to hold both — once they're in, proximity to the pen edges overrides the inter-flock repulsion (sheep stop caring about tribe when they're already safe).
308
309---
310
311#### Level 5 — "Shrinking Pasture"
312
313*Real-world parallel: habitat loss / urban sprawl. The environment erodes in real time. What was open ground becomes obstacles. The sheep adapt by clustering, which paradoxically makes them easier to herd — but the route to the pen gets harder.*
314
315| Property | Value |
316|---|---|
317| Sheep | 15 |
318| Win threshold | 85% |
319| Timer | 210 s |
320| Pen | Medium, top-left corner |
321| Spawn | Spread across the center |
322| Obstacles | 3 static (rocks along the right edge) |
323| Special | 6 `dynamicObstacles` appear at t = 30 s, 60 s, 90 s, 120 s, 150 s, 180 s — each one materializes with a brief visual pulse, starting from the right and bottom edges and progressing inward. Total effect: the usable field shrinks to about 55% of its original area by the end. |
324
325The player doesn't need to hurry at first — but procrastination means the path to the pen gets blocked. The level is completable early with a moderate strategy or late with a very tight squeeze.
326
327---
328
329#### Level 6 — "Already Spooked"
330
331*Real-world parallel: displaced or traumatized populations. Every intervention triggers a panic response. Heavy-handed herding makes things worse. The player must slow down, reduce pressure, and use patience rather than force.*
332
333| Property | Value |
334|---|---|
335| Sheep | 18 |
336| Win threshold | 80% |
337| Timer | 240 s |
338| Pen | Large, bottom-right |
339| Spawn | Scattered (6 small clusters of 3) |
340| Obstacles | None |
341| Special | All sheep start with `startFear = 1.0`. Fear decay rate is 0.5× normal (they stay spooked longer). Dog flee radius is 1.5× normal. The dog's speed is reduced to 110 px/s for this level only. |
342
343The player who taps aggressively will send sheep careening into walls and each other. The level is easier with a slow dog, moved in short steps, looping wide around the flock. The generous timer and threshold explicitly reward restraint over speed.
344
345---
346
347#### Level 7 — "The Influencers"
348
349*Real-world parallel: social contagion and opinion dynamics. In any group, a small number of nodes determine collective movement. Identify and redirect the key nodes and the crowd follows. Ignore them and you're pushing against invisible resistance.*
350
351| Property | Value |
352|---|---|
353| Sheep | 22 |
354| Win threshold | 90% |
355| Timer | 180 s |
356| Pen | Medium, right side |
357| Spawn | One large cluster in the left half |
358| Obstacles | 2 rocks in the mid-field |
359| Special | 4 `leader` sheep (marked visually). Other sheep weight leader neighbors 3× more heavily in alignment and cohesion calculations. If all 4 leaders are penned, the remaining sheep's pen-attraction force doubles — they drift in. If no leaders are penned, non-leaders resist the pen even under direct pressure. |
360
361The player who tries to herd the whole flock at once will be confused why it doesn't work. The player who figures out to target the leaders first will watch the rest follow almost by themselves.
362
363---
364
365#### Level 8 — "Race to Market"
366
367*Real-world parallel: industrial livestock production. Volume, speed, and efficiency above all else. The level is deliberately exhausting — a comment on what it feels like to operate at industrial scale.*
368
369| Property | Value |
370|---|---|
371| Sheep | 30 |
372| Win threshold | 80% (24 of 30) |
373| Timer | 75 s |
374| Pen | Medium (fits ~20 comfortably, so sheep jostle) |
375| Spawn | Two large clusters |
376| Obstacles | None (clear field — no excuses) |
377| Special | Dog speed increased to 200 px/s for this level. Sheep max speed also increased by 20% — everything moves faster. Timer is severe. |
378
379The player has to be decisive and aggressive. The 80% threshold gives a little slack but not much. It's the one level where patience is actively punished. After the calm required by Level 6, the gear shift is jarring — intentionally.
380
381---
382
383#### Level 9 — "Lights Out"
384
385*Real-world parallel: information asymmetry — making decisions when you can only see a fraction of the picture. The flock is out there. You just can't see where. You respond to what's in front of you and hope.*
386
387| Property | Value |
388|---|---|
389| Sheep | 16 |
390| Win threshold | 100% |
391| Timer | 270 s |
392| Pen | Medium, glows faintly (always visible) |
393| Spawn | Scattered evenly across the whole field |
394| Obstacles | 2 rocks |
395| Special | `darkMode: true`. Canvas is rendered dark with a radial gradient "torch" around the dog (radius ~120 px). Sheep outside the lit area are invisible. Sound cue (soft rustling) plays when unlit sheep are within ~200 px of the dog. |
396
397To win you must sweep the entire field systematically. The 100% threshold means you can't leave anyone behind, and you can't see who's left. After clearing most sheep, the final one or two are found by listening.
398
399> ⚠️ The rustling audio cue is gated on Phase 9 being implemented. Without audio, the level is still playable but loses its most important feedback signal — implement Phase 9 before shipping Level 9.
400
401---
402
403#### Level 10 — "The Last Pasture"
404
405*Real-world parallel: climate crisis. The deadline isn't a countdown — it's a slow erosion. You have time until suddenly you don't. The goal seems achievable right up until the moment it isn't.*
406
407| Property | Value |
408|---|---|
409| Sheep | 25 |
410| Win threshold | 100% |
411| Timer | None (no timer displayed — this is important) |
412| Pen | Starts large; `penShrinksEverySeconds: 25`. Every 25 s the pen shrinks by 10% in both dimensions. After ~5 minutes it becomes too small to physically contain 25 sheep and the level becomes impossible. |
413| Spawn | Two clusters |
414| Obstacles | One large central obstacle (a dried-out lake / flood zone) bisecting the direct route to the pen |
415| Special | 3 `stubborn` sheep (from Level 3 mechanic), 2 `leader` sheep (from Level 7 mechanic). Combines the hard-to-herd minority with the influence dynamic. |
416
417The player is told only that all sheep must reach the pen. There is no visible clock. The pen visually shows its current size, and a subtle ambient sound grows more urgent as the pen contracts. Players who dawdle discover the deadline too late. The level is designed to be replayed — on a second run, knowing the pen is shrinking changes everything about how you approach it.
418
419---
420
421### Difficulty Curve Summary
422
423| # | Title | Sheep | Threshold | Special mechanic |
424| --- | --- | --- | --- | --- |
425| 1 | First Lesson | 6 | 80% | None |
426| 2 | The Scatter | 12 | 100% | Dispersed spawn, no natural cohesion |
427| 3 | The Holdouts | 15 | 100% | Stubborn minority |
428| 4 | Two Tribes | 20 | 100% | Inter-flock repulsion |
429| 5 | Shrinking Pasture | 15 | 85% | Dynamic obstacles |
430| 6 | Already Spooked | 18 | 80% | Max start fear, slower dog |
431| 7 | The Influencers | 22 | 90% | Leader sheep |
432| 8 | Race to Market | 30 | 80% | Fast everything, brutal timer |
433| 9 | Lights Out | 16 | 100% | Dark canvas, audio cues |
434| 10 | The Last Pasture | 25 | 100% | Shrinking pen, combined mechanics, no timer |
435
436### Extended LevelConfig notes
437
438- All coordinates are 0.0–1.0 fractions of canvas dimensions.
439- `spawnClusters` defines where sheep appear; sheep within a cluster are placed with small random offsets from the cluster center.
440- `sheepOverrides` are applied by index after spawn assignment.
441- `darkMode` activates a canvas compositing layer; the "torch" radius and falloff are tunable constants.
442- `penShrinksEverySeconds` contracts both `w` and `h` by a fixed fraction each interval; minimum pen size is capped to prevent a zero-area pen (which would make 100% threshold impossible and trigger a specific "too late" lose state).
443- Level metadata (`title`, `subtitle`) is shown on a pre-level card screen before the game starts.
444
445---
446
447## Phase 9 — Audio (Optional / Later)
448
449- Use the Web Audio API directly (no library).
450- Sounds: soft baa on scare, dog bark on tap, success chime.
451- Keep audio files small (< 50 KB total).
452- Add a mute toggle button in the corner.
453
454---
455
456## Phase 10 — GitLab CI/CD
457
458File: `.gitlab-ci.yml`
459
460```yaml
461pages:
462 stage: deploy
463 image: node:20
464 script:
465 - npm ci
466 - npm run build
467 - mv dist public
468 artifacts:
469 paths:
470 - public
471 only:
472 - main
473```
474
475- Set `base` in `vite.config.ts` to match the GitLab Pages URL path.
476- Confirm the deployed URL loads and runs the game.
477
478---
479
480## Assets & Attribution
481
482| Asset | File | Source |
483| --- | --- | --- |
484| Grass / field icon (favicon + logo) | `plant.png` | [Icongeek26 — Outline collection](https://www.flaticon.com/authors/icongeek26/outline?author_id=296&type=standard) via [Flaticon](https://www.flaticon.com/authors/icongeek26) |
485
486The Flaticon license requires attribution when the free tier is used. Attribution is included in the README and (where space permits) in an in-game "Credits" screen or overlay.
487
488---
489
490## File Structure (target)
491
492```text
493herd/
494├── .gitlab-ci.yml
495├── index.html
496├── package.json
497├── plant.png # favicon + logo (Icongeek26 / Flaticon, outline collection)
498├── tsconfig.json
499├── vite.config.ts
500└── src/
501 ├── main.ts # entry point — mounts canvas, starts GameLoop
502 ├── math/
503 │ └── Vec2.ts
504 ├── game/
505 │ ├── Canvas.ts # canvas sizing, dpr scaling, coord transform
506 │ ├── GameLoop.ts # rAF loop, fixed timestep
507 │ └── GameState.ts # state machine, win/fail checks, UI overlays
508 ├── entities/
509 │ ├── Entity.ts
510 │ ├── Sheep.ts
511 │ ├── Dog.ts
512 │ └── Pen.ts
513 ├── ai/
514 │ └── Boids.ts # force calculation, neighbor queries
515 ├── input/
516 │ └── InputHandler.ts # pointer events → world coords
517 └── levels/
518 └── LevelConfig.ts
519```
520
521---
522
523## Key Constants to Tune (start here, adjust by feel)
524
525| Constant | Starting Value |
526| --- | --- |
527| `SHEEP_MAX_SPEED` | 90 px/s |
528| `SHEEP_MAX_FORCE` | 6 px/s² |
529| `DOG_SPEED` | 160 px/s |
530| `FLEE_RADIUS` | 150 px |
531| `SEPARATION_RADIUS` | 30 px |
532| `ALIGN_RADIUS` | 80 px |
533| `COHESION_RADIUS` | 100 px |
534| `FEAR_DECAY` | 0.95 per tick |
535
536These will need iteration. The game feel lives entirely in these numbers.
537
538---
539
540## Development Order
541
5421. Scaffold (Phase 1) — get CI green with a blank page first.
5432. Canvas + loop (Phase 2).
5443. Vec2 + entities rendering statically (Phase 3).
5454. Boids forces in isolation with no dog — just watch sheep flock (Phase 4).
5465. Add dog input and flee force (Phase 5).
5476. Win/fail states (Phase 6).
5487. Polish passes (Phase 7+) as desired.
549
550Do **not** skip to polish before the core herding feel is fun. The boids tuning in Phase 4–5 is the game.