-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflatbracket.py
More file actions
205 lines (159 loc) · 6.3 KB
/
flatbracket.py
File metadata and controls
205 lines (159 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import base64
import argparse
from random import SystemRandom
from itertools import batched
from collections.abc import Iterable
# 64 team bracket encodes to 11 characters (padding removed)
def encode(bracket_data: bytes) -> str:
return base64.urlsafe_b64encode(bracket_data).rstrip(b"=").decode("ascii")
def decode(bracket: str) -> bytes:
return base64.urlsafe_b64decode(bracket + "==")
RNG = SystemRandom()
def _generate_seeds(number_of_teams: int) -> list[int]:
if number_of_teams <= 2:
return list(range(1, number_of_teams + 1))
previous = _generate_seeds(number_of_teams // 2)
n_teams_and_one = number_of_teams + 1
result = [
previous[0],
n_teams_and_one - previous[0],
]
if len(previous) > 1: # allow for non-power of two teams
result.extend(
(
previous[1],
n_teams_and_one - previous[1],
)
)
for i in range(2, len(previous), 2):
first, second = previous[i + 1], previous[i]
result.extend(
(
first,
n_teams_and_one - first,
second,
n_teams_and_one - second,
)
)
return result
def random_result(above_index: int, below_index: int, seeds: list[int]) -> bool:
"""Generate random result weighted by seed."""
n_seeds = len(seeds)
first_seed = seeds[above_index % n_seeds]
second_seed = seeds[below_index % n_seeds]
probability = second_seed / (first_seed + second_seed)
return RNG.random() > probability
def iter_results(bracket_data: bytes):
"""Yield individual bit results from bracket data."""
for byte in bracket_data:
for i in range(8):
yield (byte >> i) & 1
def initialize_matchups(number_of_teams: int) -> list:
"""Initialize matchups for the first round."""
# Remove incomplete pairs
return [pair for pair in batched(range(number_of_teams), 2) if len(pair) == 2]
def generate_matchups(
results: Iterable[bool | int], number_of_teams: int
) -> Iterable[tuple[int, int]]:
"""Generate all matchups based on bracket results."""
matchups = initialize_matchups(number_of_teams)
yield from matchups
results_iter = iter(results)
matchup_index = 0
prev_winner = winner = 0
while matchup_index < len(matchups):
matchup = matchups[matchup_index]
result = next(results_iter, None)
if result is None:
# no more results, stop iteration
return
winner = matchup[result]
# After every second game, create next round matchup
if matchup_index % 2:
next_matchup = (prev_winner, winner)
matchups.append(next_matchup)
yield next_matchup
prev_winner = winner
matchup_index += 1
yield (winner, winner) # chicken dinner
def create_bracket(results: Iterable[int | bool]) -> bytes:
return bytes(
sum(1 << i for i, bit in enumerate(byte_results) if bit)
for byte_results in batched(results, 8)
)
def _draw(matchups: list, teams: list):
"""Generate mermaid flowchart lines for bracket visualization."""
round_size = len(teams)
game_num = 0
yield "flowchart LR"
for above, below in matchups:
next_round_size = round_size // 2
round_id = f"{round_size}-{game_num}"
if next_round_size:
next_round_id = f"{next_round_size}-{game_num // 2}"
yield f" {round_id}[{teams[above]} vs. {teams[below]}] --> {next_round_id}"
else:
yield f" {round_id}[{teams[above]}]"
game_num += 1
if game_num >= next_round_size:
game_num = 0
round_size = next_round_size
def draw(matchups, teams: list) -> str:
"""Generate mermaid flowchart diagram for bracket."""
return "\n".join(_draw(matchups, teams))
def main():
parser = argparse.ArgumentParser(
"flatbracket", description="Flat bracket encoding tool", allow_abbrev=False
)
parser.add_argument("teams", help="file containing teams list")
parser.add_argument("bracket", nargs="?", help="encoded bracket to view")
parser.add_argument(
"-r", "--random", action="store_true", help="create random bracket"
)
parser.add_argument("--regions", type=int, default=4, help="number of regions")
args = parser.parse_args()
with open(args.teams) as file:
teams = [line.rstrip() for line in file]
if args.bracket:
# View mode: decode and display bracket
print(f"Bracket: {args.bracket}\n")
bracket_data = decode(args.bracket)
diagram = draw(generate_matchups(iter_results(bracket_data), len(teams)), teams)
print(diagram)
else:
# Create mode: build bracket interactively or randomly
number_of_teams = len(teams)
# assert _is_power_of_two(number_of_teams), "Number of teams must be a power of 2"
# assert _is_power_of_two(args.regions), "Number of regions must be a power of 2"
seeds = _generate_seeds(max(number_of_teams // args.regions, 1))
matchups = initialize_matchups(number_of_teams)
matchup_index = 0
prev_winner = 0
results = []
while matchup_index < len(matchups):
above, below = matchups[matchup_index]
above_seed = seeds[above % len(seeds)]
below_seed = seeds[below % len(seeds)]
print(f"{teams[above]} ({above_seed}) vs. {teams[below]} ({below_seed})")
if args.random:
result = random_result(above, below, seeds)
print("[1/2]: ", 2 if result else 1)
else:
while True:
result_str = input("[1/2]: ").strip()
if result_str in ("1", "2"):
result = result_str == "2"
break
results.append(result)
winner = below if result else above
# Create next round matchup after every second game
if matchup_index % 2:
matchups.append((prev_winner, winner))
prev_winner = winner
matchup_index += 1
bracket_data = create_bracket(results)
print(f"\nWinner: {teams[prev_winner]}")
print(f"Bracket: {encode(bracket_data)}")
print()
if __name__ == "__main__":
main()