Capstone: A Live Scoreboard · capstone
Capstone project: Run the Whole Quiz Night
The server and the client have never yet run together. This lesson runs them through a whole quiz night, adds the test that pins how a quiz ends, and checks the entire project at once. One scenario of pl_ws.py puts your two programs on stage:
python pl_ws.py quiz-night
It calls your server’s main in one thread and your client’s main for viewer A and for every scorekeeper, plays two viewers of its own, B and S, and slows S down on purpose. A finished version, run on the site, printed the transcript below; read it block by block. Lines with | are a program’s standard output, lines with ! its standard error, and PORT stands for the
scenario quiz-night: your server and your client through a whole quiz night
viewer A is your watch; B is this program's; S is this program's and is slowed down on purpose
part 1: a quiz of three updates; first a scorekeeper with the wrong token, then three scores
your server's main() is called with --port 0 --max-updates 3 --deadline 6
viewer B joined; its first message was a snapshot with version 0
viewer S joined; its first message was a snapshot with version 0
scorekeeper (wrong token) ! the scoreboard refused the connection: HTTP 401
scorekeeper (wrong token) exited with 1
scorekeeper | version 1: Owls 3, Foxes 0, Herons 0
scorekeeper exited with 0
scorekeeper | version 2: Owls 3, Foxes 2, Herons 0
scorekeeper exited with 0
scorekeeper | version 3: Owls 3, Foxes 2, Herons 5
scorekeeper exited with 0
Your server refused the wrong token before the upgrade, and your score turned the 401 into one standard-error line and exit status 1. Then three scores, each answered with its snapshot.
viewer A (your client) | version 0: Owls 0, Foxes 0, Herons 0
viewer A (your client) | version 1: Owls 3, Foxes 0, Herons 0
viewer A (your client) | version 2: Owls 3, Foxes 2, Herons 0
viewer A (your client) | version 3: Owls 3, Foxes 2, Herons 5
viewer A (your client) exited with 0
viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer S received versions [0, 1, 3], then the server closed it with 1000 'the quiz is over'
your server | listening on 127.0.0.1:PORT
your server | the quiz is over at version 3; every viewer was closed with 1000
your main() returned 0
Your watch printed every version from 0 to 3 and left with 0 on the server’s close. Viewer S never saw version 2: while its thread was still busy sending version 1, version 3 replaced version 2 in its outbox. It still received the last version before its close with 1000, which is exactly Lesson 1’s rule. Which versions S receives depends on timing, so your run may show a different
part 2: a quiz of ten updates with a deadline of 1.5 seconds; one score
your server's main() is called with --port 0 --max-updates 10 --deadline 1.5
viewer B joined; its first message was a snapshot with version 0
viewer S joined; its first message was a snapshot with version 0
scorekeeper | version 1: Owls 2, Foxes 0, Herons 0
scorekeeper exited with 0
viewer A (your client) | version 0: Owls 0, Foxes 0, Herons 0
viewer A (your client) | version 1: Owls 2, Foxes 0, Herons 0
viewer A (your client) exited with 0
viewer B received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
viewer S received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
your server | listening on 127.0.0.1:PORT
your server | the deadline passed at version 1; every connection was closed with 1001
your main() returned 0
the token from token.txt appears in no line of this transcript
The second quiz could not finish: ten updates were due and only one came. At the deadline your server closed everyone with 1001, your watch took that as a normal ending, and main still returned 0. The last line is a check pl_ws.py makes on its own output; one more line, not shown, counts threads and changes from run to run.
In a quiz that finishes at version 3, the slowed viewer S received versions 0, 1 and 3, then a close with 1000. Is that a fault?
The last test
The ending is the rule this chapter added, and nothing in your suite pins it yet. Replace the last stub in tests/test_scoreboard.py, test_the_last_update_is_sent_before_the_close. Make a board with max_updates=1 and apply one score, so the quiz is over at version 1. Build a stand-in viewer with types.SimpleNamespace: an id, a send that records each message, parsed, and a close(code, reason) that records the code. Call scoreboard_server.viewer_handler(viewer, board). It returns at once, because the last snapshot is already the join snapshot: nothing has to wait and no recv is needed. Then assert that the last message sent has version 1, that the closes recorded are exactly [1000], and that board["finished"].is_set().
What Check my work checks
This is the whole acceptance check of the capstone, in words. Importing the finished goes up only when nobody waits. A failed, a dead and a crashing viewer each leave the registry. The scorekeeper’s answers, the token check, the room limit, the server’s address, bounds, endings and exit statuses, the client’s output and exit statuses, and your contract’s facts are checked as before, and the token appears nowhere. Then your tests run against nine versions that each make one realistic mistake: Lesson 4’s eight and a viewer that closes with 1000 without sending the last snapshot.
Optional: run it on your own computer
This part is not assessed, and nothing on the site sees it: no lesson asks for a result from it, and nothing is sent back. If you want to watch your scoreboard outside the browser, press Download project, extract the ZIP into a new, empty folder, and open a websockets 17.4 requires it; each tab starts by checking. If the number is lower, install a newer Python from python.org and use its name, such as python3.13 (or py -3.13 on Windows), wherever the commands say python3 (or py). The project’s README.md holds the same commands.
Check the version, set up once, then start the server in the first terminal:
python3 --version
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python -m pytest -q
./.venv/bin/python scoreboard_server.py --max-updates 2 --deadline 60
In a second terminal in the same folder, watch; in a third, score twice:
./.venv/bin/python scoreboard_client.py watch --url ws://127.0.0.1:8765/watch --max-messages 3 --wait 60
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Owls --points 3
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Foxes --points 2
In PowerShell, check the version, set up once, then start the server in the first terminal:
py --version
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -m pytest -q
.\.venv\Scripts\python.exe scoreboard_server.py --max-updates 2 --deadline 60
In a second terminal in the same folder, watch; in a third, score twice:
.\.venv\Scripts\python.exe scoreboard_client.py watch --url ws://127.0.0.1:8765/watch --max-messages 3 --wait 60
.\.venv\Scripts\python.exe scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Owls --points 3
.\.venv\Scripts\python.exe scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Foxes --points 2
Check the version, set up once, then start the server in the first terminal:
python3 --version
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python -m pytest -q
./.venv/bin/python scoreboard_server.py --max-updates 2 --deadline 60
In a second terminal in the same folder, watch; in a third, score twice:
./.venv/bin/python scoreboard_client.py watch --url ws://127.0.0.1:8765/watch --max-messages 3 --wait 60
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Owls --points 3
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Foxes --points 2
Every command names the Python inside .venv, so there is nothing to activate. Three terminals, because watch keeps its terminal busy until its last snapshot.
terminal 1 server: listening on 127.0.0.1:8765 ... closing line, exit 0
terminal 2 watch: version 0, version 1, version 2, exit 0
terminal 3 score Owls 3: version 1 score Foxes 2: version 2
all three talk over 127.0.0.1, which never leaves your computer
In words: one terminal holds the server, one the viewer, and one the scorekeeper, and all three talk to each other on your own computer only. With a finished version on a Mac, these commands gave this run: the server printed listening on 127.0.0.1:8765; the two scores printed version 1: Owls 3, Foxes 0, Herons 0 and version 2: Owls 3, Foxes 2, Herons 0, each with exit status 0; watch printed versions 0 to 2 and exited with 0; and the server printed its closing line and exited with 0. A watch started after that printed could not connect to ws://127.0.0.1:8765/watch and exited with 1, because nothing listened any more.
Why does the run on your own computer need three terminals, not two?
After your run on your own computer, what does Python Land know about it?
The finished scoreboard
You have a server with a written contract, bounds and two clean endings, a client that turns every refusal and close into an exit status, and tests that notice realistic mistakes. Many
Task
In tests/test_scoreboard.py, replace the last stub, test_the_last_update_is_sent_before_the_close, keeping its name: on a board made with scoreboard_server.make_board([...], max_updates=1) and given one score, call scoreboard_server.viewer_handler with a stand-in whose send records each message and whose close(code, reason) records the code, then assert that the last message sent has version 1, that the codes recorded are [1000], and that board["finished"].is_set(). Keep every other test, and keep every earlier lesson’s code correct.
Run python -m pytest -q until every test passes, and run python pl_ws.py quiz-night. Then press Check my work. It checks the whole capstone, with stand-ins and no thread or hmac.compare_digest; the room limit; serve_scoreboard and main; watch, score and the supplied client main; the facts in SCOREBOARD_PROTOCOL.md; and the token on no stream, in no viewer_handler that closes with 1000 without sending the last snapshot. Against each, at least one test must fail. A run still going after 3 seconds is stopped, and against a mistaken version that counts as noticed; no new run starts once 8 seconds have passed. Extra tests, helpers and edits to README.md are fine. The optional run on your own computer is not checked.