Imported from Freakling/PKSEI (
AGENTS.md). Install upstream withnpx skills add Freakling/PKSEI. Copyright stays with the author.
PKSEI
Four drunk guys have lashed two aluminium canoes into a catamaran and need to reach an island in the middle of the lake. You control all four at once. The lake throws random hazards at them, and they keep drinking, so the boat gets harder to control the longer the crossing takes. Nobody dies: a bad stretch drifts the boat back into water already crossed. A top-down, bright pixel-art rage game in the spirit of Getting Over It, played for the comedy of watching four idiots fail together.
Instructions for AI assistants working on this game. The workflow is Godot Director: its rules are in .godot-director/rules.md, which names the procedure for each kind of request. Read it before any work, unless your tool has already loaded it (Claude Code imports it: @.godot-director/rules.md).
Project facts
- Godot 4.7 · GDScript · 2D, top-down · Web (HTML5, itch.io); desktop builds for development
- Rendering and window: GL Compatibility · 640×360 viewport,
canvas_itemsstretch, aspectkeep, fractional scaling (integer scaling left phones with a 1× canvas: B6) · nearest texture filter, 2D transforms snapped to pixels
Layout
| Folder | Holds |
|---|---|
scripts/autoload/ |
autoloads: current state, signals |
scripts/systems/ |
game-rule classes (RefCounted) |
scripts/resources/ |
Resource schemas for tunable data |
scripts/ui/ |
screen scripts (display only; checked by tools/check.cfg): hud.gd (names, bars, and a status line saying what a guy is doing whenever he can't paddle: status_text), offscreen_arrows.gd (edge arrows toward swimmers off-screen), touch_pads.gd (hull pads on touchscreens laid out like the keys: outer part of a half is the back seat, inner the front; a press below the line low on each pad is reverse, on or above it forward, no flick; seat_for, is_reverse), speech_bubbles.gd (a Swedish line over a guy during his wind-ups, when a hazard is called out, and over a swimmer every few seconds: fetching, drinking, fallen in, swimming back), reset_button.gd (hold-to-reset corner button on touch; R resets on keys via lake.gd) |
scripts/ (root) |
scene scripts: win_cutscene.gd (the landing, camp and title; emits again), catamaran.gd (input, motion, telegraphs, debug beer keys F5/F6), lake.gd (island arrival, progress, swimmers, pickup, shore reset, hazard spawning from the plan, debug path overlay on F7; lake values in LakeTuning, scripts/resources/lake_tuning.gd, data/lake_tuning.tres), lake_camera.gd (follow with look-ahead by BoatTuning.camera_lead) |
scripts/hazards/ |
hazard scene scripts (one per hazard, most extending hazard_base.gd: tuning/spec guards, _find_boat(), one audio loop per hazard); rules in scripts/systems/; their specs are data/hazards/*.tres (HazardSpec) |
scenes/ |
scenes: main.tscn → lake.tscn (water, shore, island, camera, HUD; hands off to win.tscn on WON and restarts on again) → catamaran.tscn (boat, guys); scenes/hazards/ one scene per hazard |
data/ |
.tres content: the values the human tunes |
tests/ |
rule tests (test_*.gd) |
design/ui/ |
screen specs (one per screen) and captures/ (gitignored); no director drawings yet (no director/ folder) |
tools/ |
the check and its tests base; the generators (gen_sprites.gd, gen_audio.gd, gen_thumbnail.gd → design/itch/thumbnail.png plus @2x and @4x, the itch.io cover; needs a display); the ship script; Godot Director's play log, autoplay, dev menu and UI capture tools with the game's seeds (play_log.gd, autoplay_*.gd, dev_menu.gd, ui_screens.gd); autoplay is wired (T71): `bash tools/autoplay.sh smoke |
assets/sprites/, assets/audio/ |
pixel art and sound (agent-made, human-reviewed; see Project rules). Sprites are generated by tools/gen_sprites.gd (godot --headless -s tools/gen_sprites.gd; -- --preview <dir> writes 8× previews) and sounds by tools/gen_audio.gd |
design/ |
GDD, decisions and research notes (not loaded by Godot) |
playtesting/ |
playtest template and reports (not loaded by Godot) |
Architecture
One row per system: what it owns and where its boundary is. Screens depend on systems, never the other way round.
| System | Owns | Where | Talks to |
|---|---|---|---|
RunState (autoload) |
the current attempt: four Guys (seat, drunkenness, stamina, aboard), progress 0..1 from shore to island, elapsed time, phase LAUNCH → CROSSING → WON; owns the Paddling, Drunkenness, Stamina, Antics and Rescue instances and one RNG; the hazard effects' state (pending shove and stop, per-seat snag timers, per-hull water and its bailer), the beer-loss cooldown (beer_loss_cooldown), the empty-boat drift (empty_drift_multiplier) and lake_seed; new_attempt() (draws lake_seed; a shore reset keeps it), reset(), stroke(seat, hold_time, backwards), step(delta, boat_at) -> Motion (takes jumpers off the boat), set_progress(), arrive(), beer_overboard(hull, at), guy_overboard(seat, at), beast_grab(hull, from, spit_distance) (the hull's guy spat out toward the shore, beerless; emits guy_grabbed), pickup(seat), shore_touched() (resets only in CROSSING; a washed-up swimmer re-boards on his own in LAUNCH), shove()/take_shove(), push()/take_push() (sustained), set_extra_drift(), stop()/take_stop(), snag(hull, s), force_break(hull), water_aboard(hull, amount), can_paddle(seat); the play log (T70): every action, rescue step, hazard effect, antic, break, bar change, shore reset and win writes a PlayLog line with a stable id (action.stroke, rescue.jumped, hazard.snag, guy.antic, run.won, ...), a snapshot at each attempt start and shore reset; one log run per process, each new_attempt() (R, the touch hold, autoplay's reset) is attempt n+1 of it (attempt, T83); snapshot() and block_reason() feed the log's hooks; the dev menu's hooks (T72): debug_beer_left/right(), debug_force_antic(seat, kind), debug_set_drunkenness/stamina(seat, level), debug_force_break(seat), debug_snag(seat), debug_water(side), debug_win() and debug_inspect(); the rule classes Antics, Stamina, Drunkenness and Rescue each have a debug_inspect() |
scripts/autoload/run_state.gd |
the boat scene calls stroke/step and applies the Motion; the lake sets progress, calls arrive, pickup and shore_touched; screens read it; emits run_reset, shore_reset_done, stroke_landed, stroke_fumbled, guy_jumped, guy_fell_in, guy_picked_up, guy_grabbed, puked, beer_overboard_started |
PlayLog (autoload, Godot Director) |
the play log of each run: log_event(category, event, text, data), begin_run(), log_snapshot(), start_capture() for tests; the base is framework-owned (tools/play_log_base.gd), the game's seed tools/play_log.gd holds the hooks (_build_snapshot(), progress_block_reason(), view_changed) |
tools/play_log.gd |
registered first in project.godot; the hooks read RunState.snapshot() and RunState.block_reason(); lake.gd logs the lake and win views and the reset; tests capture lines with start_capture() (T70) |
DevMenu (autoload, Godot Director) |
the in-game dev menu on F12 in debug builds: tabs per registered system with its debug_* actions and debug_inspect(), and a Log tab; the base is framework-owned (tools/dev_menu_base.gd), the game's seed tools/dev_menu.gd registers the systems |
tools/dev_menu.gd |
registered last in project.godot; tabs Run (RunState: its debug_* hooks, new_attempt, shore_touched, pickup), Rescue, Antics (interrupt), Stamina (call_break), Drunkenness (relieve) and Log (T72); Beast (registered by lake.gd per lake, T84); absent in release builds |
Audio (autoload) |
the Sfx player pool, play(name, pitch_scale), loops for hazards (play_loop/stop_loop, capped), the master volume (held here until a settings system exists), and the mapping of RunState signals (stroke landed/fumbled, jump, pickup, beer overboard) to sounds; buses in assets/audio/default_bus_layout.tres |
scripts/autoload/audio.gd |
listens to RunState; inert during the check; sounds generated by tools/gen_audio.gd |
Lines (Resource) |
the Swedish lines per telegraph kind (guy wind-ups and hazards) and pick(kind, rng); data/lines.tres is the human's to edit |
scripts/resources/lines.gd |
RunState owns it (pick_line, telegraph(kind, hull) → signal telegraphed); hazards call _telegraph at their warning |
Names (Resource) |
the crew's nicknames and pick_four(rng); data/names.tres is the human's to edit |
scripts/resources/names.gd |
RunState owns it (guy_names drawn at new_attempt(), kept by a shore reset, name_of(seat)); the HUD and the win title show them |
Paddling (rule class) |
stroke events (seat, hold time, backwards: thrust × reverse_thrust_fraction negated, turn negated) and a timestep into thrust, signed turn (+ = right) and drift toward the shore; seats enum matching RunState; tuning in BoatTuning (scripts/resources/boat_tuning.gd, data/boat_tuning.tres) |
scripts/systems/paddling.gd |
pure: receives BoatTuning; RunState owns the instance; catamaran.gd applies its Motion with the boat feel values in BoatTuning (thrust_scale, turn_scale, damping) |
Drunkenness (rule class) |
one 0..1 level per seat, relieve(seat, amount) for the puke and pee drops; drink rate by state (break, water, no beer); stroke delay and miss chance per level (Curves); seeded miss roll; the timing bar at bar_level: a per-seat cycle of wind-up, bar_duration on, bar_cooldown off, paused while he is in the water (uses_bar, bar_winding_up, bar_position, bar_hit; bar_period, green_width); tuning in DrinkTuning (scripts/resources/drink_tuning.gd, data/drink_tuning.tres) |
scripts/systems/drunkenness.gd |
pure: receives DrinkTuning and an RNG; RunState owns the instance and steps it |
Antics (rule class) |
per seat IDLE → WINDING_UP (windup_time) → STANDING (rocks the boat) | PADDLE_DROPPED | NODDED_OFF | PEEING (knocked in by a shove ≥ pee_fall_shove) | PUKING (started by RunState at puke_level, never drawn) → RECOVERING; just_finished for the step an antic ran its course; one antic per antic_thresholds crossing per attempt, drawn from the caller's RNG; can_stroke, rock, knocked_in, interrupt; tunables on DrinkTuning |
scripts/systems/antics.gd |
pure; RunState owns the instance, steps it per guy aboard, adds the rock to the Motion's turn and applies the knock-in via guy_overboard; catamaran.gd shows each antic |
Stamina (rule class) |
one 0..1 level per seat; burn per stroke power, recovery; per-seat PADDLING → WARNING → BREAK machine (can_stroke, time_left, call_break for hazards); random breaks from the caller's RNG; tuning in StaminaTuning (scripts/resources/stamina_tuning.gd, data/stamina_tuning.tres) |
scripts/systems/stamina.gd |
pure: receives StaminaTuning and an RNG; RunState owns the instance and steps it; catamaran.gd reads its state for the telegraphs |
Hazards (rule class) |
hazard placement: the lake in bands of band_length, each drawing density_curve hazards from the weighted HazardSpec list (weight, first_band), objects placed within lake_width with their half_width footprint clear of the guaranteed path (skipped when no room), event triggers (HazardSpec.is_event) placed on it; the path is a corridor of path_width that never enters the beast's bounds or its warning band whose centre random-walks by at most path_wander per band from the launch to the island (path_center, corridor_x(tuning, seed, y)), all from one seed (plan(tuning, seed); per band plan_band(tuning, seed, band, generation), kept for the path and future events; objects are fixed and never refill) |
scripts/systems/hazards.gd |
pure; lake.gd spawns the planned scenes under its Hazards node from RunState.lake_seed; hazard scenes (scripts/hazards/*.gd) get tuning (LakeTuning) and spec from the lake and call RunState's effect methods on contact |
GeeseFlock (rule class) |
one flock: NESTING → HISSING (warning; the charge always follows) → CHARGING (pecks at an interval while in range, each a beer overboard on the nearest hull) → RETURNING → cooldown; its position and timers; tunables geese_* on LakeTuning |
scripts/systems/geese.gd |
pure; scripts/hazards/geese.gd steps it with the boat's position and applies pecks via RunState.beer_overboard |
MotorboatWake (rule class) |
one wake event: WAITING → CROSSING (a motorboat across the lake ahead of the trigger) → ROLLING (a front toward the shore at wake_speed) → SPENT; a head-on boat is untouched, a broadside one gets a beer overboard on the high hull, a sideways shove and water aboard; tunables wake_* on LakeTuning |
scripts/systems/wake.gd |
pure; scripts/hazards/wake.gd steps it with the boat's position and heading and applies the hit via RunState |
WindGust (rule class) |
one gust event: WAITING → WARNING (gust_warning, ripples) → BLOWING (a sideways push of gust_shove for gust_time, direction from the trigger's side) → SPENT |
scripts/systems/gust.gd |
pure; scripts/hazards/gust.gd steps it and feeds RunState.push() |
Headwind (rule class) |
one harsh-wind stretch (headwind_length tall, lake wide) where the drift is stronger by headwind_force |
scripts/systems/headwind.gd |
pure; scripts/hazards/headwind.gd calls RunState.set_extra_drift() while the boat is inside |
CrossCurrent (rule class) |
one fixed lake-wide stripe of stripe_length pushing sideways by stripe_force (direction from its side of the lake) |
scripts/systems/cross_current.gd |
pure; scripts/hazards/cross_current.gd feeds RunState.push() while the boat is inside |
JetSki (rule class) |
one jet-ski event: WAITING → WARNING (jetski_warning) → CROSSING (a diagonal from the lake edge at the trigger's y through a point jetski_lead ahead of the boat, at jetski_speed) → SPENT; one hit within jetski_hit_radius: a shove of jetski_shove along its travel and a beer overboard on the hull it came from |
scripts/systems/jetski.gd |
pure; scripts/hazards/jetski.gd steps it and applies the hit via RunState |
Beast (rule class) |
the one Loch-Brez beast per attempt: SUBMERGED → CIRCLING (beast_warning) → RAMMING (closes at beast_speed from the side away from the lake's middle; a hit shoves by beast_shove toward the middle and grabs the hull's guy) → DIVING; rises when the boat is out of bounds (beast_edge_margin, beast_beyond_island) until it is back, and in open water only if beast_random_rate is above zero (it is 0: never a random encounter); a puke heard in open water rolls beast_puke_chance (hear_puke); a warning band of beast_warn_margin inside the bounds (near_bounds, check_approach once per approach → telegraph edge); rise_cause names why it rose; debug_inspect() for the dev menu's Beast tab, which lake.gd registers on spawn and replaces per lake (T84) |
scripts/systems/beast.gd |
pure; scripts/hazards/beast.gd (spawned once per lake by lake.gd, not planned) steps it with its own RNG (Beast.rng_for(RunState.lake_seed), so the same lake gives the same visits) and applies the hit via RunState.shove and RunState.beast_grab; idle on WON |
Stats (rule class) |
the crossing's counters (T86): strokes landed, fumbles, reverse strokes, beers overboard and rescued, jumps, falls, beast grabs, pickups, pukes, pisses, naps, stand-ups, paddles dropped, breaks, snags, water bailed, shoves, stops, pushes, shore resets, drunkenness gained and the drunkest level; count(event, data) maps the play log's event ids to them, add_drunk(), beers_drunk(level_per_beer) (gained ÷ DrinkTuning.level_per_beer, rounded), times_in_water(), rows(elapsed, level_per_beer) in a stable order with zero rows kept, format_time() |
scripts/systems/stats.gd |
pure; RunState owns the instance, feeds it from _log and the drink step, resets it at new_attempt() (a shore reset keeps it); win_cutscene.gd fills %Stats from rows() |
Rescue (rule class) |
the guys in the water and their beers: LooseBeer records (a beer bobbing away while its guy winds up for jump_delay), Swimmer records (seat, position, beer, SWIMMING → FLOATING, has_beer; kind(swim_back_delay) names what he is doing, KIND_FETCH/DRINK/FELL/BACK/WASHED_UP, read by the bubbles and the HUD, T80), who jumps for a beer on a hull, step(delta, current, tuning, boat_at) -> seats that jumped, winding_up, within_reach, pickup; no limit on swimmers; after swim_back_delay a floating guy swims slowly back toward the boat (swim_back_speed); nothing loose drifts past the shoreline (BoatTuning.shore_margin), a floating guy there is washed_up; guy_overboard for beerless falls (pee antic; a guy winding up swims for his beer instead) |
scripts/systems/rescue.gd |
pure; RunState owns the instance; lake.gd draws swimmers and beers, picks up within BoatTuning.pickup_radius, and reports shore contact |
Ownership
| Area | Owner | Placeholder |
|---|---|---|
| Design, balance, art direction, priorities | human | open questions and ## PLACEHOLDER tunables |
| Code, data schemas, tests, records | agent | (none) |
| Art / sprites | agent (generated by tools/gen_sprites.gd), human reviews |
flat-colour primitives under Visual |
| Audio / SFX | agent (generated by tools/gen_audio.gd), human reviews |
silence |
| Player-facing text (Swedish lines, names) | agent writes, human edits data/lines.tres and data/names.tres |
(none) |
| UI / screen layout | agent builds from the GDD; no director drawings yet | minimal functional screen |
Project rules
- The agent may produce player-facing text, pixel-art sprites and audio, not only placeholders. The human still owns art direction and reviews every asset like any other change.
- Remote: https://github.com/Freakling/PKSEI.git (
origin). The agent pushes only when the human asks. - Model sizing: on
- XS: claude-haiku-4-5-20251001
- S: claude-haiku-4-5-20251001
- M: claude-sonnet-5-5
- L: claude-opus-5-5
- XL: claude-opus-5-5
- Dev menu: in game (F12, debug builds only)
- Exports run on this machine:
bash tools/ship.sh --export-only(the Web preset);/exportruns them too. Release is Q7; until thenbash tools/ship.shpushes to itch.io.
