A deterministic fishing engine for a language model, seeded with a famous constant
A deterministic text-based fishing mini-game for AI to play — single file, zero dependencies, let your AI companion go fishing.
At a glance
- What is it?
- ai-fishing-game is a single file, dependency free Python text game built to be played by an AI rather than by a person: state lives in a save file rather than in the conversation, every reply ends in one line of machine readable JSON, and identical seeds replay bit for bit. The readme is unusually honest about the one guarantee it cannot make, which is that the spoiler free build hides the game by encoding rather than encrypting it.
- Who is it for?
- ai-fishing-game is worth reading as an interface design exercise even if you never fish, because it solves the parts of writing a text game for a language model that are usually ignored: state that outlives the context window, replies shaped so the model does not have to ask twice, an input surface that cannot crash the host, and a replay guarantee. Two things to know before you point a model at it.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 88 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 12, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Determinism runs on a default seed that is a published constant
The central claim is reproducibility. Randomness comes from a mulberry32 generator, the generator's state is serialised into the save file, and the same seed with the same command sequence produces the same result down to the last bit. That makes a game reproducible for review, for testing, and for two people wanting to compare notes on the same run.
The save file is `fishing_save.json`, and it is written next to the script rather than anywhere in a user directory. Deleting it restarts the game. For a single player that is fine. For several, the readme's advice is to give each one their own working directory so each has their own save, which is a hint that the location is not designed for concurrent access.
The default seed is the detail worth pausing on: `0x9e3779b9`. That is not a random number, it is the well known golden ratio constant used as a default in hash functions and in the TEA cipher. So anyone who starts a game without passing a seed to `new_game()` starts from the same initial state as everyone else who did the same, and the first few casts of a fresh game are identical for all of them. Determinism is what makes this work, and the default is what makes the opening predictable.
The interface for choosing otherwise is small. `new_game(seed)` starts a fresh run, and `cmd("...")` does everything else. The whole public surface of the engine is those two calls.
The spoiler free build hides the game by encoding it, and says so
There are two files containing the same game. `engine.py` is readable source with the fish tables, the probabilities and the structure in the open. `fishing.py` is the same game with the engine packed into the file and only `cmd()` and `new_game()` exposed. A small script, `build_blind.py`, regenerates the second from the first, so you edit the readable one, run the build, and the hidden one keeps up.
The purpose is to stop a model from reading the fish list and the odds before it plays. Give the model the packed file and it can only cast, buy and move, discovering what exists one cast at a time.
And then the readme says the thing that makes this an interesting design rather than a trick: packing is encoding and not encryption, a model that has decided to look can unpack it with one line of code, and blind play fundamentally depends on the model cooperating. The suggested instruction to the model is simply to call `cmd()` and not to go decoding the packed data.
So the feature is a social contract enforced by the wrapper file rather than a security boundary, and the project documents it that way. What the arrangement does buy is real: a model that follows instructions plays blind, and a model that does not gets a shorter file to read than the full source would have given it.
Both files are usable as a library as well as a script, so an integrator can import either. They are the same game, and choosing one is choosing whether to trust the player.
Every reply ends in one line of JSON, and that is the whole interface contract
The design target is a language model on a token budget, and every part of the interface follows from that.
Bulk first. `cast` takes a count, so twenty casts come back as one summary instead of twenty round trips: new species, rare catches, events and season changes keep their full narration, while repeated common fish and empty casts collapse into a single tally line. The readme puts the saving at roughly two round trips instead of two per cast. A `stop=` argument ends the batch early on a condition, and it accepts a comma separated list, so a model can ask to stop on a new species, on anything rare, or on any event.
Then batching. Several different commands can be chained with a semicolon or a newline, up to eight at a time, executed in order, each with its own small heading in the output. The common pairs are named explicitly: buy bait then cast, change location then cast. A failing step affects only itself and does not stop the ones after it.
Then the status line. Every single return ends with one compact JSON object carrying points, location, season, turn, encyclopedia progress, remaining bait, and unsold catch count, with an extra field when a chest is waiting to be opened. The stated purpose is that the model can read that line instead of issuing a separate status call.
Together those three features remove the loop an AI player would otherwise be trapped in, where every action costs a request and every request costs context.
The command surface cannot raise, and a failed save says so in the text
The interface is one string in and one string out. `cmd("...")` takes free text and returns text, and the readme makes an explicit promise about the failure modes: any input, including nonsense, produces a line of instructional text rather than an exception and a stack trace. Save read and write problems, whether the file is corrupt or the directory is not writable, are reported inside the returned text instead of being swallowed.
That promise is the most important line in the whole document for anyone wiring this to a model. A text game exposed to an autonomous caller has to be unable to take the host down, and has to be honest when it has not done what it was asked. A save that silently fails is worse than a crash here, because the caller keeps playing against state that is not being written.
The whole surface fits in one script, and every call is a string in and a string out:
import engine # 或 import fishing(盲玩版,接口一样)
print(engine.cmd("help")) # 看规则
print(engine.cmd("status")) # 看当前状态
print(engine.cmd("cast")) # 抛一竿
print(engine.cmd("cast 10")) # 一次连钓 10 竿(只回一个汇总)
print(engine.cmd("cast 20 stop=rare")) # 连钓 20 竿,钓到稀有就停
print(engine.cmd("buy basic_worm 10; cast 10")) # 多条指令串一批、一次跑完
print(engine.new_game(2024)) # 用种子 2024 重开一局The command surface itself is small enough to read in a minute. There is `help`, `status`, `shop`, `buy` with an item id and a count, `cast` with an optional bait, count and stop conditions, `dive`, `choose` for a decision point, `surface`, `goto` with or without a location, `inventory`, `sell` in several forms including selling everything or selling a whole species, `open` for a chest, `encyclopedia`, and `look` by id or by the fish's Chinese name.
Two details in that list are worth noting. `goto` with no argument lists every location with its price and how many species remain unseen there this season, with legendaries listed separately, so the game can point at novelty without naming it. And `look` on a fish you have not caught shows question marks instead of a description, which is the same anti-spoiler discipline applied to the inspection command.
Two versions on two branches, and the light one has no diving
The readme documents two downloadable versions that are not two builds of the same thing.
The full version is on the default branch and includes a complete diving system: treasure map fragments unlocking dive sites, twenty two fish that exist only underwater with their own catch feel, dive atmosphere lines keyed by location and season, fourteen underwater encounters, twenty one treasure items, a seabed vault, and a purchasable oxygen bottle package.
The lightweight version lives on a separate branch. It contains only the token saving work, meaning batched commands, the compact status line, a count of undiscovered fish per spot and a streak hint, plus six lucky events, and it does not contain diving at all.
So the choice is not full versus light in one codebase. It is a branch, with a different feature set, and the readme on the full branch documents both. Anyone who picks the light version for token reasons opts out of the diving content and of whatever changes land on the default branch, and nothing in the interface tells the model which one it has.
The content scale of the full version is worth recording as numbers: fifty five surface fish across six rarity tiers from common to mythic, twenty two dive only fish, atmosphere lines per location and season, fourteen underwater encounters of which five are choice points at ruins, twenty one treasures, and eleven fishing locations. Four seasons advance with the number of casts rather than with a calendar, which means a run has a definite length. A new game starts with two hundred points and five ordinary worms, with two locations already unlocked.
Diving is gated behind fragments, and oxygen is the currency every choice spends
The late game is built around a resource you cannot buy until you have earned the right to buy it.
Dive sites are locked. For each location, fishing on the surface can turn up treasure map fragments, three to five of them depending on how deep the water is, which assemble automatically into a map and unlock the dive there. Fragments also come out of driftwood bottles and chests, and a lucky fragment can be a complete map that unlocks a site outright.
The oxygen gate follows from that. The shop does not stock oxygen bottles until the first dive site is unlocked, so the economy opens only once the player has done enough surface fishing. Bottles are discounted in volume, five for twenty percent off and ten for thirty percent off. A dive takes a number of bottles as a depth budget, and surfacing early is always allowed, with a settlement on return that accounts for the catch, the treasures, the new discoveries, the oxygen spent and the net.
Ruins are where the budget becomes a decision. A big ruin pauses the dive and offers choices, and each option spends a different amount of oxygen, so an option you cannot afford is an option you cannot take and all you can do is give up. Fourteen underwater encounters exist, five of them built around these pauses.
The surface game has its own rare moments, all of them probabilistic rather than scripted: a split hook landing three at once, a stone turning to gold at triple value, a run of doubled catches, a blessing that skips bait consumption for a few casts, a tide that produces a record fish, and a pearl from an oyster.
Three patches in two days, a push in July, and no tests in the tree
The release history is small and quick. v2.2.1, v2.2.2 and v2.2.3 all landed on 25 and 26 June 2026, and the names are scope statements: playtest polish with trimmed announcements and fixes, a redone golden lure with ruins appearing more often once the encyclopedia is complete, and ruins switched to independent dice rolls. Three patches in about two days on a project with no published test suite.
That is visible in the tree. The repository holds the readme, a changelog, a licence, `.gitignore`, two Python files, the build script, the tool schema and one example document. There is no `tests/` directory, no package manifest, no lockfile and no CI configuration in the entries listed. Python 3.8 or newer is the only stated requirement, and the project describes itself as single file with zero dependencies, which the two module files and the standard library bear out.
The last push is dated 2026-07-17, so the code has moved since the last release without a tag. The readme points at the changelog for release highlights and unreleased changes, which makes that file the only place where the difference between what is on disk and what a user can install is recorded.
The licence is another loose end: a `LICENSE` file sits at the root, the project listing records no recognised identifier for it, and the description does not name one. Everything else about the reuse terms is stated precisely, down to the batch limit and the stopping conditions, which makes the missing licence identification the one part of the documentation that does not tell you what you are allowed to do.
Editorial conclusion
ai-fishing-game is worth reading as an interface design exercise even if you never fish, because it solves the parts of writing a text game for a language model that are usually ignored: state that outlives the context window, replies shaped so the model does not have to ask twice, an input surface that cannot crash the host, and a replay guarantee. Two things to know before you point a model at it. The spoiler free build only hides the fish tables by encoding them, and the readme says a determined model can undo that in one line, so treat it as a social contract rather than a sandbox. And if you want the lightweight build, you are choosing a different branch with no diving content, not a flag on the same code. Set your own seed if you care about variety, because the default is a published constant that gives every new game the same opening.
Frequently asked questions
What does ai-fishing-game need to run?
Python 3.8 or newer and nothing else. There are no dependencies, no package manifest and no install step; you import `engine` for the readable build or `fishing` for the blind one and call `cmd()` with a command string.
Is ai-fishing-game reproducible across runs?
Yes. Randomness uses a mulberry32 generator whose state is serialised into `fishing_save.json`, so the same seed with the same command sequence replays identically. The default seed is the constant `0x9e3779b9`, so unseeded games start from the same state for everyone.
How does ai-fishing-game keep an AI player from seeing the fish list?
By giving it `fishing.py` rather than `engine.py`. The blind build packs the engine into the file and exposes only `cmd()` and `new_game()`. The readme notes the packing is encoding rather than encryption and that a model determined to look can undo it in one line, so the design relies on cooperation.
How does ai-fishing-game reduce token usage for an AI player?
Three ways: `cast` with a count returns one summary that keeps the interesting moments and tallies the rest, up to eight different commands can be chained with a semicolon into a single call, and every return ends with one compact JSON status line so the model does not need a separate status call.
What are the differences between the two ai-fishing-game versions?
The full version on the default branch adds the diving system with underwater only fish, map fragments, oxygen bottles and ruins. The lightweight version is on a separate branch and carries only the token saving work plus six lucky events, with no diving content at all.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tutusagi-ai-fishing-game)