Public: anyone can read this; the crew can change it.
Credit, decided before it is coded: what a round pays for, and to whom #72
Check · Review · Land · Live done — Landed and live in production.
vesper/drift:vesper/38-credit-decided-before-it-is-coded-what-a → main 3 commits fork
4 files
changed +247
−19
AGENTS.md +1 −1
| @@ -41,7 +41,7 @@ needs a person checks `who.role`; ownership is a column (`author_id`) and | ||
| 41 | 41 | the check that reads it, in the action. |
| 42 | 42 | |
| 43 | 43 | The game itself is written down: `docs/wake.md` is the design record — the |
| 44 | mechanic, the starting numbers and the six invariants a build is measured | |
| 44 | mechanic, the starting numbers and the numbered invariants a build is measured | |
| 45 | 45 | against — and `src/concept.ts` is the part of it the app says out loud. Take |
| 46 | 46 | a number from there rather than spelling it again, cite an invariant by its |
| 47 | 47 | number, and when play says a number should move, move it in both. Physics |
docs/wake.md +137 −11
| @@ -241,6 +241,8 @@ only the first guess at holding them. | ||
| 241 | 241 | | unsteered seat | 20 s | `src/concept.ts` `UNSTEERED_SECONDS` | |
| 242 | 242 | | round | 180 s | `src/concept.ts` `ROUND_SECONDS` | |
| 243 | 243 | | intermission | 20 s | `src/concept.ts` `INTERMISSION_SECONDS` | |
| 244 | | point per unit of progress | 1 | **Credit**, below — #LADDER's constant | | |
| 245 | | credit tolerance, in points | 10 | invariant 8 — #LADDER's constant | | |
| 244 | 246 | | arena | 1000 units across | `src/world.ts` | |
| 245 | 247 | | tick | 20 Hz, one loop, server-authoritative | `src/round.ts` | |
| 246 | 248 | | frames out | 10 Hz | `src/round.ts` | |
| @@ -257,13 +259,129 @@ turned away; **A seat** above says who a seat belongs to and how it is let go. | ||
| 257 | 259 | |
| 258 | 260 | The split is deliberate. Physics tuning lives in one block at the top of `src/world.ts` (#5) and |
| 259 | 261 | moves whenever play says it should — this file names the *target*, not the constant. The six |
| 260 | numbers this file owns, and the two sentences quoted above — the rule, and what a spectator reads — | |
| 261 | are what a person meets on the page, so they live in `src/concept.ts` and the check holds them to | |
| 262 | this file. | |
| 262 | numbers this file owns, and the sentences quoted in it — the rule, what a spectator reads and what a | |
| 263 | round pays for — are what a person meets on the page, so they live in `src/concept.ts` and the check | |
| 264 | holds them to this file. The two credit numbers are neither: no door ever renders a rate or a | |
| 265 | tolerance, so they are decided here and #LADDER (#12) is where they become constants. | |
| 266 | ||
| 267 | ## Credit | |
| 268 | ||
| 269 | A round pays for one thing: the distance the bloom closed on the gate. That is the whole of it, and | |
| 270 | it is decided here rather than in the pull that implements it (#LADDER), because a rule invented | |
| 271 | under the pressure of making a test pass is the one that ends up paying for standing near the bloom. | |
| 272 | ||
| 273 | > A round pays for the distance the bloom closed on the gate, divided among the swimmers whose | |
| 274 | > current carried it: nothing is paid for being near the bloom, for thrusting at it, or for riding a | |
| 275 | > lane you did not build. | |
| 276 | ||
| 277 | **Progress.** The gate's mouth is the middle of its arc on the rim. A round's *gate-ward progress* | |
| 278 | is the bloom's distance to that mouth when the round starts, minus its distance when the round ends: | |
| 279 | one scalar, in units, read off the pure simulation. A bloom that ends where it began made no | |
| 280 | progress however far it wandered on the way, and a round that closed 200 units and never reached the | |
| 281 | rim still pays for 200 units. The gate ends a round; it is not what a round pays for. | |
| 282 | ||
| 283 | **The purse.** The purse is the gate-ward progress at one point per unit — about 500 points for a | |
| 284 | bloom carried clean across the arena — divided among the players by the ledger below, in whole | |
| 285 | points, so a share that rounds to nothing pays nothing. If the progress is not positive, the purse | |
| 286 | is empty and the round pays nobody at all. | |
| 287 | ||
| 288 | **The ledger.** Decay and spread are linear, so the sea is exactly the sum of what each player put | |
| 289 | into it: every cell carries a *share* per player, and the shares of a cell add up to the cell. | |
| 290 | Three rules keep that book. | |
| 291 | ||
| 292 | - **What you add is yours.** A mote deposits its momentum into the cell under it. The part of that | |
| 293 | deposit the cell's current already carried is not yours — you were a passenger — and it goes to | |
| 294 | the players whose shares make up that current, in their proportions. What is left, the momentum | |
| 295 | the water did not already have, is yours. Thrust into still water and the whole deposit is yours; | |
| 296 | drift with a lane at the lane's own speed and none of it is, and the people who built the lane are | |
| 297 | paid for carrying you. Braid a lane faster than it was and you are paid the difference. | |
| 298 | - **Only the bloom pays.** Drag is linear too, so the bloom's velocity is exactly the sum, over | |
| 299 | players, of the velocity that player's shares alone would have given it — call that their *pull*. | |
| 300 | A player's credit for a tick is the gate-ward part of their pull, times the tick. Nothing else is | |
| 301 | ever credited: not a deposit, not a second in the sea, not a distance to the bloom. The pull is | |
| 302 | read where the sea applies it — at the top of the tick, from the current the bloom actually felt, | |
| 303 | *before* `rim()` answers for where that leaves it. The rim is not a player: it neither pays nor | |
| 304 | collects, so the speed it deletes off a wall arrival is not charged back to the lane that | |
| 305 | delivered the bloom there, and the nudge it hands back is credited to nobody. Which side is a | |
| 306 | decision rather than a measurement: driven against the pure simulation it is worth well under a | |
| 307 | point on a whole round either way, so it is settled here before a test can settle it by accident. | |
| 308 | - **The bloom's own wake is nobody's.** The bloom writes momentum into the sea like any body, and | |
| 309 | that share belongs to no player and pays no one. The own-wake subtraction — "a body never feels | |
| 310 | its own wake", with a ledger to hand — comes out of that share first, and it is routinely larger | |
| 311 | than it: `src/world.ts` clamps the subtraction at the whole forward current, and for a mass-50 | |
| 312 | body in an ordinary lane that clamp is what binds on most ticks rather than the exception. So the | |
| 313 | rule has to say where the excess lands, and | |
| 314 | it is this: out of the bloom's own share first, then out of the players' shares in proportion to | |
| 315 | their forward parts. That is what keeps the sum intact — every unit of current the bloom feels | |
| 316 | belongs to somebody, and every unit taken off it is taken from somebody — and it is why a tick | |
| 317 | on which the clamp binds pays no forward credit at all: the bloom feels no forward current that | |
| 318 | tick, and the ground it covers anyway is inertia the sea handed it earlier. | |
| 319 | ||
| 320 | A pull that points away from the gate is negative credit, and a player who fought the bloom all | |
| 321 | round ends the round below zero. The purse is divided among the positive shares only and no round | |
| 322 | score is negative: the sea does not collect debts. | |
| 323 | ||
| 324 | **What it does not pay for**, said plainly because these are the three things a player will assume: | |
| 325 | ||
| 326 | - **Proximity.** Sitting on the bloom is worth nothing. Credit arrives through the cell the bloom is | |
| 327 | in, from the shares in it, and a body you are next to is not a share. | |
| 328 | - **Thrusting at the bloom.** Invariant 3 says one mote's thruster moves the bloom less than one | |
| 329 | cell in ten seconds, so a round spent shoving it directly pays what it achieves, which is nothing. | |
| 330 | - **Riding a lane somebody else built.** The passenger rule above: their current, their credit. | |
| 331 | ||
| 332 | ### The three ways a player will try to game it | |
| 333 | ||
| 334 | - **Build a lane and disconnect before the gate.** Paid in full for the lane, and nothing for the | |
| 335 | gate, because the gate pays nobody. Credit is written on the tick the current does the work, and a | |
| 336 | wake outlives the mote that left it — the sea keeps what a departing mote deposited — so those | |
| 337 | shares go on earning while they last. Leaving early costs only the seconds you were not | |
| 338 | depositing, and the score is held against the player, not against a mote in the roster. | |
| 339 | - **Two players trading deposits in a corner with no bloom near them.** They earn exactly zero. No | |
| 340 | deposit is ever paid for; the only thing that pays is the bloom's gate-ward progress, and momentum | |
| 341 | in a cell the bloom is not in moves the bloom by nothing. A corner far from the bloom is a corner | |
| 342 | whose shares are gone by the time the bloom could reach it. | |
| 343 | - **A client on the JSON door (#DOORS) steering at 20 Hz with no browser.** Credited by exactly the | |
| 344 | same rule, which is the answer rather than a dodge: it is paid for the bloom's displacement, | |
| 345 | invariant 3 says a lone thruster produces none of that, and steering faster than the tick changes | |
| 346 | nothing the sea can see. A bot is another current — legitimate here, and worth precisely the | |
| 347 | current it builds — and the door's ration bounds how often it may speak, not what it may earn. | |
| 348 | ||
| 349 | ### A guest's credit | |
| 350 | ||
| 351 | Credit is physics, not identity: a guest earns from the same purse by the same split as a player | |
| 352 | with a forge name, and the number is the same number. What signing in changes is whether it is kept | |
| 353 | — #LADDER shows forge names only, so a guest's score is the number on their own page when the round | |
| 354 | ends and never stands on the board. The sentence that says so to a guest is #LADDER's to word, next | |
| 355 | to the score it is about. | |
| 356 | ||
| 357 | ### The tolerance, and why it is ten points | |
| 358 | ||
| 359 | Attribution across a decaying field does not sum exactly, and "within a tolerance" is not a rule | |
| 360 | unless the tolerance is a number with a reason. Ten points is two percent of the most a round can | |
| 361 | ever pay — the bloom starts at the middle, the gate's mouth is on the rim and reaching it ends the | |
| 362 | round, so no round closes more than `ARENA_RADIUS`, the 500 points of a clean crossing — and it is a | |
| 363 | flat band rather than a percentage because two percent of any shorter round is smaller still, so a | |
| 364 | percentage clause could never be the one that binds, and because a percentage says nothing at all | |
| 365 | about the case this record deliberately allows, where the bloom ends further from the gate than it | |
| 366 | started. Its four sources are the field's cap and floor (`FIELD_MAX`, `FIELD_MIN`, which are not | |
| 367 | linear, so a saturated cell's shares no longer add up to the cell), the own-wake subtraction (a | |
| 368 | fitted curve clamped at the whole forward current — the largest of the four, and #24's to end rather | |
| 369 | than shrink), the gate-ward direction read once per tick against a path that curves inside the step, | |
| 370 | and the rim, which answers after the pull is read; none of them is a wrong rule, and ten points is | |
| 371 | about a third of one field cell at today's 32×32 grid — finer than the grain the sea is stored at, | |
| 372 | and so finer than any claim attribution can make about whose current a bloom was riding. | |
| 373 | ||
| 374 | Ten points is therefore no place for a wrong rule to hide: forgetting the passenger split, crediting | |
| 375 | gross deposits or dropping the drag term each move the sum by tens of percent — fifty points and up | |
| 376 | on a round that scores. An absolute band only bites where there is progress to be wrong about, so | |
| 377 | invariant 8 is proved on a round that moved the bloom; its other clause, that a round in which the | |
| 378 | bloom never moves pays nobody, is exact and carries no tolerance at all. If a build cannot hold ten | |
| 379 | points on a round that scores, the answer this record expects is #24 and an exact own-wake term, not | |
| 380 | a larger number here. | |
| 263 | 381 | |
| 264 | 382 | ## Invariants |
| 265 | 383 | |
| 266 | Every build issue is measured against these seven. They are numbered, and issues cite them by | |
| 384 | Every build issue is measured against these eight. They are numbered, and issues cite them by | |
| 267 | 385 | number; `INVARIANTS` in `src/concept.ts` is the same list as data, so a test can name the one it |
| 268 | 386 | proves. The list only ever grows at the end. |
| 269 | 387 | |
| @@ -280,6 +398,8 @@ proves. The list only ever grows at the end. | ||
| 280 | 398 | 7. One key, one seat: the number of live motes never exceeds the number of distinct player keys in |
| 281 | 399 | the roster, whatever a client opens — a second connection under a key steers that key's own mote, |
| 282 | 400 | and a request carrying no session key gets no mote at all. |
| 401 | 8. Credit over a round, signed and summed over every player, equals the bloom's gate-ward progress | |
| 402 | within 10 points; a round in which the bloom never moves pays nobody. | |
| 283 | 403 | |
| 284 | 404 | Where each is proven: 1–4 and 6 in `src/world.test.ts` against the pure simulation (#5), and 4 |
| 285 | 405 | again in `src/round.test.ts` once a real round runs it (#8). 5 is a property of `step` alone and is |
| @@ -287,7 +407,11 @@ also the seam #18 must not break when it seeds a round from a stored field. 6 is | ||
| 287 | 407 | difference rather than an absolute: the same lane twice, one arrival over the scatter speed and one |
| 288 | 408 | under, so a build where the rim is free — or where it pays, as the first one did — fails it. 7 is |
| 289 | 409 | `join`'s to prove in `src/round.test.ts` (#8) — the same key joining twice is one mote, a join with |
| 290 | no key is none — and the upgrade's to hold in `src/socket.test.ts` (#9). | |
| 410 | no key is none — and the upgrade's to hold in `src/socket.test.ts` (#9). 8 is #LADDER's to prove | |
| 411 | (#12), because there is no ledger in the repository to sum yet: it is proved against the pure | |
| 412 | simulation — a round driven by hand, the ledger summed, the bloom's two distances to the gate's | |
| 413 | mouth subtracted — and it needs no clock, no database and no socket to hold, which is why it is | |
| 414 | stated here in full and its arithmetic is written nowhere in this pull. | |
| 291 | 415 | |
| 292 | 416 | ### Two clarifications the build turned up |
| 293 | 417 | |
| @@ -304,10 +428,12 @@ no key is none — and the upgrade's to hold in `src/socket.test.ts` (#9). | ||
| 304 | 428 | |
| 305 | 429 | ## What this record does not decide |
| 306 | 430 | |
| 307 | The wire format, the scoring, the words on the page and the shape of the socket are all the build | |
| 308 | issues'. This file decides the mechanic, the seven invariants, what a seat is, what the rim costs, how | |
| 309 | the sea reads (**The look**) and the numbers in the table it owns. It says what a seat is; it does not | |
| 310 | write `join` (#8) or the upgrade (#9), and how a spectator's page differs from a swimmer's is #10's and | |
| 311 | #11's. **The look** says what every door's marks mean; it draws nothing, budgets no frame and names no | |
| 312 | pixel a door must spend. It stays open while the map is built against it and closes when the | |
| 431 | The wire format, the words on the page and the shape of the socket are all the build issues'. | |
| 432 | Scoring is decided above, but only as far as the rule: how the board reads, how many days it spans, | |
| 433 | where the points are stored and how the arithmetic is carried are #LADDER's. This file decides the | |
| 434 | mechanic, the eight invariants, what a seat is, what the rim costs, what a round's credit pays for, | |
| 435 | how the sea reads (**The look**) and the numbers in the table it owns. It does not write `join` (#8) | |
| 436 | or the upgrade (#9), and how a spectator's page differs from a swimmer's is #10's and #11's. **The | |
| 437 | look** says what every door's marks mean; it draws nothing, budgets no frame and names no pixel a | |
| 438 | door must spend. It stays open while the map is built against it and closes when the | |
| 313 | 439 | vertical slice is live (#10, #11) and these numbers have been tuned against real play. |
src/concept.test.ts +89 −4
| @@ -1,6 +1,10 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import * as concept from "./concept.ts"; |
| 3 | 3 | import { home } from "./page.ts"; |
| 4 | // The record argues the credit tolerance from two of the world's numbers — | |
| 5 | // the arena a round is played in, and the grain the sea is stored at — so the | |
| 6 | // check reads them from the world rather than restating them. | |
| 7 | import { ARENA_RADIUS, FIELD_CELL } from "./world.ts"; | |
| 4 | 8 | import type { Who } from "./who.ts"; |
| 5 | 9 | |
| 6 | 10 | const record = await Bun.file(new URL("../docs/wake.md", import.meta.url)) |
| @@ -122,10 +126,10 @@ test("the invariants read the same in the record and in the code, in order", () | ||
| 122 | 126 | .split(/\n(?=\d+\. )/) |
| 123 | 127 | .filter((entry) => /^\d+\. /.test(entry.trim())) |
| 124 | 128 | .map((entry) => tidy(entry.trim().replace(/^\d+\.\s*/, ""))); |
| 125 | expect(listed.length).toBe(7); | |
| 129 | expect(listed.length).toBe(8); | |
| 126 | 130 | expect(listed).toEqual(concept.INVARIANTS.map(tidy)); |
| 127 | 131 | // The list only grows at the end: an index cited by a test today has to |
| 128 | // mean the same promise tomorrow, so 1–6 are pinned by their opening words. | |
| 132 | // mean the same promise tomorrow, so 1–7 are pinned by their opening words. | |
| 129 | 133 | const opens = [ |
| 130 | 134 | "A lone mote", |
| 131 | 135 | "A mote dropped", |
| @@ -133,6 +137,7 @@ test("the invariants read the same in the record and in the code, in order", () | ||
| 133 | 137 | "Three motes braiding", |
| 134 | 138 | "With no input", |
| 135 | 139 | "The rim costs", |
| 140 | "One key, one seat", | |
| 136 | 141 | ]; |
| 137 | 142 | opens.forEach((open, i) => expect(concept.INVARIANTS[i]).toStartWith(open)); |
| 138 | 143 | }); |
| @@ -168,15 +173,95 @@ test("the seat's seconds are ordered the way the record argues them", () => { | ||
| 168 | 173 | ); |
| 169 | 174 | }); |
| 170 | 175 | |
| 171 | test("invariant 7 is the seat rule, and it is last", () => { | |
| 176 | test("invariant 7 is the seat rule, whatever is appended after it", () => { | |
| 172 | 177 | const seven = concept.INVARIANTS[6]; |
| 173 | 178 | expect(seven).toBeDefined(); |
| 174 | expect(concept.INVARIANTS.length).toBe(7); | |
| 179 | expect(concept.INVARIANTS.length).toBe(8); | |
| 175 | 180 | expect(seven).toStartWith("One key, one seat"); |
| 176 | 181 | // Worded so #ROUND's test can measure it rather than agree with it. |
| 177 | 182 | expect(seven).toContain("never exceeds the number of distinct player keys"); |
| 178 | 183 | }); |
| 179 | 184 | |
| 185 | /** A row of the numbers table, by the name in its first cell. */ | |
| 186 | const number = (name: string): number => { | |
| 187 | const row = rows().find((cells) => cells[0] === name); | |
| 188 | expect(row, `docs/wake.md has no "${name}" row`).toBeDefined(); | |
| 189 | return Number((row as string[])[1]); | |
| 190 | }; | |
| 191 | ||
| 192 | test("invariant 8 states the tolerance the record's own table names", () => { | |
| 193 | // The rule is one number in two voices — a sentence a person reads, and the | |
| 194 | // row #LADDER (#12) turns into a constant. If either moves alone the | |
| 195 | // invariant stops being provable, so the words have to quote the row. | |
| 196 | const tolerance = number("credit tolerance, in points"); | |
| 197 | const eighth = concept.INVARIANTS[7] as string; | |
| 198 | expect(eighth).toContain(`${tolerance} points`); | |
| 199 | // A round that pays nobody is the half of it no tolerance can express. | |
| 200 | expect(eighth).toContain("pays nobody"); | |
| 201 | expect(eighth).toContain("gate-ward progress"); | |
| 202 | expect(Number.isInteger(tolerance)).toBe(true); | |
| 203 | expect(tolerance).toBeGreaterThan(0); | |
| 204 | // And it is a flat band rather than a percentage, because a percentage | |
| 205 | // clause could never be the one that binds: the bloom starts at the middle, | |
| 206 | // the gate's mouth is on the rim and arriving there ends the round, so no | |
| 207 | // round can pay more than the arena's radius — and two percent of that is | |
| 208 | // the flat band itself. | |
| 209 | expect(eighth).not.toContain("%"); | |
| 210 | const mostARoundCanPay = ARENA_RADIUS * number("point per unit of progress"); | |
| 211 | expect(0.02 * mostARoundCanPay).toBeLessThanOrEqual(tolerance); | |
| 212 | // A third of one field cell: a ledger cannot locate a current inside the | |
| 213 | // cell that holds it, so the band is below the resolution of any claim it | |
| 214 | // could make, and it stays small against the biggest purse there is. | |
| 215 | expect(tolerance).toBeLessThanOrEqual(FIELD_CELL / 3); | |
| 216 | expect(tolerance * 10).toBeLessThan(mostARoundCanPay); | |
| 217 | }); | |
| 218 | ||
| 219 | test("the record decides which side of the rim a pull is read on", () => { | |
| 220 | // The rim deletes velocity the sea has just applied, and with a 60° gate | |
| 221 | // most arrivals are wall arrivals, so "before or after" is a decision | |
| 222 | // #LADDER must not have to make under test pressure. The Credit section | |
| 223 | // makes it, and names the rim among the things that make the ledger miss. | |
| 224 | const ledger = tidy(section("## Credit")); | |
| 225 | expect(ledger).toContain("before rim()"); | |
| 226 | expect(ledger).toContain("The rim is not a player"); | |
| 227 | const why = tidy(section("### The tolerance, and why it is ten points")); | |
| 228 | expect(why, "the residual list does not name the rim").toContain("the rim, which answers after"); | |
| 229 | // And the own-wake term is described the way the code has it: clamped at | |
| 230 | // the whole forward current, with the excess apportioned, not swallowed. | |
| 231 | expect(ledger).toContain("in proportion to"); | |
| 232 | }); | |
| 233 | ||
| 234 | test("the credit rule is the record's, word for word", () => { | |
| 235 | // The sentence is quoted under the heading that decides it, so a heading | |
| 236 | // that moves is a failure rather than a sentence that drifted somewhere else. | |
| 237 | expect( | |
| 238 | tidy(section("## Credit")), | |
| 239 | 'docs/wake.md "## Credit" does not quote CREDIT_RULE', | |
| 240 | ).toContain(tidy(concept.CREDIT_RULE)); | |
| 241 | // What it refuses to pay for is the half a player will assume wrong. | |
| 242 | for (const refusal of ["being near the bloom", "thrusting at it", "did not build"]) { | |
| 243 | expect(tidy(concept.CREDIT_RULE)).toContain(refusal); | |
| 244 | } | |
| 245 | }); | |
| 246 | ||
| 247 | test("this module says what a player reads about credit and computes none of it", () => { | |
| 248 | // #LADDER (#12) owns the arithmetic and cites invariant 8 by number. No | |
| 249 | // door renders a rate or a tolerance, so a credit constant appearing here | |
| 250 | // means the maths leaked into the part of the record the app speaks aloud. | |
| 251 | const exported = concept as unknown as Record<string, unknown>; | |
| 252 | for (const [name, value] of Object.entries(exported)) { | |
| 253 | if (!/CREDIT/.test(name)) continue; | |
| 254 | expect(typeof value, `${name} is a number this module has no door for`).toBe("string"); | |
| 255 | } | |
| 256 | }); | |
| 257 | ||
| 258 | test("the record answers the three ways credit gets gamed", () => { | |
| 259 | const gaming = tidy(section("### The three ways a player will try to game it")); | |
| 260 | for (const attack of ["disconnect", "corner", "20 Hz"]) { | |
| 261 | expect(gaming, `the record does not answer "${attack}"`).toContain(attack); | |
| 262 | } | |
| 263 | }); | |
| 264 | ||
| 180 | 265 | test("the record decides how the sea reads before three doors draw it", () => { |
| 181 | 266 | const look = tidy(section("## The look")); |
| 182 | 267 | // One paragraph per element, and the order marks are laid down, so a |
src/concept.ts +20 −3
| @@ -8,15 +8,31 @@ | ||
| 8 | 8 | * Physics tuning is not here. Thrust, field half-life, spread, drag, the |
| 9 | 9 | * arena and the gate are the world's, in one block at the top of |
| 10 | 10 | * `src/world.ts` (#WORLD), and they move whenever play says they should. What |
| 11 | * lives here is the shape of a round, who may have a seat in it and the words | |
| 12 | * for both — the parts a person reads — plus the invariants, numbered, so a | |
| 13 | * test can name the one it proves. | |
| 11 | * lives here is the shape of a round, who may have a seat in it, what it pays | |
| 12 | * for and the words for all three — the parts a person reads — plus the | |
| 13 | * invariants, numbered, so a test can name the one it proves. | |
| 14 | 14 | */ |
| 15 | 15 | |
| 16 | 16 | /** The one sentence a newcomer has to read: the whole game is other people. */ |
| 17 | 17 | export const RULE = |
| 18 | 18 | "Your own thruster is feeble; the wake other swimmers leave is not. Ride their lane, braid it, and throw the bloom through the gate."; |
| 19 | 19 | |
| 20 | /** | |
| 21 | * What a round pays for, in the sentence a player reads. The rule behind it | |
| 22 | * is the Credit section of `docs/wake.md`, decided there before #LADDER | |
| 23 | * computes it: the purse is the bloom's gate-ward progress, and it is split | |
| 24 | * by whose current carried the bloom, not by who was standing next to it. | |
| 25 | * | |
| 26 | * No door says this one yet. `RULE` and `SPECTATOR` are on the page because | |
| 27 | * the page has somewhere to put them; this sentence belongs beside a score, | |
| 28 | * so #LADDER (#12) puts it on the page and the terminal when there is a score | |
| 29 | * to put it beside. Until then it lives here, once, so two doors cannot word | |
| 30 | * it differently later. The purse, the rate and the tolerance are that issue's | |
| 31 | * arithmetic and not the app's speech: they stay in the record. | |
| 32 | */ | |
| 33 | export const CREDIT_RULE = | |
| 34 | "A round pays for the distance the bloom closed on the gate, divided among the swimmers whose current carried it: nothing is paid for being near the bloom, for thrusting at it, or for riding a lane you did not build."; | |
| 35 | ||
| 20 | 36 | /** Swimmers in one round. An arrival over the cap watches as a spectator. */ |
| 21 | 37 | export const ROSTER_MAX = 24; |
| 22 | 38 | |
| @@ -82,4 +98,5 @@ export const INVARIANTS = [ | ||
| 82 | 98 | "With no input, total field magnitude strictly decreases every tick and reaches zero: no energy from nowhere.", |
| 83 | 99 | "The rim costs seconds and ground, never a round: a mote that arrives above the scatter speed comes away slower than it arrived, loses 3 s of thrust and a cell of ground against the same mote in the same lane that arrived under it, and steers again the moment those seconds are up — one arrival is one scatter, however hard the lane holds it there.", |
| 84 | 100 | "One key, one seat: the number of live motes never exceeds the number of distinct player keys in the roster, whatever a client opens — a second connection under a key steers that key's own mote, and a request carrying no session key gets no mote at all.", |
| 101 | "Credit over a round, signed and summed over every player, equals the bloom's gate-ward progress within 10 points; a round in which the bloom never moves pays nobody.", | |
| 85 | 102 | ] as const; |