feat(packages): ludic.fishing - cast, bite, the reel's fight and the landing, species as an open registry, the record in its own save section, facts for bait, catch and a lost fish

A line is cast at a point the FishingWorld port says is water (depth, or afloat), bait (asked,
then said as FISHING_BAIT_USED for the game to take) halves the wait, the bite picks a species -
a legend only on a lure, likelier in deep water and with skill, else an everyday one by weight -
and the fight is a marker, a green band that moves above Relaxed and closes on Hard or for a
fish whose data says so, a Gentle ring of one press, and a legend's runs and the line it takes
back. Length and weight come from FishSpecies data. The package never draws, speaks or touches
a pack; every step is a FishingFact (CAUGHT, BAIT_USED, LINE_SNAPPED, SLACK, MISSED, ...). Its
dice are its own Rng. A legend on Gentle fights as a legend (the old ring lost him at once).
Uses ludic.base only; thirteen tests over a fake lake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 06:53:14 +03:00
parent 12cfcb10ac
commit 80f3468ff8
11 changed files with 787 additions and 0 deletions

View file

@ -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_<KEY>` 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<FishingFact>` | `{ 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
```

View file

@ -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()
}

View file

@ -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"

View file

@ -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
}

View file

@ -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

View file

@ -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 }

View file

@ -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)
}

View file

@ -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<FishingFact> = 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<FishingFact> {
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
}

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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() }
}