1- """`codeclash replay` — turn a tournament log folder into browsable game playback .
1+ """`codeclash replay` — serve a tournament folder and watch game replays in the browser .
22
33Points at a tournament folder (the one holding ``metadata.json`` + ``rounds/``); the arena
4- is read from the metadata (or the folder name) and dispatched to its renderer. Games are
5- discovered under ``rounds/`` (including ``round_<R>.tar.gz`` archives).
4+ is read from the metadata (or the folder name). Games are discovered under ``rounds/``
5+ (including ``round_<R>.tar.gz`` archives) and each replay is rendered on demand — nothing
6+ is written to disk.
67"""
78
89from __future__ import annotations
910
10- import webbrowser
1111from pathlib import Path
1212
1313import typer
1414
15- from codeclash .replay import base , get_replayer , load_tournament
16-
17- # Cap on how many games `--all` will pre-generate before it stops and says so.
18- ALL_CAP = 300
19-
20-
21- def _bad (msg : str ):
22- typer .echo (f"Error: { msg } " , err = True )
23- raise typer .Exit (2 )
15+ from codeclash .replay import get_replayer , load_tournament
16+ from codeclash .replay .serve import run_server
2417
2518
2619def replay (
2720 folder : Path = typer .Argument (..., help = "Tournament log folder (contains metadata.json + rounds/)." ),
28- round : int | None = typer .Option (None , "--round" , "-r" , help = "Round to replay (alone: every sim in the round)." ),
29- sim : int | None = typer .Option (None , "--sim" , "-s" , help = "Sim index (alone: that sim across every round)." ),
30- all_games : bool = typer .Option (False , "--all" , help = "Pre-generate a playback page for every game." ),
31- ascii_out : bool = typer .Option (False , "--ascii" , help = "Dump a single game to the terminal instead of HTML." ),
32- open_browser : bool = typer .Option (False , "--open" , "-o" , help = "Open the result in a browser when done." ),
21+ port : int = typer .Option (8000 , "--port" , "-p" , help = "Port to serve on (falls back to a free one if taken)." ),
3322):
34- """Build a browsable replay site (index + on-demand per- game pages) for a tournament .
23+ """Serve a tournament folder and lazily watch game replays in the browser .
3524
36- [dim]• codeclash replay logs/<tournament-folder> # build the browsable index[/dim]
37- [dim]• codeclash replay logs/<folder> -r 1 -s 0 --open # one game, open in browser[/dim]
38- [dim]• codeclash replay logs/<folder> -r 1 # every sim in round 1[/dim]
39- [dim]• codeclash replay logs/<folder> --all # pre-generate every game[/dim]
40- [dim]• codeclash replay logs/<folder> --ascii # dump a game to the terminal[/dim]
25+ [dim]• codeclash replay logs/<tournament-folder>[/dim]
26+ [dim]• codeclash replay logs/<folder> -p 9000[/dim]
4127 """
42- # Reject contradictory modes up front, rather than silently picking one.
43- if ascii_out and all_games :
44- _bad ("use either --ascii or --all, not both." )
45- if ascii_out and open_browser :
46- _bad ("--ascii writes nothing to open; drop --open." )
47- if all_games and (round is not None or sim is not None ):
48- _bad ("--all replays every game — drop -r/-s (or omit --all to pick specific games)." )
49-
5028 tour = load_tournament (folder )
5129 renderer = get_replayer (tour .arena )
5230 if renderer is None :
@@ -55,74 +33,4 @@ def replay(
5533 if not tour .games :
5634 typer .echo (f"No sim files found under { folder } (looked for { renderer .sim_glob } )." )
5735 raise typer .Exit (1 )
58-
59- def tour_payload (r : int , s : int ) -> dict :
60- return {
61- "arena" : tour .arena ,
62- "players" : tour .players ,
63- "round" : r ,
64- "sim" : s ,
65- "round_winner" : tour .round_winners .get (r ),
66- }
67-
68- def load (game ):
69- return renderer .parse (base .read_sim (game ), tour .players )
70-
71- # ascii: a single game to the terminal, no files written.
72- if ascii_out :
73- g0 = tour .games [0 ]
74- r = round if round is not None else g0 .round
75- s = sim if sim is not None else g0 .sim
76- game = next ((g for g in tour .games if g .round == r and g .sim == s ), None )
77- if game is None :
78- typer .echo (f"No game at round { r } , sim { s } ." )
79- raise typer .Exit (1 )
80- typer .echo (renderer .ascii (load (game )))
81- return
82-
83- # Which games to generate as HTML pages:
84- # -r R -s S -> that one game
85- # -r R -> every sim in round R
86- # -s S -> sim S across every round
87- # --all -> every game (capped)
88- # (none) -> index only
89- if all_games :
90- todo = tour .games [:ALL_CAP ]
91- elif round is not None and sim is not None :
92- todo = [g for g in tour .games if g .round == round and g .sim == sim ]
93- elif round is not None :
94- todo = [g for g in tour .games if g .round == round ]
95- elif sim is not None :
96- todo = [g for g in tour .games if g .sim == sim ]
97- else :
98- todo = []
99-
100- if (round is not None or sim is not None ) and not todo :
101- typer .echo (f"No games match round={ round } sim={ sim } . Run `codeclash replay { folder } ` to list games." )
102- raise typer .Exit (1 )
103-
104- replay_dir = folder / "replay"
105- base .write_assets (replay_dir , renderer )
106- for g in todo :
107- (replay_dir / f"r{ g .round } _s{ g .sim } .html" ).write_text (
108- base .build_sim_stub (load (g ), tour_payload (g .round , g .sim ), renderer )
109- )
110- # Always (re)build the index so newly generated pages show as watchable.
111- index = replay_dir / "index.html"
112- index .write_text (base .build_index (tour , replay_dir ))
113-
114- if todo :
115- typer .echo (f"Generated { len (todo )} game page(s). Index: { index } " )
116- if all_games and len (tour .games ) > ALL_CAP :
117- typer .echo (f"Note: capped at { ALL_CAP } of { len (tour .games )} games — pass -r/-s for the rest." )
118- else :
119- typer .echo (f"Replay index: { index } " )
120- typer .echo (f"{ len (tour .games )} games found. Open a specific one with `-r <round> [-s <sim>]`." )
121-
122- if open_browser :
123- # Open the single generated page if there's exactly one, else the index.
124- target = replay_dir / f"r{ todo [0 ].round } _s{ todo [0 ].sim } .html" if len (todo ) == 1 else index
125- try :
126- webbrowser .open (target .resolve ().as_uri ())
127- except Exception :
128- typer .echo (f"(couldn't open a browser automatically — open { target } manually)" )
36+ run_server (folder , tour , renderer , port = port )
0 commit comments