Find every playable game under a tournament folder. Handles three layouts: ``rounds/round_ .tar.gz`` archives (the common case), uncompressed ``rounds/ /sim_*`` directories, and a flat folder of sim files (e.g. an ad-hoc sample with a single ``sim.json``).
(folder: Path, sim_glob: str)
| 112 | ``round_1`` -> 1), else 0. ``discover_games`` de-duplicates any collisions within a round.""" |
| 113 | nums = re.findall(r"\d+", name.rsplit(".", 1)[0]) |
| 114 | return int(nums[-1]) if nums else 0 |
| 115 | |
| 116 | |
| 117 | def _round_num(name: str) -> int: |
| 118 | m = re.search(r"(\d+)", name) |
| 119 | return int(m.group(1)) if m else 0 |
| 120 | |
| 121 | |
| 122 | def discover_games(folder: Path, sim_glob: str) -> list[GameRef]: |
| 123 | """Find every playable game under a tournament folder. |
| 124 | |
| 125 | Handles three layouts: ``rounds/round_<R>.tar.gz`` archives (the common case), |
| 126 | uncompressed ``rounds/<R>/sim_*`` directories, and a flat folder of sim files |
| 127 | (e.g. an ad-hoc sample with a single ``sim.json``). |
| 128 | """ |
| 129 | games: list[GameRef] = [] |
| 130 | rounds_dir = folder / "rounds" |
| 131 | if rounds_dir.is_dir(): |
| 132 | entries = sorted(rounds_dir.iterdir()) |
| 133 | # Archives first, recording which rounds they cover. |
| 134 | archived: set[int] = set() |
| 135 | for entry in entries: |
| 136 | if entry.is_file() and entry.name.startswith("round") and entry.suffixes[-1:] == [".gz"]: |
| 137 | rnum = _round_num(entry.name) |
| 138 | archived.add(rnum) |
| 139 | with tarfile.open(entry) as tf: |
| 140 | for m in tf.getmembers(): |
| 141 | base = Path(m.name).name |
| 142 | if m.isfile() and m.size > 0 and not base.startswith("._") and fnmatch.fnmatch(base, sim_glob): |
| 143 | games.append( |
| 144 | GameRef(round=rnum, sim=_sim_index(Path(m.name).name), archive=entry, member=m.name) |
| 145 | ) |
| 146 | # Then round dirs — but skip any round already sourced from an archive. A round can |
| 147 | # exist both extracted and tarred; counting both would double up the same games. |
| 148 | for entry in entries: |
| 149 | if entry.is_dir() and _round_num(entry.name) not in archived: |
| 150 | rnum = _round_num(entry.name) |
| 151 | for f in sorted(entry.iterdir()): |
| 152 | if ( |
| 153 | f.is_file() |
| 154 | and f.stat().st_size > 0 |
| 155 | and not f.name.startswith("._") |
| 156 | and fnmatch.fnmatch(f.name, sim_glob) |
| 157 | ): |
| 158 | games.append(GameRef(round=rnum, sim=_sim_index(f.name), path=f)) |
| 159 | else: |
| 160 | for f in sorted(folder.iterdir()): |
| 161 | if f.is_file() and f.stat().st_size > 0 and fnmatch.fnmatch(f.name, sim_glob): |
| 162 | games.append(GameRef(round=0, sim=_sim_index(f.name), path=f)) |
| 163 | # Ensure sim indices are unique within each round; if a filename scheme collides |
| 164 | # (or lacks numbers), fall back to enumeration order. |
| 165 | per_round: dict[int, list[GameRef]] = {} |
| 166 | for g in games: |
| 167 | per_round.setdefault(g.round, []).append(g) |
| 168 | for gs in per_round.values(): |
| 169 | sims = [g.sim for g in gs] |
| 170 | # Enumerate on collisions, or when the parsed numbers are clearly seeds/timestamps |
| 171 | # (e.g. Halite's .hlt names) rather than small sim indices. |
no test coverage detected