Skip to content

About

Fall Guys solo tournament scoring and leaderboard for the FOM LAN party

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

391 Commits

Folders and files

Repository files navigation

FOM Fall Guys Tournament

Next up: docs/handoff-capture-perf.md — the capture pipeline stalls the admin server; three changes, measured.

Admin: I-36

🥇 1st: NH-D15 G2 🥈 2nd: NH-U12A chromax.black 🥉 3rd: NH-D12L --> Rules: we need two tabs "rules" & prizes on that page --> Prizes tab: add the podium like on the dashboard but instead of showing a bean: show a picture of the prize

Saturday from 14:00 -19:00 --> when it's not yet saturday 14:00, can you do a countdown till the game start on the dashboard? --> also add text there: join the discord https://discord.gg/J8msxSh6v and add the Fall Guys role

TODO:

  • Check that WITH sound actually works
  • How does the "now playing" work? on dashboard vs on Results?
  • Test run!

Also?

  • Cut the show mp4 per round
  • Cut stuff and put them on the website (or youtube?) "HIGHLIGHTS" :D

Scoring and leaderboard for the Fall Guys solo tournament at the FOM LAN party.

Participant list: https://www.fom.be/compos/view/507

Rules handed to the FOM board: docs/rules.md

Tournament day

  • Check server is Europe
  • Your crown rank on the website? Send me a screenshot!
  • Your FOM Name on the website? Send me a PM!

I am: AnotherAccount58

Fall Guys is on a different monitor at every venue, and recording the wrong one is silent until the shows are over.

  1. Set event.date in data/event.json to the day being played.
  2. Start the game and put it on the screen it will stay on.
  3. Record ten seconds of each monitor until one of them is the game:
    CAPTURE_OUTPUT=0 bun run dev
    The admin's Info tab names the folder. Play back the newest seg-*.mkv under segments/<newest>/.
  4. Put the one that worked in .env, which every bun run reads:
    echo CAPTURE_OUTPUT=1 >> .env
  5. bun run live, and confirm the header badge says recording.
  6. Play one round and confirm a frame with a trophy pill turns up in the capture panel.

Scoring

Achievement Points
Finishing a round first 3
Qualifying for a show's final round 1
Winning the final 5

Simultaneous winners split the 5, rounded down.

Running the admin

bun run live               # every save is committed and pushed
bun run dev                # saves stay on this machine
bun run dev --no-record    # and leave the screen alone — see below

Both serve the admin on http://localhost:3000/admin and the board beside it.

bun run live is the event itself: recording a show writes data/, commits it under data: record show N — Name and pushes, which rebuilds the published site. bun run dev writes the same files and stops there.

Nothing is pushed unless data/event.json and data/players.json parse and hold the shape the board reads. A file that fails is still saved, so no typing is lost, but the publish is refused with the field named, and the admin carries a banner until it is fixed. The Commit & push button runs the same check under a message you write.

Running the event

bun run cli show      # start a show
bun run cli round     # record a race or survival round
bun run cli final     # close the show: final map, finalists, winners
bun run cli penalty   # deduct points
bun run cli shows 21  # which shows work at this headcount
bun run cli board     # print current standings

Each command commits data/event.json, and pushes when a remote is configured. Add --no-commit for a dry run.

Players

data/players.json maps in-game names to FOM names. The in-game name is what the board calls a player; fom is optional and shown under it, for the players who have one.

{ "ingame": "OptiBean", "fom": "Optinux_Prime", "discord": "optinux" }

A player without an ingame shows on the leaderboard under their FOM name, on zero, and cannot be scored.

crownRank is the crown level the game shows beside their in-game name, typed in by hand in the admin's Players tab:

{ "ingame": "GrumpyTrout294", "fom": "Grumpy_Trout", "crownRank": 45 }

The admin runs the event instead of competing, and is marked so they stay off the leaderboard:

{ "fom": "Wouter_Van_Schandevijl", "admin": true }

Scoring is entered by hand

The game's Player.log records every player's result, but against numeric playerIDs that carry no name and change from show to show. Log parsing was tried and dropped; see data/logs/2026-09-01-join-order.md. Results go in through the CLI.

Show limits

data/shows.json holds the minimum and maximum player count for every custom playlist, taken from the Fall Guys wiki. A show the headcount cannot support is skipped. Refresh it if the game changes the numbers.

Crown levels and level clips

data/sangu.json is read off Fall Guys @ Sangu: the crowns each crown level costs, which levels have a clip page there, and which have an icon. A crown badge carries the first as a tooltip; a round on the results page draws its map with the icon and links the name out to the clips. Refresh it when a season adds levels or moves the crown track:

bun run sangu

The icons themselves land in site/img/levels/ and are committed, so the board still draws on a LAN with no way out; bun run sangu only downloads the ones missing. Every map played has an icon, but a quarter have no clip page yet and those go unlinked.

Admin UI

bun run build
bun run dev     # http://localhost:3000/admin
PORT=3100 bun run dev

The admin page runs only against the local dev server, never on GitHub Pages. It reads the Fall Guys log to fill in each show's rounds, player counts and finals, so the only typing is names. It writes data/players.json and data/event.json directly.

Names already entered are offered as a dropdown on every name field, so a player is typed out once and picked from the list after that.

Commit & push commits data/ and pushes, which rebuilds the public site. Only data/ is committed, so anything else staged is left alone.

Custom lobby code is published beside it and shown as a badge on every page of the board. It is filled in from what is already published, so clearing the box is what takes the badge down.

× takes a misfired show back off the leaderboard, after a confirmation. Only the last recorded show has it: a show's slot is its number in the log, and pulling an earlier one out would re-point every show after it at the wrong log entry. The show's JSON is written to deleted-show.json in that show's own capture folder first — numbered on collision, so nothing is ever overwritten. Restoring is pasting it back into data/event.json.

It finds Player.log under AppData/LocalLow/Mediatonic/FallGuys_client for any user on the C: drive. Set FALLGUYS_LOG to override.

Shows must be recorded in the order they were played: only the next unrecorded show is editable, so its number matches the log.

Player IDs in the log are reassigned every show — even inside one lobby — so the log can never say who did something. See data/logs/2026-09-01-join-order.md.

The site

bun run build   # bundle site/ and data/ into dist/
bun run dev     # serve dist/ on http://localhost:3000, admin on /admin

Five pages, all built from data/ and docs/rules.md:

Page What it shows
Dashboard Which show and round is on, who is left in it, the podium, and the rest of the field
Standings Every player with their races, finals, wins and penalties
Results Every show round by round, newest first, with the field coloured
Rules docs/rules.md
Show order The ten shows plus the if-time-allows replays, marked played / playing now / upcoming

Every show carries its field: gold won it, green got through the last board read, grey is still in the round on screen, red is out with the round number that did it. The field is the roster — everyone in players.json who is not an admin and has an ingame name — because a player knocked out in round 1 is named on no screen at all.

Clicking any of those badges — or anywhere on a standings row — opens that player's whole tournament: one row per show, one cell per round.

Mark Means
⚡ crossed the line first
✓ named on that round's qualification board
✗ still in going into the round, absent from its board
👑 won the show
? nobody read a board for that round, so it says nothing either way
· the show has no such round, or they were already out

A show reads Winner, Finalist or Contestant. Only the last show can read Still in: an earlier show missing its winners was typed in short, not left unfinished.

Every page but the rules refreshes itself every 15 seconds, so nobody has to reload during the event. Players who gained points since the last refresh are ringed in green for a few seconds.

Run from bun run dev, the status ribbon comes from the Fall Guys log over /live.json, so it names the show and the round on screen before a single result has been typed in. Only the machine running the game can serve that file; on GitHub Pages it is absent and the ribbon falls back to event.json — the last show recorded, and the round after the last one entered.

So the board projected at the LAN should be the local one on http://localhost:3000. The published site is for everyone else, and is as current as the last push.

Pushing to main publishes dist/ to GitHub Pages via .github/workflows/pages.yml. Set the Pages source to "GitHub Actions" in the repository settings.

Screenshots

Shooting by hand is the backup now — see Capturing the screens automatically. It still works exactly as described here, and a hand-shot capture and a frame cut from the recording are treated identically.

ShareX captures the active window with Alt + Print Screen. Rebind under Hotkey settings → ... → Task = Capture → Active window.

Set Fall Guys to Borderless (Settings → Display → Display Mode, or Alt + Enter). Exclusive fullscreen captures as a black frame.

Shoot the race finish, the finalists and the winner screen. A final with several winners has no winner screen; shoot the surviving beans instead.

The admin shows them in a panel down the right. Click a round, the finalists block or the winners block and the panel fills with the captures taken during it, so the names can be read off the screen instead of remembered. Click a capture for full resolution.

It reads Documents/ShareX/Screenshots for any user on the C: drive, and inside it only the month folder the event falls in, taken from date in event.json. Set SHAREX_DIR to override. Nothing is copied and nothing is committed — the folder is only ever read.

A capture is placed by its modified time against the log's round times, so a file copied into the folder lands wherever its new mtime falls. Anything that matches no round shows under This show, between rounds or Outside every show at the bottom of the panel.

The log stamps every line with a UTC clock and no date, so the day comes from date in event.json and every time on the admin page is converted to Europe/Brussels. A log started the day before the event would shift every window.

Reading names off the captures

The admin reads the captures and fills the names it finds into fields that are still blank. Three screens are read:

Screen Fills Read from
Qualification board rounds[n].qualified the name over every green card
Winner screen winners the nameplate under the bean
Qualified toast rounds[n].first the pill wearing the gold trophy

The trophy is what marks first place, not the pill's position — the column does not run in finish order.

The board comes up after every round, and every one of them is read: the survivors go onto the round they were read after. The finalists are the survivors of the round before the final, so they are not stored separately.

Names are matched against the ingame names in data/players.json. Everyone playing the tournament is registered, so the roster is the answer key rather than a spelling aid: each name read is given the one roster entry it is closest to, and a name already used on that board is not offered again. Where two entries are equally close the field is left as read rather than guessed at — so two players whose in-game names differ by a single character cannot be told apart, which is worth a glance over players.json once everyone has reported.

A name the roster does not hold goes in as it was read. That is what happens when testing outside the tournament, where players are not registered.

Only blank fields are filled, and a filled field is ringed in green with the capture it came from on hover. Type over one and it is yours — nothing later overwrites it, and clearing it on purpose does not bring the name back.

The answer key

data/ingame-names.txt is every in-game name the tournament has seen, one per line. It is what the reading is scored against, and it only ever grows.

bun run scripts/collect-names.ts              # from event.json, players.json and the fixtures
pbpaste | bun run scripts/collect-names.ts    # and whatever you read off a screen by eye

event.json cannot tell a name somebody typed from a reading nobody corrected, so a name that reaches no roster entry goes in exactly as it was read. Run an eye over the file after an evening of testing outside the tournament.

Scoring the reading

bun run scripts/ocr-score.ts

Reads the boards fixtures/manifest.json gives names — read by eye, in board order — and reports how many names come back exactly, the character error rate, and how many reach the right roster entry against two rosters. That last pair is what to move:

Roster What it stands for
board The board's own names — a lobby where exactly those players were registered
everyone data/ingame-names.txt — the size of pool the event really hands it

Each board is committed next to the names it is scored against, so the numbers are the same on every machine. To name a new one, put the capture under fixtures/ and let the reader do the typing:

bun run scripts/ocr-score.ts --dump auto-7-finalists-005302-2.jpg

It prints what the reader saw, in board order, ready to correct and paste into manifest.json. It looks under fixtures/, the auto-capture folder and ShareX, so a board still only on this machine can be dumped before it is committed.

Not read: the lobby's View Names screen, whose nametags follow the beans around in 3D and overlap into pileups in a full lobby, any name not written in the Latin alphabet, and the board while its plate still reads N REMAIN! — see below.

The first run downloads Tesseract's English model, about 5MB, into .ocr-cache/. Do that once before the event — nothing afterwards needs the network. Read names are cached in the same folder against each file's modified time, so restarting the server does not re-read everything.

Capturing the screens automatically

The server records the screen for the whole event and cuts the frames the reader needs out of the recording afterwards, so nothing has to be shot by hand. The screen naming who finished first can be gone in a fraction of a second when a dozen beans qualify together, and no capture that reacts to an event can catch it.

Frames are found by the clock stamp inside the log line, never by when the line arrived, so a log that flushed late still names the right frame. The same recording is cut into one mp4 per show.

One frame per show is kept for the picture rather than for the names: the board after round one while its plate still reads N REMAIN!. Every card is a bean under its own nameplate then — the eliminated only turn into a nameless pink X once the plate goes green — so it is the one screen that shows the whole field. It is filed with the other captures and never read, because before the plate settles every card looks qualified.

CAPTURE_OUTPUT=1 bun run live      # records
bun run dev --no-record            # does not

Recording is on by default, because a show that was not recorded cannot be recovered. It grabs a whole monitor and writes gigabytes an hour, so --no-record is there for working on the admin. Either way the console says Recording on with the folder, or off, on every start.

Setting Default What it is
CAPTURE_OUTPUT 0 Which monitor to record, numbered from 0
CAPTURE_DIR /mnt/c/temp/FallGuysCapture Where segments, clips and frames go
CAPTURE_AUDIO virtual-audio-capturer dshow device to record sound from; off for silent
FFMPEG_PATH /mnt/c/Program Files/ShareX/ffmpeg.exe ShareX ships the one this uses

The header carries a recording badge. If it reads NOT RECORDING, nothing is being captured and Alt + Print Screen is the only thing still working.

Everything one show produced lands in one folder, CAPTURE_DIR/shows/show-2026-09-02T23h25-solos-4, named for the clock its first round loaded on:

In the folder What it is
2026-09-02-show-04-solos-4.mp4 the show, cut from the first round to just past the victory screen without re-encoding
transcript.txt that show's transcript lines, and no others
round-01-first-race-finisher-01.jpg the frames, named for the round and what they show

Rounds are numbered from 1; the winner screen is filed under the final's number. The whole evening's transcript stays at CAPTURE_DIR/2026-09-02.transcript.txt.

The raw recording sits under CAPTURE_DIR/segments in one folder per run, stamped 2026-09-02T21h41m03: every start of the server, and every recovery from an ffmpeg that died mid-event, gets its own, so nothing ever writes over footage that is already there. A clip cut across such a recovery has a jump in it, and the console says which one. Nothing is cleaned up: CAPTURE_DIR is yours to empty. A static desktop runs about 1.7 Mbps, so budget a few gigabytes an hour and leave 30GB free.

Before the event

  1. Install virtual-audio-capturer (from the screen-capture-recorder installer) and confirm ffmpeg lists it:
    "/mnt/c/Program Files/ShareX/ffmpeg.exe" -list_devices true -f dshow -i dummy
    Without it the recording still happens, silently, and the badge says recording — no sound.
  2. Pick the monitor and prove the captures work: Tournament day.

This machine has no NVENC or AMF encoder, so the recording uses Quick Sync (h264_qsv).

Tests

bun install
bun test
bun run typecheck

About

Fall Guys solo tournament scoring and leaderboard for the FOM LAN party

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages