Skip to content

Repository files navigation

PUTTSEED

CI Play in your browser

One mini-golf hole per day. Everyone on Earth plays the same course.

A putt being played: the aim line is drawn back across sand, a ramp, a bumper and water; the ball is released, rolls into the cup, and the hole closes on 'Par — well played!' with three stars

No install, no account. The WebGL build is not a demo version — it runs the same simulation the phone does, because a mouse drag and a finger drag reach the quantizer as the same two integers, and the core has no floats to disagree about. Progress lives in that browser only.

PUTTSEED is a daily-seed, bit-deterministic 2D mini-golf game for Android (Unity 6 + a pure C# core). Each UTC day derives a seed; the seed generates a course that is machine-verified solvable before any player sees it; and because the physics is fixed-point and fully deterministic, a finished run compresses to a ~30-character code that replays the exact same shots on any device — no backend, no accounts, no uploads.

PUTTSEED day 214 — 3 strokes (par 3)
⬜🟫⛳
🔥 12-day streak
Watch: PUTT-BGUAAAAAAAAAAiD8AwDg_gEA

One glyph per stroke — what the ball met on its way: a bank off a wall, sand, the cup. The code proves the run; the row is the part a stranger can read.

Main menu: today's hole finished in three, a countdown to the next one, and the modes — journey, gauntlet, practice, tutorial, archive — over a thumbnail of the day's course A daily hole mid-shot: the aim line drawn back from the ball on an ice patch, with bumpers, a portal, a ramp and water between it and the cup The card that closes the day: two stars, four strokes with a personal best of three, the streak, a countdown and the share button
Today's hole
one course, everyone
The elements
ice, bumpers, portals, water
The day closes
your first finish is the answer
Journey level select: a paged grid where every cell is etched with the shape of its own course, stars underneath, locked levels dimmed Collection: twelve ball skins in a grid, the equipped one ringed in its own colour, locked ones dark The archive as a month calendar: played days lit with their stars, today picked out in amber, days that have not happened yet left dead
Journey
100 curated seeds
Collection
earned, never sold
The archive
every past day regenerates

Game rules in one breath

Drag to aim (slingshot or direct — your pick), release to shoot; ball must rest before the next stroke. Walls bounce, bumpers boost, sand drags, ice slides, water costs a stroke and a reset; gates let you through one way only, ramps push, portals teleport, and windmill blades sweep whether or not you are ready. One day in eighteen adds a twist of its own — ice underfoot, springier bumpers, or a crosswind — proven solvable under the twist before it ships. Capture needs a slow ball over the cup — fast attempts rim out. A hole is worth two strokes or three, and which one is the generator's answer, proven before you see it. Stroke limit is par + 3; holing out scores stars — 3 at par or better, 2 one over, 1 within the limit. Retries are unlimited, but the day's answer is your first finish — a score reached on the thirty-fourth attempt is nobody else's score.

Five modes, one generator

  • Daily — the headline mode: one UTC hole, identical worldwide, with a local streak. Your best run rides along as a translucent ghost on the next attempt, and the archive is a month calendar you can page back through — every past day reopens, scored, because courses regenerate from the date alone and browsing history needs no storage. A random-day button picks an unplayed date for you.
  • Gauntlet — last week's seven dailies played as one round, strokes cumulative, one score. It ships no new content by design: the holes are dailies that already existed. A gauntlet is named by its week index, so everyone runs the same seven, and only fully elapsed weeks open — a week still in progress would hand one player courses another has not reached. A round shares as a PUTTWK- code.
  • Journey — a 100-level campaign of curated seeds, unlocked in order, three stars per level. Levels are literally just seeds, picked from generator scans on a ramp that climbs three axes at once: hazards, rated difficulty, and par. Par 3 is one level in five at the start and four in five at the end — weighted, never staged, so the two lengths stay interleaved the whole way. The solvability proof and the replay codec apply unchanged.
  • Practice — unlimited courses by difficulty (Easy / Normal / Hard), per-difficulty personal bests, an undo mulligan, and course invites: any course shares as a code a friend can play on their own device.
  • Tutorial — five fixed courses covering all nine elements: related ones share a lesson, so a pair is one sentence rather than two facts. The first launch walks straight into them. Each seed is held to its lesson by a test — the course must carry exactly the elements the hint names and nothing else, every element must be taught somewhere, and no lesson may be a themed day.

Meta — local by design

No backend, no accounts, no IAP. Everything below lives in the local save and travels between devices as a single PUTTSAVE- code (export/import in the stats panel):

  • thirteen achievements, plus a stats panel with a stroke-distribution histogram;
  • cosmetics on two axes — twelve ball skins (colour, plus a stripe and a dot pattern) and eight trails (tint, plus a comet and a bubble style) — gated by achievements and Journey progress. The Collection grid spells out each locked item's condition; nothing is for sale;
  • watchable replays: paste any PUTT- code to watch that run in-game;
  • EN/TR localization, and settings for sound, haptics, aim style, a colorblind palette and a 60/120 FPS battery mode;
  • the typeface is held to both languages by test: UiFontTests asserts the active face can print every character the UI can show, and two of the six candidates failed that audition — one with no right arrow, one with no Turkish ğ/İ/ş;
  • every sound is synthesized: the 16 WAV clips are generated by a committed editor tool (SfxSynth) — zero recorded audio in the repo.

Feel tuning lives in one ScriptableObject (Assets/PuttSeed/Resources/FeelConfig.asset). Out-of-scope ideas live in LATER.md — deliberately.

Architecture

+--------------------------------------------------------------+
|  Unity layer (repo root: Assets/)                            |
|  rendering · drag input · UI · audio/haptics · interpolation |
|                                                              |
|  InputQuantizer   -> drag becomes a 10-bit angle + 8-bit     |
|                      power index — THE quantization boundary |
|  FixView          -> Fix64 becomes float — render-only,      |
|                      one-way, never flows back               |
|  FixedStepper     -> frame time becomes whole 120 Hz ticks   |
+--------------------------------------------------------------+
|  core/src/PuttSeed.Core (netstandard2.1 — ZERO UnityEngine)  |
|                                                              |
|  FixedMath   Fix64 (Q32.32) · Vec2Fix · xorshift128 RNG ·    |
|              committed 1024-entry sine table                 |
|  Sim         GolfSim: 120 Hz fixed tick, circle-segment      |
|              walls with sub-stepping, bumpers, sand, ice,    |
|              water, gates, ramps, portals, windmills, hole   |
|              capture, rest, FNV-1a StateHash                 |
|  CourseGen   corridor growth -> hazard decoration ->         |
|              SolvabilityChecker (bounded BFS over the        |
|              quantized shot space) -> DifficultyRater;       |
|              versioned configs freeze published courses      |
|  Replay      [seed + timed shots] <-> PUTT- base64url codes  |
|  Daily       UTC date -> seed (FNV-1a + SplitMix64, salted)  |
+--------------------------------------------------------------+

The Unity project sits at the repo root; core sources are linked into Assets/PuttSeedCore/src via a directory junction under an asmdef with noEngineReferences: true, so the compiler itself enforces the layering. (Unity writes .meta files into core/src through that junction; they are committed for stable asset GUIDs and are inert to dotnet build and the purity check — the C# sources themselves stay UnityEngine-free.)

The determinism proof

The whole product depends on one property: the same seed and the same inputs produce the same bits on every device. That claim is enforced mechanically, not by care:

  • No floats exist in core. tools/check-purity.py scans core/src for float, double, System.Random, DateTime and UnityEngine and fails the build if any appear in code — comments and string literals are blanked first, because the check used to be a grep and a grep matched the comment explaining that floats are banned. It runs its own self-test in CI: a guard nobody has watched fail is not known to work. All math is Fix64 — Q32.32 fixed point on long, with 128-bit multiply intermediates and Newton square root. Trig is a committed 1024-entry table; angles only ever exist as table indices.
  • The 10k-tick golden hash (DeterminismTests): a fixture course holding every element — walls, bumpers, sand, ice, water, gates, ramps, portals and windmills — runs a scripted 16-shot, 10,000-tick session twice in-process, and the final FNV-1a state hash must equal a committed constant: 11426007175965104957. Any accidental change to sim math fails it. A companion test guards the guard: removing any one element must move the hash, so nothing can sit in the fixture without being met. That test found two elements the previous fixture never touched, water among them.
  • Golden replay fixtures (GoldenReplayTests): three seeds run end-to-end — generate, replay the author solution — against frozen final hashes and frozen PUTT- codes, on BOTH the generator the game ships (v4) and the frozen v1 one: a version byte that was ever emitted has to keep decoding forever.
  • The 1000-seed property suite (PropertySuiteTests): every seed generates within bounded attempts, every accepted course's author solution replays to a capture within par, and every replay code round-trips.
  • Quantization boundary tests (Unity EditMode): the drag-to-ShotInput mapping and the frame-time-to-ticks stepper are tested where analog meets discrete; floats stop existing at that line.
  • A replay that desyncs is a bug by definition — the codec stores only (seed, shots), plus the blade phase each shot was taken at once windmills started turning while the ball rests; playback is re-simulation, never recorded motion.

A course, as the generator prints it

       #                    seed        3
      ###                   par         2   difficulty Hard
     ##  #                  walls       10
    ##    #                 bumpers 2 · sand 2 · ice 2 · water 1
   #       #
  #   S    ##               S ball start
  #         ##              H hole
   #         ##             # wall      o bumper
   ##         #             : sand      * ice     ~ water
    ##        #
     ##   *oo~#
      #****oo~##
      ##***oo~~#
       #***~~~~#
       #*****  ##
       #**     :##
        #     :o:##
        #    :oo::#
        ##   :oo:::#
         ##   ::::::#####
          ##   :::: *** #
           #    ::  *** #
            #       *** #
             #      :H: #
             ##     ::: #
              ##    ::: #
               #    ::: #
                #########

No engine involved: that is the core library growing a course and a console app printing it. The same call the game makes.

Verify it yourself

None of the above has to be taken on trust — the suites that enforce it run from a clone:

What How
Core test suite (293 tests) dotnet test core
Purity check + core tests, as CI runs them scripts\test.bat
Unity EditMode tests (178 tests) scripts\unity-tests.bat
Print any course as ASCII dotnet run --project tools/CourseViewer -c Release -- 3 --stats

The core suite is the interesting one and it needs .NET SDK 8 and nothing else — no Unity, no device, no network. That is the layering being real rather than claimed. The EditMode tests want Unity 6000.3.x; open the repo root in Unity Hub, there is no unity/ subfolder.

Building the game, signing a release, refreshing the screenshots and deploying the demo are in docs/DEVELOPING.md.

The paper trail

Everything that shaped the build is committed, including the parts that argue with it:

Document What it holds
docs/GDD.md the design — rules, scoring, modes, and the explicit non-goals
docs/ARCHITECTURE.md the layering, and why the compiler is what enforces it
docs/AUDIT.md a studio-style audit of this game: scorecard, competitor teardown, costed proposals, and a cut list
docs/STATUS.md measured numbers — including the rows still honestly marked NOT YET MEASURED
docs/ROADMAP.md the phase plan the work followed
docs/DEVELOPING.md building, signing, capturing and deploying
docs/store/ Play listing copy in EN and TR, and the privacy policy
LATER.md the idea parking lot: nothing here is planned, which is the point

Workflow note

This repo was built with an AI-assisted workflow (Claude Code); the phase prompts that drove each week's work are committed verbatim in prompts/PROMPTS.md. The guardrails those prompts enforce — TDD for the core, the purity check, committed golden hashes — are exactly what makes an AI-assisted codebase auditable: correctness is proven by machine-checked tests, not by trust in the author, human or otherwise.

License

MIT.

About

One mini-golf hole per day, grown from the date and identical on every device. Fixed-point deterministic physics, courses machine-proved solvable before anyone sees them, runs as ~30-character replay codes. Unity 6 over a float-free C# core.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages