diff --git a/packages/ludic.fishing/README.md b/packages/ludic.fishing/README.md new file mode 100644 index 00000000..06b210ae --- /dev/null +++ b/packages/ludic.fishing/README.md @@ -0,0 +1,69 @@ +# ludic.fishing + +One angler's line: a cast into water deep enough, the wait for a bite (bait halves it), the strike, +the reel's fight - a marker sweeping a bar and a green band to press in, a Gentle ring of one +press, a legend that runs and takes line back - and the landing, with the species, length and +weight from data. Uses [`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.fishing" +``` + +The package never draws, never speaks and never touches a pack. It reports what happened as facts +(`fishing_facts()`), and the game takes the bait, puts the fish in the pack, shows it and says the +line. Its dice are its own `Rng`. + +## The species + +`FishSpecies` is an `open registry` the game fills, `FISH_` in the order written: + +```ludic +def FishSpecies cutthroat { weight: 0.45 } +def FishSpecies brook { weight: 0.25, cm_min: 20, cm_spread: 10 } +def FishSpecies old_maroon { legend: true, cm_min: 70, cm_spread: 25, skill_cm: 0.0, need: 5, runs: 2, fight: 30.0, speed: 1.3, speed_per_hit: 0.22, closes: 0.12 } +``` + +| field | | +| --- | --- | +| `weight` | how often it takes, among the everyday ones | +| `cm_min`, `cm_spread`, `skill_cm` | length: `cm_min` + up to `cm_spread`, + `skill_cm` per point of skill | +| `kg_k` | weight is `kg_k x cm^3` (a trout by default) | +| `legend`, `chance`, `chance_deep` | takes only a lure, at `chance` in the shallows and `chance_deep` past the deep line or afloat (+ skill) | +| `need`, `runs`, `fight` | crossings to land it, runs it makes, seconds before the line goes slack | +| `speed`, `speed_per_hit`, `closes`, `floor` | the marker's sweep, and a window that narrows with each crossing won (at every rung) | + +## The port + +```ludic +export port FishingWorld { + depth: fn(float, float) -> float # metres of water at a point + afloat: fn() -> bool # in a boat (unbound: no) + has_bait: fn() -> bool # (unbound: no) + has_lure: fn() -> bool # (unbound: no) + skill: fn() -> float # gear or hands (unbound: 0) + bite_mult: fn() -> float # the wait, x (unbound: 1) + window: fn(int) -> float # the green's half width for a species (unbound: 0.12) + level: fn() -> int # FISHING_RELAXED / _STANDARD / _HARD / _GENTLE (unbound: relaxed) +} +``` + +## API + +| | | +| --- | --- | +| `fishing_config(min_depth, deep, seed)` | 0.15 m to cast, 2.5 m is deep, the line's seed | +| `fishing_cast(x, z) -> int` | `FISHING_CAST_OK`, `FISHING_NOT_WATER`, `FISHING_BUSY` | +| `fishing_press()` | the one button: waiting it reels in, on a bite it strikes, on a fish it reels | +| `fishing_run(dt)`, `fishing_stop()`, `fishing_set_wait(secs)` | real seconds of line; put away; the bite in exactly `secs` | +| `fishing_state()` (`FISHING_OFF`, `_WAIT`, `_BITE_ON`, `_REEL`), `fishing_active()`, `fishing_species()`, `fishing_cm()`, `fishing_line_x()`, `fishing_line_z()`, `fishing_baited()`, `fishing_lured()` | the line | +| `fishing_marker()`, `fishing_window()`, `fishing_window_lo()`, `fishing_window_hi()`, `fishing_hits()`, `fishing_need()`, `fishing_running()`, `fishing_gentle()`, `fishing_ring()` | the fight, for a HUD | +| `fishing_caught()`, `fishing_biggest()`, `fishing_legend_caught()`, `fishing_record_set(n, cm, legend)` | the record | +| `fishing_weight_of(species, cm)`, `fishing_legend()`, `fishing_water_at(x, z)` | questions | +| `fishing_facts() -> Queue` | `{ what, species, cm, kg, hits, need, bait, lure, x, z }`: `FISHING_CAST`, `_BAIT_USED`, `_BITE`, `_MISSED`, `_HOOKED`, `_GOOD`, `_CHECKED`, `_TURNED`, `_RUNS`, `_TIRED`, `_REELED_IN`, `_CAUGHT`, `_LINE_SNAPPED`, `_SLACK` | +| `fishing_reset()`, `fishing_system() -> System` | `"fishing"`, `PH_SIMULATE`: its tick, and the record in its save section (a line in the water is not saved) | + +## Tests + +```bash +ludic test packages/ludic.fishing +``` diff --git a/packages/ludic.fishing/fight.ludic b/packages/ludic.fishing/fight.ludic new file mode 100644 index 00000000..668a22a7 --- /dev/null +++ b/packages/ludic.fishing/fight.ludic @@ -0,0 +1,57 @@ +# fight.ludic - what takes, how big it is, and the green band the marker has to cross in +# a legend on a lure, likelier in deep water and with skill; else an everyday one by weight +function fishing_pick() -> int { + let leg = fishing_legend() + if leg >= 0 and fishing_lure_on { + let k = FishSpecies[leg] + var p = k.chance + if FishingWorld.afloat() or FishingWorld.depth(fishing_bx, fishing_bz) >= fishing_deep { p = k.chance_deep } + if fishing_rand() < p + FishingWorld.skill() { return leg } + } + var total = 0.0 + for s in 0 .. FISH_COUNT { if not FishSpecies[s].legend { total = total + FishSpecies[s].weight } } + var r = fishing_rand() * total + var last = -1 + for s in 0 .. FISH_COUNT { + if FishSpecies[s].legend { continue } + last = s + r = r - FishSpecies[s].weight + if r < 0.0 { return s } + } + return last +} + +function fishing_size(s: int) -> int { + let k = FishSpecies[s] + let spread = int(fishing_rand() * float(k.cm_spread + 1)) + return k.cm_min + spread + int(FishingWorld.skill() * k.skill_cm) +} + +# the green's half width this crossing: a fish that closes it does so at every rung, and Hard +# closes it for everything else +export function fishing_window() -> float { + if not fishing_kind_ok(fishing_sp) { return 0.12 } + let k = FishSpecies[fishing_sp] + let hw = FishingWorld.window(fishing_sp) + if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_won)), hw * k.floor) } + if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_won)), hw * 0.55) } + return hw +} + +export function fishing_window_lo() -> float { return Math.clamp(fishing_win_c - fishing_window(), 0.0, 1.0) } +export function fishing_window_hi() -> float { return Math.clamp(fishing_win_c + fishing_window(), 0.0, 1.0) } + +# where it sits: the middle on Relaxed, somewhere new each crossing above it +function fishing_new_window() -> void { + let lv = FishingWorld.level() + if lv == FISHING_RELAXED or fishing_gentle_on { + fishing_win_c = 0.5 + return + } + let hw = fishing_window() + 0.05 + fishing_win_c = hw + fishing_rand() * (1.0 - hw - hw) +} + +function fishing_in_window() -> bool { + return fishing_mark > fishing_win_c - fishing_window() and fishing_mark < fishing_win_c + fishing_window() +} diff --git a/packages/ludic.fishing/index.ludic b/packages/ludic.fishing/index.ludic new file mode 100644 index 00000000..cc285a58 --- /dev/null +++ b/packages/ludic.fishing/index.ludic @@ -0,0 +1,12 @@ +# ludic.fishing - one angler's line: the game says where the water is and how deep, what tackle +# is in the pack and how good the rod is; the package runs the fight and reports what happened +module ludic_fishing uses ludic_base +numbers float +import "ludic.base" +import "kinds.ludic" +import "ports.ludic" +import "state.ludic" +import "fight.ludic" +import "verbs.ludic" +import "run.ludic" +import "system.ludic" diff --git a/packages/ludic.fishing/kinds.ludic b/packages/ludic.fishing/kinds.ludic new file mode 100644 index 00000000..d2b4cea9 --- /dev/null +++ b/packages/ludic.fishing/kinds.ludic @@ -0,0 +1,66 @@ +# kinds.ludic - what swims: a species' size, weight and fight as data, and the facts a line reports +export property FishKind { + key: string = "" + weight: float = 1.0 # how often it takes, among the everyday ones + cm_min: int = 28 # its length: cm_min plus up to cm_spread + cm_spread: int = 20 + skill_cm: float = 60.0 # centimetres a skill of 1 adds + kg_k: float = 0.0000110 # its weight is kg_k x cm cubed + legend: bool = false # takes only a lure, and only at `chance` + chance: float = 0.2 # a legend's chance of taking the lure, in the shallows + chance_deep: float = 0.35 # ... and in deep water + need: int = 3 # crossings to land it + runs: int = 0 # runs it makes, when the only right answer is to leave the line + fight: float = 12.0 # seconds before the line goes slack + speed: float = 0.9 # the marker's sweep, and how much faster each crossing won + speed_per_hit: float = 0.0 + closes: float = 0.0 # the window narrows this much a crossing, down to `floor` of it + floor: float = 0.45 +} + +# the game fills it: `def FishSpecies brook { weight: 0.25 }`, and one legend +export open registry FishSpecies of FishKind as FISH + +# what happened on the line +export const FISHING_CAST: int = 0 +export const FISHING_BAIT_USED: int = 1 +export const FISHING_BITE: int = 2 +export const FISHING_MISSED: int = 3 # too slow on the bite +export const FISHING_HOOKED: int = 4 +export const FISHING_GOOD: int = 5 # a crossing won +export const FISHING_CHECKED: int = 6 # the reel touched mid-run: a crossing lost +export const FISHING_TURNED: int = 7 # a legend took line back +export const FISHING_RUNS: int = 8 +export const FISHING_TIRED: int = 9 # a run is over +export const FISHING_REELED_IN: int = 10 # put away before a bite +export const FISHING_CAUGHT: int = 11 +export const FISHING_LINE_SNAPPED: int = 12 # it threw the hook +export const FISHING_SLACK: int = 13 # the fight ran out of time + +export property FishingFact { + what: int = 0 + species: int = -1 + cm: int = 0 + kg: float = 0.0 + hits: int = 0 + need: int = 0 + bait: bool = false + lure: bool = false + x: float = 0.0 + z: float = 0.0 +} + +export function fishing_kind_ok(s: int) -> bool { return s >= 0 and s < FISH_COUNT } + +# how heavy a fish of this species and length is +export function fishing_weight_of(s: int, cm: int) -> float { + if not fishing_kind_ok(s) { return 0.0 } + let c = float(cm) + return FishSpecies[s].kg_k * c * c * c +} + +# the first legend, or -1 +export function fishing_legend() -> int { + for s in 0 .. FISH_COUNT { if FishSpecies[s].legend { return s } } + return -1 +} diff --git a/packages/ludic.fishing/package.ludic b/packages/ludic.fishing/package.ludic new file mode 100644 index 00000000..3b936911 --- /dev/null +++ b/packages/ludic.fishing/package.ludic @@ -0,0 +1,5 @@ +# ludic.fishing - a rod at the water: cast, the wait for a bite, the strike, the reel's fight and +# the landing, species from an open registry. Uses ludic.base and nothing else. +package "ludic.fishing" +version "0.1.0" +kind source diff --git a/packages/ludic.fishing/ports.ludic b/packages/ludic.fishing/ports.ludic new file mode 100644 index 00000000..97f27c23 --- /dev/null +++ b/packages/ludic.fishing/ports.ludic @@ -0,0 +1,24 @@ +# ports.ludic - what a line asks the game: the water, the tackle, the angler +# the ladder of difficulty, by the numbers a game is likely to keep them in +export const FISHING_RELAXED: int = 0 # the window dead centre +export const FISHING_STANDARD: int = 1 # it moves each crossing +export const FISHING_HARD: int = 2 # it moves and closes +export const FISHING_GENTLE: int = 3 # no bar: a ring and one press (a legend still fights) + +# Unbound, the angler is on the bank, has no bait or lure and no skill, waits the plain wait, +# holds a 0.12 window and plays Relaxed. +export port FishingWorld { + depth: fn(float, float) -> float # metres of water at a point (0: dry) + afloat: fn() -> bool = fn fishing_no # in a boat: every cast lands in deep water + has_bait: fn() -> bool = fn fishing_no + has_lure: fn() -> bool = fn fishing_no + skill: fn() -> float = fn fishing_zero # bigger fish, and a legend more often + bite_mult: fn() -> float = fn fishing_one # the wait, x + window: fn(int) -> float = fn fishing_plain_window # the green's half width for a species + level: fn() -> int = fn fishing_relaxed +} +function fishing_no() -> bool { return false } +function fishing_zero() -> float { return 0.0 } +function fishing_one() -> float { return 1.0 } +function fishing_plain_window(s: int) -> float { return 0.12 } +function fishing_relaxed() -> int { return FISHING_RELAXED } diff --git a/packages/ludic.fishing/run.ludic b/packages/ludic.fishing/run.ludic new file mode 100644 index 00000000..1325e024 --- /dev/null +++ b/packages/ludic.fishing/run.ludic @@ -0,0 +1,64 @@ +# run.ludic - `dt` real seconds of a line in the water: the wait, the bite, and the fight +function fishing_run_wait() -> void { + if fishing_t >= 0.0 { return } + fishing_st = FISHING_BITE_ON + fishing_t = 1.6 + fishing_sp = fishing_pick() + fishing_say(FISHING_BITE) +} + +# a fish that runs: the marker is all over the bar and nothing done with it helps +function fishing_run_runs(dt: float) -> float { + let k = FishSpecies[fishing_sp] + if fishing_run_t > 0.0 { + fishing_run_t = fishing_run_t - dt + if fishing_run_t <= 0.0 { + fishing_run_t = 0.0 + fishing_say(FISHING_TIRED) + } + return 4.2 + } + if fishing_run_left > 0 { + fishing_run_next = fishing_run_next - dt + if fishing_run_next <= 0.0 { + fishing_run_left -= 1 + fishing_run_t = 2.2 + fishing_run_next = 4.0 + fishing_rand() * 4.0 + fishing_say(FISHING_RUNS) + } + } + return k.speed + k.speed_per_hit * float(fishing_won) +} + +function fishing_run_reel(dt: float) -> void { + let sp = fishing_run_runs(dt) + if fishing_gentle_on { + fishing_ring_t = fishing_ring_t - dt + if fishing_ring_t <= 0.0 { fishing_lost(FISHING_LINE_SNAPPED) } + return + } + fishing_mark = fishing_mark + sp * fishing_dir * dt + if fishing_mark > 1.0 { + fishing_mark = 1.0 + fishing_dir = -1.0 + } + if fishing_mark < 0.0 { + fishing_mark = 0.0 + fishing_dir = 1.0 + } + if fishing_t < 0.0 { fishing_lost(FISHING_SLACK) } +} + +export function fishing_run(dt: float) -> void { + if fishing_st == FISHING_OFF { return } + fishing_t = fishing_t - dt + if fishing_st == FISHING_WAIT { + fishing_run_wait() + return + } + if fishing_st == FISHING_BITE_ON { + if fishing_t < 0.0 { fishing_lost(FISHING_MISSED) } + return + } + fishing_run_reel(dt) +} diff --git a/packages/ludic.fishing/state.ludic b/packages/ludic.fishing/state.ludic new file mode 100644 index 00000000..0a6eac12 --- /dev/null +++ b/packages/ludic.fishing/state.ludic @@ -0,0 +1,89 @@ +# state.ludic - the line, the record, the numbers a game may change, and what a game asks +export const FISHING_OFF: int = 0 +export const FISHING_WAIT: int = 1 +export const FISHING_BITE_ON: int = 2 +export const FISHING_REEL: int = 3 + +var fishing_st: int = 0 +var fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight +var fishing_bx: float = 0.0 # where the line is in the water +var fishing_bz: float = 0.0 +var fishing_sp: int = -1 # what is on it +var fishing_len: int = 0 # cm +var fishing_bait_on: bool = false +var fishing_lure_on: bool = false +var fishing_mark: float = 0.0 # 0..1 across the bar +var fishing_dir: float = 1.0 +var fishing_win_c: float = 0.5 # where the green sits this crossing +var fishing_ring_t: float = 0.0 # Gentle: seconds left to press +var fishing_gentle_on: bool = false +var fishing_won: int = 0 # crossings won +var fishing_need_n: int = 3 +var fishing_run_t: float = 0.0 # seconds left of a run +var fishing_run_left: int = 0 +var fishing_run_next: float = 0.0 +var fishing_dice: Rng = null +var fishing_fact_q: Queue = null +var fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed +var fishing_big: int = 0 +var fishing_leg: bool = false + +var fishing_min_depth: float = 0.15 # a cast needs this much water +var fishing_deep: float = 2.5 # a legend's deep-water chance from here down +var fishing_seed_n: int = 4242 + +# a cast needs `min_depth` metres; `deep` is where a legend is likelier; the line's own dice +export function fishing_config(min_depth: float, deep: float, seed: int) -> void { + fishing_min_depth = min_depth + fishing_deep = deep + fishing_seed_n = seed + fishing_dice = rng_new(seed) +} + +export function fishing_facts() -> Queue { + if fishing_fact_q == null { fishing_fact_q = queue_new("fishing.facts") } + return fishing_fact_q +} + +function fishing_rand() -> float { + if fishing_dice == null { fishing_dice = rng_new(fishing_seed_n) } + return rng_float(fishing_dice) +} + +function fishing_say(what: int) -> void { + let f = new FishingFact + f.what = what + f.species = fishing_sp + f.cm = fishing_len + f.kg = fishing_weight_of(fishing_sp, fishing_len) + f.hits = fishing_won + f.need = fishing_need_n + f.bait = fishing_bait_on + f.lure = fishing_lure_on + f.x = fishing_bx + f.z = fishing_bz + q_push(fishing_facts(), f) +} + +export function fishing_state() -> int { return fishing_st } +export function fishing_active() -> bool { return fishing_st != FISHING_OFF } +export function fishing_species() -> int { return fishing_sp } +export function fishing_cm() -> int { return fishing_len } +export function fishing_line_x() -> float { return fishing_bx } +export function fishing_line_z() -> float { return fishing_bz } +export function fishing_marker() -> float { return fishing_mark } +export function fishing_hits() -> int { return fishing_won } +export function fishing_need() -> int { return fishing_need_n } +export function fishing_running() -> bool { return fishing_run_t > 0.0 } +export function fishing_gentle() -> bool { return fishing_gentle_on } +export function fishing_ring() -> float { return fishing_ring_t } +export function fishing_baited() -> bool { return fishing_bait_on } +export function fishing_lured() -> bool { return fishing_lure_on } +export function fishing_caught() -> int { return fishing_n } +export function fishing_biggest() -> int { return fishing_big } +export function fishing_legend_caught() -> bool { return fishing_leg } + +# can a line land at (x, z)? +export function fishing_water_at(x: float, z: float) -> bool { + return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_min_depth +} diff --git a/packages/ludic.fishing/system.ludic b/packages/ludic.fishing/system.ludic new file mode 100644 index 00000000..e8f48db9 --- /dev/null +++ b/packages/ludic.fishing/system.ludic @@ -0,0 +1,41 @@ +# system.ludic - fishing as a system: its tick, and the record in its own save section (a line in +# the water is never saved) +export function fishing_reset() -> void { + fishing_stop() + fishing_n = 0 + fishing_big = 0 + fishing_leg = false + fishing_dice = rng_new(fishing_seed_n) + q_clear(fishing_facts()) +} + +# the record outright (staging, a test, the network's word) +export function fishing_record_set(caught: int, biggest: int, legend: bool) -> void { + fishing_n = caught + fishing_big = biggest + fishing_leg = legend +} + +export function fishing_save() -> Val { + let v = Value.object() + sv_put_int(v, "caught", fishing_n) + sv_put_int(v, "biggest", fishing_big) + sv_put_bool(v, "legend", fishing_leg) + return v +} + +export function fishing_load(v: Val, version: int) -> void { + fishing_reset() + fishing_record_set(sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false)) +} + +function fishing_sys_tick(t: Tick) -> void { fishing_run(t.dt) } + +export function fishing_system() -> System { + let s = system_new("fishing", PH_SIMULATE) + s.reset = fn fishing_reset + s.tick = fn fishing_sys_tick + s.save = fn fishing_save + s.load = fn fishing_load + return s +} diff --git a/packages/ludic.fishing/tests/fishing_test.ludic b/packages/ludic.fishing/tests/fishing_test.ludic new file mode 100644 index 00000000..49c59611 --- /dev/null +++ b/packages/ludic.fishing/tests/fishing_test.ludic @@ -0,0 +1,255 @@ +# fishing_test.ludic - a line over a fake lake: casting only into water, bait halving the wait and +# said as a fact, the bite, the strike, the reel's window, a legend on a lure that runs and turns, +# the gentle ring, losing a fish every way there is, species and weight from data, and the save +import "ludic.fishing" +import "ludic.base" +program FishingTest { + numbers float + def FishSpecies cutthroat { weight: 0.45 } + def FishSpecies rainbow { weight: 0.30 } + def FishSpecies brook { weight: 0.25, cm_min: 20, cm_spread: 10 } + def FishSpecies old_maroon { legend: true, cm_min: 70, cm_spread: 25, skill_cm: 0.0, chance: 1.0, chance_deep: 1.0, need: 5, runs: 2, fight: 30.0, speed: 1.3, speed_per_hit: 0.22, closes: 0.12 } + + var bait: bool = false + var lure: bool = false + var boat: bool = false + var level: int = 0 + var skill: float = 0.0 + + # water past x = 10, deepening a metre every ten + function depth(x: float, z: float) -> float { return Math.max((x - 10.0) / 10.0, 0.0) } + function afloat() -> bool { return boat } + function has_bait() -> bool { return bait } + function has_lure() -> bool { return lure } + function fake_skill() -> float { return skill } + function fake_level() -> int { return level } + + bind FishingWorld { depth: fn depth, afloat: fn afloat, has_bait: fn has_bait, has_lure: fn has_lure, skill: fn fake_skill, level: fn fake_level } + + function fresh() -> void { + fishing_config(0.15, 2.5, 99) + fishing_reset() + bait = false + lure = false + boat = false + level = FISHING_RELAXED + skill = 0.0 + } + + function facts() -> []FishingFact { return q_drain(fishing_facts()) } + function saw(fs: []FishingFact, what: int) -> bool { + for i in 0 .. len(fs) { if fs[i].what == what { return true } } + return false + } + + # run until it bites, then strike + function hook() -> void { + fishing_set_wait(0.0) + fishing_run(0.02) + fishing_press() + } + + # hold the marker in the green and press, until it is landed or `n` presses are spent + function play(n: int) -> void { + for i in 0 .. n { + if fishing_state() != FISHING_REEL { return } + var guard = 0 + while (fishing_running() or fishing_marker() <= fishing_window_lo() or fishing_marker() >= fishing_window_hi()) and guard < 4000 { + fishing_run(0.005) + guard += 1 + } + if fishing_state() == FISHING_REEL { fishing_press() } + } + } + + test "a cast lands only in water, and never twice" { + fresh() + expect_eq(fishing_cast(5.0, 0.0), FISHING_NOT_WATER) + expect(not fishing_active()) + expect_eq(fishing_cast(20.0, 0.0), FISHING_CAST_OK) + expect_eq(fishing_state(), FISHING_WAIT) + expect_eq(fishing_cast(20.0, 0.0), FISHING_BUSY) + boat = true + fishing_stop() + expect_eq(fishing_cast(0.0, 0.0), FISHING_CAST_OK) + } + + test "bait is used and said, and halves the wait" { + fresh() + fishing_cast(20.0, 0.0) + expect(not saw(facts(), FISHING_BAIT_USED)) + var plain = 0 + while fishing_state() == FISHING_WAIT { + fishing_run(0.1) + plain += 1 + } + expect(plain <= 201) + fresh() + bait = true + fishing_cast(20.0, 0.0) + let fs = facts() + expect(saw(fs, FISHING_BAIT_USED)) + expect(fs[0].bait) + var baited = 0 + while fishing_state() == FISHING_WAIT { + fishing_run(0.1) + baited += 1 + } + expect(baited <= 101) + } + + test "a bite missed is gone, a bite struck is hooked" { + fresh() + fishing_cast(20.0, 0.0) + fishing_set_wait(0.0) + fishing_run(0.02) + expect_eq(fishing_state(), FISHING_BITE_ON) + expect(fishing_species() >= 0) + fishing_run(2.0) + expect(not fishing_active()) + expect(saw(facts(), FISHING_MISSED)) + fishing_cast(20.0, 0.0) + hook() + expect_eq(fishing_state(), FISHING_REEL) + expect(saw(facts(), FISHING_HOOKED)) + } + + test "everyday fish only without a lure, sized and weighed from the data" { + fresh() + for i in 0 .. 40 { + fishing_cast(20.0, 0.0) + hook() + let s = fishing_species() + expect(s != FISH_OLD_MAROON) + let k = FishSpecies[s] + expect(fishing_cm() >= k.cm_min and fishing_cm() <= k.cm_min + k.cm_spread) + fishing_stop() + } + expect(fishing_weight_of(FISH_CUTTHROAT, 50) > fishing_weight_of(FISH_CUTTHROAT, 30)) + } + + test "reeled in the green three times, it is landed and counted" { + fresh() + fishing_cast(20.0, 0.0) + hook() + facts() + play(3) + let fs = facts() + expect(saw(fs, FISHING_CAUGHT)) + expect_eq(fishing_caught(), 1) + expect(fishing_biggest() > 0) + expect(not fishing_active()) + let last = fs[len(fs) - 1] + expect(last.kg > 0.0) + expect(last.x == 20.0) + } + + test "a press outside the green throws the hook" { + fresh() + fishing_cast(20.0, 0.0) + hook() + while fishing_marker() < fishing_window_hi() + 0.01 { fishing_run(0.005) } + fishing_press() + expect(saw(facts(), FISHING_LINE_SNAPPED)) + expect(not fishing_active()) + } + + test "a fight left alone goes slack" { + fresh() + fishing_cast(20.0, 0.0) + hook() + for i in 0 .. 700 { fishing_run(0.02) } + expect(saw(facts(), FISHING_SLACK)) + expect_eq(fishing_caught(), 0) + } + + test "a legend takes the lure, runs, turns, and is landed at five" { + fresh() + lure = true + fishing_cast(20.0, 0.0) + hook() + expect_eq(fishing_species(), FISH_OLD_MAROON) + expect_eq(fishing_need(), 5) + let w0 = fishing_window() + play(2) + expect(fishing_window() < w0) + for i in 0 .. 400 { + if not fishing_running() { fishing_run(0.02) } + } + let fs = facts() + expect(saw(fs, FISHING_RUNS)) + if fishing_running() { + let before = fishing_hits() + fishing_press() + expect_eq(fishing_hits(), before - 1) + } + play(40) + expect(fishing_legend_caught()) + let last = facts() + expect(last[len(last) - 1].lure) + } + + test "gentle: one press inside the ring, and the ring closes" { + fresh() + level = FISHING_GENTLE + fishing_cast(20.0, 0.0) + hook() + expect(fishing_gentle()) + expect_eq(fishing_need(), 1) + fishing_press() + expect_eq(fishing_caught(), 1) + fishing_cast(20.0, 0.0) + hook() + fishing_run(2.1) + expect(saw(facts(), FISHING_LINE_SNAPPED)) + } + + test "a legend fights on gentle too" { + fresh() + level = FISHING_GENTLE + lure = true + fishing_cast(20.0, 0.0) + hook() + expect(not fishing_gentle()) + expect_eq(fishing_need(), 5) + } + + test "hard closes the window for everyday fish, relaxed keeps it centred" { + fresh() + fishing_cast(20.0, 0.0) + hook() + expect(fishing_window_lo() + fishing_window_hi() > 0.999 and fishing_window_lo() + fishing_window_hi() < 1.001) + fishing_stop() + level = FISHING_HARD + fishing_cast(20.0, 0.0) + hook() + let w0 = fishing_window() + play(1) + expect(fishing_window() < w0) + } + + test "skill makes a fish bigger" { + fresh() + skill = 1.0 + fishing_cast(20.0, 0.0) + hook() + let k = FishSpecies[fishing_species()] + expect(fishing_cm() >= k.cm_min + 60) + } + + test "the record is saved, and a line in the water is not" { + fresh() + fishing_record_set(7, 52, true) + fishing_cast(20.0, 0.0) + let root = save_tree() + save_section(root, "fishing", 1, fishing_save()) + let text = save_encode(root) + fishing_reset() + let n = load_section(save_decode(text), "fishing") + fishing_load(n.data, n.version) + expect_eq(fishing_caught(), 7) + expect_eq(fishing_biggest(), 52) + expect(fishing_legend_caught()) + expect(not fishing_active()) + } +} diff --git a/packages/ludic.fishing/verbs.ludic b/packages/ludic.fishing/verbs.ludic new file mode 100644 index 00000000..9f1767c6 --- /dev/null +++ b/packages/ludic.fishing/verbs.ludic @@ -0,0 +1,105 @@ +# verbs.ludic - casting, the one button (reel in, strike, reel), landing and putting the rod away +export const FISHING_CAST_OK: int = 0 +export const FISHING_NOT_WATER: int = 1 +export const FISHING_BUSY: int = 2 + +# a line into (x, z): bait halves the wait (and is a fact, for the game to take from the pack) +export function fishing_cast(x: float, z: float) -> int { + if fishing_st != FISHING_OFF { return FISHING_BUSY } + fishing_bx = x + fishing_bz = z + if not fishing_water_at(x, z) { return FISHING_NOT_WATER } + fishing_st = FISHING_WAIT + fishing_sp = -1 + fishing_len = 0 + fishing_lure_on = FishingWorld.has_lure() + fishing_bait_on = FishingWorld.has_bait() + var wait = 6.0 + fishing_rand() * 14.0 + if fishing_bait_on { wait = wait * 0.5 } + fishing_t = wait * FishingWorld.bite_mult() + if fishing_bait_on { fishing_say(FISHING_BAIT_USED) } + fishing_say(FISHING_CAST) + return FISHING_CAST_OK +} + +# the bite comes exactly `secs` after now (staging, footage) +export function fishing_set_wait(secs: float) -> void { + if fishing_st == FISHING_WAIT { fishing_t = secs } +} + +export function fishing_stop() -> void { + fishing_st = FISHING_OFF + fishing_run_t = 0.0 +} + +function fishing_strike() -> void { + let k = FishSpecies[fishing_sp] + fishing_st = FISHING_REEL + fishing_won = 0 + fishing_mark = 0.0 + fishing_dir = 1.0 + fishing_t = k.fight + fishing_need_n = k.need + fishing_run_t = 0.0 + fishing_run_left = k.runs + fishing_run_next = 3.0 + fishing_rand() * 3.0 + fishing_len = fishing_size(fishing_sp) + fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend + fishing_ring_t = 0.0 + if fishing_gentle_on { + fishing_need_n = 1 + fishing_ring_t = 2.0 + } + fishing_new_window() + fishing_say(FISHING_HOOKED) +} + +function fishing_land() -> void { + fishing_n += 1 + if fishing_len > fishing_big { fishing_big = fishing_len } + if FishSpecies[fishing_sp].legend { fishing_leg = true } + fishing_say(FISHING_CAUGHT) + fishing_stop() +} + +function fishing_lost(what: int) -> void { + fishing_say(what) + fishing_stop() +} + +function fishing_reel_press() -> void { + if fishing_gentle_on { + if fishing_ring_t > 0.0 { fishing_land() } else { fishing_lost(FISHING_LINE_SNAPPED) } + return + } + if fishing_run_t > 0.0 { + fishing_won = Math.max(fishing_won - 1, 0) + fishing_say(FISHING_CHECKED) + return + } + if fishing_in_window() { + fishing_won += 1 + fishing_new_window() + if fishing_won >= fishing_need_n { fishing_land() } else { fishing_say(FISHING_GOOD) } + return + } + if FishSpecies[fishing_sp].legend and fishing_won > 0 { + fishing_won -= 1 + fishing_say(FISHING_TURNED) + return + } + fishing_lost(FISHING_LINE_SNAPPED) +} + +# the button: waiting it reels in, on a bite it strikes, on a fish it reels +export function fishing_press() -> void { + if fishing_st == FISHING_WAIT { + fishing_lost(FISHING_REELED_IN) + return + } + if fishing_st == FISHING_BITE_ON { + fishing_strike() + return + } + if fishing_st == FISHING_REEL { fishing_reel_press() } +}