Appearance
Filling a new instance
A server nobody has played on shows an empty ladder, an empty match list, and a dice audit with nothing in it. Three of the features that make the project worth looking at are invisible until somebody plays, and you cannot judge a leaderboard layout against zero rows.
bin/seed.js plays the matches.
bash
npm run seed # 30 players, 10 matches each, against localhost
npm run seed -- --players 12 --matches 4What it actually does
Nothing is inserted into the database behind the server's back. Each roster entry connects over the same WebSocket protocol the browser speaks: it proves ownership of an Ed25519 key, publishes a reveal-chain tip, signs h0, feeds a fresh link into every game and every roll, counter-signs every event hash, and plays.
So the rows that appear are rows that were earned:
- the ladder moves because Glicko-2 processed real results
- the match list points at records that verify —
npm run verify <file>passes on every one of them - the dice audit is a histogram of dice that were really rolled, derived from committed secrets like any other
The cost of that honesty is time: a match takes a few seconds, not milliseconds, because both sides are evaluated and paced.
Who they are
The names are generated, and a different set comes out of every run: plain first names, names with a number stuck on, and two ordinary words run together — Pavel, linnea37, amber_willow, greyriver, tavla10. Nothing is numbered bot-07, because a column of bot-07 tells you nothing about whether the column looks right, and because a fixed cast of thirty would be recognisable as a fixed cast of thirty.
Names are unique within a roster, at most thirteen characters so nothing is truncated, and once generated they are kept (see the keys file below) so a returning player stays the same player.
Strength
Difficulty on the site has three tiers. A roster of thirty has thirty, interpolated along the same axis: how deep it looks, how often it deliberately throws a move away, how far down the ranked list it reaches when it does, and how reckless it is with the cube.
| Roster position | Looks ahead | Throws a move away | Cube |
|---|---|---|---|
top (skill 0.97) | one ply | ~0.3% of moves | sane |
| middle (0.5) | one ply | ~21% | loose |
| bottom (0.07) | not at all | ~56% | loose |
Strength is spread evenly across the roster whatever its size, and a player who already exists keeps the strength their rating was earned with — newcomers take the rungs of the ladder nobody is standing on.
That spread is the point. A ladder in which everyone is equally good sorts itself by luck, and then nothing about the rating system can be judged from looking at it.
The draw
A circle-method round robin: ten rounds of fifteen simultaneous matches, in which every player plays exactly ten matches and nobody meets the same opponent twice. Asking for more rounds than there are opponents is an error rather than a repeated pairing — Glicko learns far more from ten different opponents than from the same one ten times. Match lengths vary by round (1, 3, 5 and 7 points), and the clock is casual, so nothing flags.
test/seed.js checks those properties directly, because "thirty players, ten matches each" is the kind of claim that stays true in the output while being false in the data.
The keys file
Each player's name, strength and key are kept in seed-players.json (--keys to move it). A second run therefore adds matches to the same players and their ratings keep converging, instead of thirty new provisional 1500s appearing beside the old ones. Asking for more players than the file holds keeps everyone in it and generates only the shortfall.
That file is a secret
It holds the private key of every seeded player. It is git-ignored and written 0600. Whoever holds it can play, and sign, as those players.
Running it against something that is not localhost
It refuses without --yes:
bash
node bin/seed.js --url wss://example.com --yesBe deliberate about this. The matches are rated and published: the players appear on the public ladder beside real ones, their records are downloadable forever, and their dice are folded into the fairness statistics. Nothing marks a seeded player as a program, and the names are designed to look ordinary, so a visitor cannot tell. On a live instance that is a decision about what you are willing to tell your visitors, which is why it is not a default.
Deleting them afterwards is not clean either: the rating rows can go, but the signed records cannot be unpublished, and the dice audit has already counted their rolls.
Options
| Flag | Default | |
|---|---|---|
--url | ws://localhost:8080 | where to play |
--players | 30 | even, 2 to 60 |
--matches | 10 | per player; at most players - 1 |
--concurrency | 4 | matches in flight at once |
--think | 70 | ms between actions — see below |
--clock | casual | standard, relaxed or casual |
--keys | ./seed-players.json | the roster's identities |
--timeout | 240 | seconds before a match is given up on |
--yes | — | required for a non-local URL |
--think is not decoration. Every event is counter-signed by both players, so a client that answers instantly sends more than forty messages a second and the server closes it for flooding — correctly. The pause keeps generated matches inside the limits real ones live inside, and it is why --concurrency buys throughput: the matches are waiting, not computing.
Failed pairings are retried once at the end of the run, after a pause long enough for the server to let go of any match an interrupted run left open (90 seconds, then it forfeits).