Project P034

Tournament bracket

A single-elimination bracket for 2 to 16 players typed in seed order: the top seeds get the byes, seeds are placed so that the strongest meet as late as possible, results are typed match by match and winners go through round by round to a champion. A result can be corrected while the winner's next match is still to be played.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-10

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A single-elimination bracket for 2 to 16 players, typed in seed order - the strongest first. The bracket is the next power of two: the top seeds get the byes, and the seeds are placed so that the strongest meet as late as possible. Results are typed match by match, and the winners go through round by round to the final and a champion. A result can be corrected while the winner's next match is still to be played.

  • main.eml - the players and their checks, the menu, entering and correcting results
  • bracket.eml - the draw: the bracket size, the seed order, the rounds, who meets whom and who goes through, the open and the correctable matches
  • view.eml - the bracket on screen, round by round

How each part works:

  • The seed order is built by doubling: [1, 2] becomes [1, 4, 2, 3], then [1, 8, 4, 5, 2, 7, 3, 6] - each seed goes next to the one that makes the pair add up to the new size plus one. So if the better seed always wins, every round pairs the best player left with the worst: 1 and 2 can only meet in the final, 1 to 4 only from the semi-finals on. The corpus case tournament-bracket pairs teams in the order they were listed, and there its two strongest teams meet in the semi-final - the draw decided that, not the ratings.
  • With fewer players than the bracket holds, the empty places are byes. They sit opposite the top seeds, so a bye is a free pass to the second round for the strongest, and two byes never meet.
  • Every place after the first round is the winner of the match that feeds it, worked out again from the scores whenever it is needed, so correcting a result changes who goes through with nothing else to update. That is also why a result can only be corrected while its winner's next match has no score: after that, the later result belongs to a pairing that would no longer exist.
  • A winner who was the lower seed is marked as an upset.

What is checked: names of 1 to 12 letters, each different in any case, 2 to 16 of them; a match from the list, typed as 2 or M2; a score as two numbers from 0 to 99 (3-1, 3:1 or 3 - 1), and never a draw - a knockout match needs a winner. An empty answer cancels.

Sessions: sessions/basic.in types six players, so seeds 1 and 2 get byes; enters two upsets in the quarter-finals, shows the bracket, and corrects one of them - Dee, not Eve, now goes on to meet Ana; then plays the semi-finals (one more upset) and the final, which makes Ana the champion. sessions/bad-input.in gives an empty list, a name with a digit, a name of 13 letters, the same name in capitals and an empty line after one player; then sixteen players, a full bracket with no byes; menu choices 0 and x; a correction before any result; matches M16, x, M and 9, none of them open; scores 3, a-b, 100-1, 3- and a draw. It then plays two first-round matches and the quarter-final they lead to, tries to correct a first-round result that can no longer change and a match not yet played, corrects the quarter-final and shows the bracket.

Built on the verified corpus case tournament-bracket (a single-elimination field that halves every round until one is left).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Tournament bracket ==
Type the players from the strongest down - seed 1 first - one name a line;
an empty line ends the list (2 to 16 players).
player 1> 
At least 2 players are needed.
player 1> Al1
A name is 1 to 12 letters.
player 1> Abcdefghijklm
A name is 1 to 12 letters.
player 1> Ann
player 2> ANN
Ann is already on the list.
player 2> 
At least 2 players are needed.
player 2> Ben
player 3> Cal
player 4> Dot
player 5> Ed
player 6> Flo
player 7> Gil
player 8> Hal
player 9> Ivy
player 10> Jo
player 11> Kit
player 12> Lou
player 13> Max
player 14> Ned
player 15> Ola
player 16> Pip
That makes 16, the most.
16 players: a bracket of 16, no byes.
Round of 16
  M1  (1) Ann           v      (16) Pip
  M2  (8) Hal           v      (9) Ivy
  M3  (4) Dot           v      (13) Max
  M4  (5) Ed            v      (12) Lou
  M5  (2) Ben           v      (15) Ola
  M6  (7) Gil           v      (10) Jo
  M7  (3) Cal           v      (14) Ned
  M8  (6) Flo           v      (11) Kit
Quarter-finals
  M9  winner of M1      v      winner of M2
  M10 winner of M3      v      winner of M4
  M11 winner of M5      v      winner of M6
  M12 winner of M7      v      winner of M8
Semi-finals
  M13 winner of M9      v      winner of M10
  M14 winner of M11     v      winner of M12
Final
  M15 winner of M13     v      winner of M14

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 0
Pick a number from 1 to 4.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> x
Pick a number from 1 to 4.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 3
No result can be corrected now.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M1  (1) Ann v (16) Pip
  M2  (8) Hal v (9) Ivy
  M3  (4) Dot v (13) Max
  M4  (5) Ed v (12) Lou
  M5  (2) Ben v (15) Ola
  M6  (7) Gil v (10) Jo
  M7  (3) Cal v (14) Ned
  M8  (6) Flo v (11) Kit
match> M16
Type one of the matches listed.
match> x
Type one of the matches listed.
match> M
Type one of the matches listed.
match> 9
Type one of the matches listed.
match> 
Cancelled.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M1  (1) Ann v (16) Pip
  M2  (8) Hal v (9) Ivy
  M3  (4) Dot v (13) Max
  M4  (5) Ed v (12) Lou
  M5  (2) Ben v (15) Ola
  M6  (7) Gil v (10) Jo
  M7  (3) Cal v (14) Ned
  M8  (6) Flo v (11) Kit
match> 1
score, Ann first (like 3-1)> 3
Type the score as two numbers from 0 to 99, like 3-1.
score, Ann first (like 3-1)> a-b
Type the score as two numbers from 0 to 99, like 3-1.
score, Ann first (like 3-1)> 100-1
Type the score as two numbers from 0 to 99, like 3-1.
score, Ann first (like 3-1)> 3-
Type the score as two numbers from 0 to 99, like 3-1.
score, Ann first (like 3-1)> 2-2
A knockout match needs a winner: give the score after extra time or penalties.
score, Ann first (like 3-1)> 
Cancelled.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M1  (1) Ann v (16) Pip
  M2  (8) Hal v (9) Ivy
  M3  (4) Dot v (13) Max
  M4  (5) Ed v (12) Lou
  M5  (2) Ben v (15) Ola
  M6  (7) Gil v (10) Jo
  M7  (3) Cal v (14) Ned
  M8  (6) Flo v (11) Kit
match> 1
score, Ann first (like 3-1)> 2-0
M1: Ann beats Pip 2-0.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M2  (8) Hal v (9) Ivy
  M3  (4) Dot v (13) Max
  M4  (5) Ed v (12) Lou
  M5  (2) Ben v (15) Ola
  M6  (7) Gil v (10) Jo
  M7  (3) Cal v (14) Ned
  M8  (6) Flo v (11) Kit
match> M2
score, Hal first (like 3-1)> 0-1
M2: Ivy beats Hal 1-0.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M3  (4) Dot v (13) Max
  M4  (5) Ed v (12) Lou
  M5  (2) Ben v (15) Ola
  M6  (7) Gil v (10) Jo
  M7  (3) Cal v (14) Ned
  M8  (6) Flo v (11) Kit
  M9  (1) Ann v (9) Ivy
match> M9
score, Ann first (like 3-1)> 1-1
A knockout match needs a winner: give the score after extra time or penalties.
score, Ann first (like 3-1)> 1-2
M9: Ivy beats Ann 2-1.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 3
Results that can still be corrected:
  M9  (1) Ann 1-2 (9) Ivy
match> M1
Type one of the matches listed.
match> M3
Type one of the matches listed.
match> 
Cancelled.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 3
Results that can still be corrected:
  M9  (1) Ann 1-2 (9) Ivy
match> M9
score, Ann first (like 3-1)> 5-4
M9 corrected: Ann beats Ivy 5-4.
Ann takes Ivy's place in M13.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 1
Round of 16
  M1  (1) Ann           2-0    (16) Pip          -> Ann
  M2  (8) Hal           0-1    (9) Ivy           -> Ivy (upset)
  M3  (4) Dot           v      (13) Max
  M4  (5) Ed            v      (12) Lou
  M5  (2) Ben           v      (15) Ola
  M6  (7) Gil           v      (10) Jo
  M7  (3) Cal           v      (14) Ned
  M8  (6) Flo           v      (11) Kit
Quarter-finals
  M9  (1) Ann           5-4    (9) Ivy           -> Ann
  M10 winner of M3      v      winner of M4
  M11 winner of M5      v      winner of M6
  M12 winner of M7      v      winner of M8
Semi-finals
  M13 (1) Ann           v      winner of M10
  M14 winner of M11     v      winner of M12
Final
  M15 winner of M13     v      winner of M14

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 4
Bye.
What was typed (57 lines)

Al1
Abcdefghijklm
Ann
ANN

Ben
Cal
Dot
Ed
Flo
Gil
Hal
Ivy
Jo
Kit
Lou
Max
Ned
Ola
Pip
0
x
3
2
M16
x
M
9

2
1
3
a-b
100-1
3-
2-2

2
1
2-0
2
M2
0-1
2
M9
1-1
1-2
3
M1
M3

3
M9
5-4
1
4

basic

interpreter: byte-equal
== Tournament bracket ==
Type the players from the strongest down - seed 1 first - one name a line;
an empty line ends the list (2 to 16 players).
player 1> Ana
player 2> Bo
player 3> Cy
player 4> Dee
player 5> Eve
player 6> Fay
player 7> 
6 players: a bracket of 8, with byes for seeds 1 and 2.
Quarter-finals
  M1  (1) Ana                  bye               -> Ana
  M2  (4) Dee           v      (5) Eve
  M3  (2) Bo                   bye               -> Bo
  M4  (3) Cy            v      (6) Fay
Semi-finals
  M5  (1) Ana           v      winner of M2
  M6  (2) Bo            v      winner of M4
Final
  M7  winner of M5      v      winner of M6

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M2  (4) Dee v (5) Eve
  M4  (3) Cy v (6) Fay
match> M2
score, Dee first (like 3-1)> 1-3
M2: Eve beats Dee 3-1.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M4  (3) Cy v (6) Fay
  M5  (1) Ana v (5) Eve
match> m4
score, Cy first (like 3-1)> 2-4
M4: Fay beats Cy 4-2.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 1
Quarter-finals
  M1  (1) Ana                  bye               -> Ana
  M2  (4) Dee           1-3    (5) Eve           -> Eve (upset)
  M3  (2) Bo                   bye               -> Bo
  M4  (3) Cy            2-4    (6) Fay           -> Fay (upset)
Semi-finals
  M5  (1) Ana           v      (5) Eve
  M6  (2) Bo            v      (6) Fay
Final
  M7  winner of M5      v      winner of M6

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 3
Results that can still be corrected:
  M2  (4) Dee 1-3 (5) Eve
  M4  (3) Cy 2-4 (6) Fay
match> M2
score, Dee first (like 3-1)> 3-1
M2 corrected: Dee beats Eve 3-1.
Dee takes Eve's place in M5.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M5  (1) Ana v (4) Dee
  M6  (2) Bo v (6) Fay
match> 5
score, Ana first (like 3-1)> 2-1
M5: Ana beats Dee 2-1.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M6  (2) Bo v (6) Fay
match> 6
score, Bo first (like 3-1)> 0:1
M6: Fay beats Bo 1-0.

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 2
Open matches:
  M7  (1) Ana v (6) Fay
match> 7
score, Ana first (like 3-1)> 3 - 2
M7: Ana beats Fay 3-2.
Ana is the champion!

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 1
Quarter-finals
  M1  (1) Ana                  bye               -> Ana
  M2  (4) Dee           3-1    (5) Eve           -> Dee
  M3  (2) Bo                   bye               -> Bo
  M4  (3) Cy            2-4    (6) Fay           -> Fay (upset)
Semi-finals
  M5  (1) Ana           2-1    (4) Dee           -> Ana
  M6  (2) Bo            0-1    (6) Fay           -> Fay (upset)
Final
  M7  (1) Ana           3-2    (6) Fay           -> Ana
Champion: (1) Ana

1) show the bracket  2) enter a result  3) correct a result  4) quit
choice> 4
Bye.
What was typed (28 lines)
Ana
Bo
Cy
Dee
Eve
Fay

2
M2
1-3
2
m4
2-4
1
3
M2
3-1
2
5
2-1
2
6
0:1
2
7
3 - 2
1
4

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P034 tournament bracket: a single-elimination bracket for 2 to 16 players.
# The players are typed in seed order; the bracket is the next power of two,
# the top seeds get the byes, and seeds are placed so that the strongest meet
# as late as possible. Results are typed match by match, winners go through
# round by round, and a result can be corrected while the winner's next match
# is still to be played.
import bracket
import view

16 => most_players
12 => longest_name
99 => highest_score

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def lower(s):
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => big
    "abcdefghijklmnopqrstuvwxyz" => small
    "" => out
    for c in s:
        0 => k
        while k < 26 and big[k] != c:
            k + 1 => k
        if k < 26:
            out + small[k] => out
        else:
            out + c => out
    return out

def is_name(s):
    if s == "" or len(s) > longest_name:
        return False
    for c in s:
        if not (c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"):
            return False
    return True

def number(s):
    # The value of 1 or 2 digits, otherwise -1.
    if s == "" or len(s) > 2:
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n

def parse_score(s):
    # "3-1", "3:1" or "3 - 1" as [3, 1]; [] if it is not a score.
    0 => k
    while k < len(s) and s[k] != "-" and s[k] != ":":
        k + 1 => k
    if k == len(s):
        return []
    number(trim(s[0:k])) => a
    number(trim(s[k + 1:len(s)])) => b
    if a < 0 or b < 0:
        return []
    return [a, b]

def read_players():
    [] => players
    True => reading
    while reading:
        trim(input("player " + str(len(players) + 1) + "> ")) => name
        if name == "":
            if len(players) >= 2:
                False => reading
            else:
                "At least 2 players are needed." ^0
        elif not is_name(name):
            ("A name is 1 to " + str(longest_name) + " letters.") ^0
        else:
            "" => taken
            for p in players:
                if lower(p) == lower(name):
                    p => taken
            if taken != "":
                (taken + " is already on the list.") ^0
            else:
                players + [name] => players
                if len(players) == most_players:
                    ("That makes " + str(most_players) + ", the most.") ^0
                    False => reading
    return players

def ask_match(prompt, allowed):
    # One of the allowed match numbers (typed as 2 or M2), or -1 when the
    # answer is empty.
    while True:
        trim(input(prompt)) => answer
        if answer == "":
            return -1
        if answer[0] == "M" or answer[0] == "m":
            answer[1:len(answer)] => answer
        number(answer) - 1 => m
        for a in allowed:
            if a == m:
                return m
        "Type one of the matches listed." ^0

def ask_score(first):
    # A score with a winner, or [] when the answer is empty.
    while True:
        trim(input("score, " + first + " first (like 3-1)> ")) => answer
        if answer == "":
            return []
        parse_score(answer) => score
        if len(score) == 0:
            ("Type the score as two numbers from 0 to " + str(highest_score) + ", like 3-1.") ^0
        elif score[0] == score[1]:
            "A knockout match needs a winner: give the score after extra time or penalties." ^0
        else:
            return score

def result_text(players, sides, score):
    # "Eve beats Dee 3-1", winner first.
    if score[0] > score[1]:
        return players[sides[0]] + " beats " + players[sides[1]] + " " + str(score[0]) + "-" + str(score[1])
    return players[sides[1]] + " beats " + players[sides[0]] + " " + str(score[1]) + "-" + str(score[0])

def with_score(scores, m, score):
    scores[0:m] + [score] + scores[m + 1:len(scores)] => out
    return out

def enter(players, size, scores):
    bracket.open_matches(len(players), size, scores) => free
    if len(free) == 0:
        ("The tournament is over: " + players[bracket.champion(len(players), size, scores)] + " is the champion.") ^0
        return scores
    "Open matches:" ^0
    for m in free:
        view.match_line(players, size, scores, m) ^0
    ask_match("match> ", free) => m
    if m == -1:
        "Cancelled." ^0
        return scores
    bracket.pairs(len(players), size, scores)[0][m] => sides
    ask_score(players[sides[0]]) => score
    if len(score) == 0:
        "Cancelled." ^0
        return scores
    with_score(scores, m, score) => scores
    ("M" + str(m + 1) + ": " + result_text(players, sides, score) + ".") ^0
    bracket.champion(len(players), size, scores) => c
    if c >= 0:
        (players[c] + " is the champion!") ^0
    return scores

def correct(players, size, scores):
    bracket.correctable(size, scores) => fixable
    if len(fixable) == 0:
        "No result can be corrected now." ^0
        return scores
    "Results that can still be corrected:" ^0
    for m in fixable:
        view.match_line(players, size, scores, m) ^0
    ask_match("match> ", fixable) => m
    if m == -1:
        "Cancelled." ^0
        return scores
    len(players) => n
    bracket.pairs(n, size, scores) => pw
    pw[0][m] => sides
    pw[1][m] => before
    ask_score(players[sides[0]]) => score
    if len(score) == 0:
        "Cancelled." ^0
        return scores
    with_score(scores, m, score) => scores
    ("M" + str(m + 1) + " corrected: " + result_text(players, sides, score) + ".") ^0
    bracket.pairs(n, size, scores)[1][m] => after
    if after != before:
        bracket.next_match(size, m) => going
        if going == -1:
            (players[after] + " is the champion now.") ^0
        else:
            (players[after] + " takes " + players[before] + "'s place in M" + str(going + 1) + ".") ^0
    return scores

"== Tournament bracket ==" ^0
"Type the players from the strongest down - seed 1 first - one name a line;" ^0
"an empty line ends the list (2 to 16 players)." ^0
read_players() => players
len(players) => n
bracket.size_for(n) => size
size - n => byes
if byes == 0:
    (str(n) + " players: a bracket of " + str(size) + ", no byes.") ^0
elif byes == 1:
    (str(n) + " players: a bracket of " + str(size) + ", with a bye for seed 1.") ^0
elif byes == 2:
    (str(n) + " players: a bracket of " + str(size) + ", with byes for seeds 1 and 2.") ^0
else:
    (str(n) + " players: a bracket of " + str(size) + ", with byes for seeds 1 to " + str(byes) + ".") ^0
[[] for m in [1:size - 1]] => scores
view.show(players, size, scores)
True => running
while running:
    "" ^0
    "1) show the bracket  2) enter a result  3) correct a result  4) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        view.show(players, size, scores)
    elif choice == "2":
        enter(players, size, scores) => scores
    elif choice == "3":
        correct(players, size, scores) => scores
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^0
"Bye." ^0
Python projection (main.py)
import bracket
import view
most_players = 16
longest_name = 12
highest_score = 99

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def lower(s):
    big = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        k = 0
        while k < 26 and big[k] != c:
            k = k + 1
        if k < 26:
            out = out + small[k]
        else:
            out = out + c
    return out

def is_name(s):
    if s == "" or len(s) > longest_name:
        return False
    for c in s:
        if not c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
            return False
    return True

def number(s):
    if s == "" or len(s) > 2:
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

def parse_score(s):
    k = 0
    while k < len(s) and s[k] != "-" and s[k] != ":":
        k = k + 1
    if k == len(s):
        return []
    a = number(trim(s[0:k]))
    b = number(trim(s[k + 1:len(s)]))
    if a < 0 or b < 0:
        return []
    return [a, b]

def read_players():
    players = []
    reading = True
    while reading:
        name = trim(input("player " + str(len(players) + 1) + "> "))
        if name == "":
            if len(players) >= 2:
                reading = False
            else:
                print("At least 2 players are needed.")
        elif not is_name(name):
            print("A name is 1 to " + str(longest_name) + " letters.")
        else:
            taken = ""
            for p in players:
                if lower(p) == lower(name):
                    taken = p
            if taken != "":
                print(taken + " is already on the list.")
            else:
                players = players + [name]
                if len(players) == most_players:
                    print("That makes " + str(most_players) + ", the most.")
                    reading = False
    return players

def ask_match(prompt, allowed):
    while True:
        answer = trim(input(prompt))
        if answer == "":
            return -1
        if answer[0] == "M" or answer[0] == "m":
            answer = answer[1:len(answer)]
        m = number(answer) - 1
        for a in allowed:
            if a == m:
                return m
        print("Type one of the matches listed.")

def ask_score(first):
    while True:
        answer = trim(input("score, " + first + " first (like 3-1)> "))
        if answer == "":
            return []
        score = parse_score(answer)
        if len(score) == 0:
            print("Type the score as two numbers from 0 to " + str(highest_score) + ", like 3-1.")
        elif score[0] == score[1]:
            print("A knockout match needs a winner: give the score after extra time or penalties.")
        else:
            return score

def result_text(players, sides, score):
    if score[0] > score[1]:
        return players[sides[0]] + " beats " + players[sides[1]] + " " + str(score[0]) + "-" + str(score[1])
    return players[sides[1]] + " beats " + players[sides[0]] + " " + str(score[1]) + "-" + str(score[0])

def with_score(scores, m, score):
    out = scores[0:m] + [score] + scores[m + 1:len(scores)]
    return out

def enter(players, size, scores):
    free = bracket.open_matches(len(players), size, scores)
    if len(free) == 0:
        print("The tournament is over: " + players[bracket.champion(len(players), size, scores)] + " is the champion.")
        return scores
    print("Open matches:")
    for m in free:
        print(view.match_line(players, size, scores, m))
    m = ask_match("match> ", free)
    if m == -1:
        print("Cancelled.")
        return scores
    sides = bracket.pairs(len(players), size, scores)[0][m]
    score = ask_score(players[sides[0]])
    if len(score) == 0:
        print("Cancelled.")
        return scores
    scores = with_score(scores, m, score)
    print("M" + str(m + 1) + ": " + result_text(players, sides, score) + ".")
    c = bracket.champion(len(players), size, scores)
    if c >= 0:
        print(players[c] + " is the champion!")
    return scores

def correct(players, size, scores):
    fixable = bracket.correctable(size, scores)
    if len(fixable) == 0:
        print("No result can be corrected now.")
        return scores
    print("Results that can still be corrected:")
    for m in fixable:
        print(view.match_line(players, size, scores, m))
    m = ask_match("match> ", fixable)
    if m == -1:
        print("Cancelled.")
        return scores
    n = len(players)
    pw = bracket.pairs(n, size, scores)
    sides = pw[0][m]
    before = pw[1][m]
    score = ask_score(players[sides[0]])
    if len(score) == 0:
        print("Cancelled.")
        return scores
    scores = with_score(scores, m, score)
    print("M" + str(m + 1) + " corrected: " + result_text(players, sides, score) + ".")
    after = bracket.pairs(n, size, scores)[1][m]
    if after != before:
        going = bracket.next_match(size, m)
        if going == -1:
            print(players[after] + " is the champion now.")
        else:
            print(players[after] + " takes " + players[before] + "'s place in M" + str(going + 1) + ".")
    return scores

print("== Tournament bracket ==")
print("Type the players from the strongest down - seed 1 first - one name a line;")
print("an empty line ends the list (2 to 16 players).")
players = read_players()
n = len(players)
size = bracket.size_for(n)
byes = size - n
if byes == 0:
    print(str(n) + " players: a bracket of " + str(size) + ", no byes.")
elif byes == 1:
    print(str(n) + " players: a bracket of " + str(size) + ", with a bye for seed 1.")
elif byes == 2:
    print(str(n) + " players: a bracket of " + str(size) + ", with byes for seeds 1 and 2.")
else:
    print(str(n) + " players: a bracket of " + str(size) + ", with byes for seeds 1 to " + str(byes) + ".")
scores = [[] for m in range(1, size)]
view.show(players, size, scores)
running = True
while running:
    print("")
    print("1) show the bracket  2) enter a result  3) correct a result  4) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        view.show(players, size, scores)
    elif choice == "2":
        scores = enter(players, size, scores)
    elif choice == "3":
        scores = correct(players, size, scores)
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")

bracket.eml

eml
# P034 tournament bracket - the draw and who goes through. Players are
# numbered by seed, 0 for seed 1. A slot holds a player's number, -1 for a
# bye or -2 while it waits for the winner of an earlier match. Matches are
# numbered through the rounds: in a bracket of 8, M1-M4 are the
# quarter-finals, M5-M6 the semi-finals and M7 the final. A score is
# [first side, second side], or [] before the match is played.

def size_for(n):
    # The smallest power of two that holds n players.
    1 => size
    while size < n:
        size * 2 => size
    return size

def seed_order(size):
    # Seeds by position in the bracket, so that seeds 1 and 2 can only meet
    # in the final, 1 to 4 only from the semi-finals on, and so on: each
    # doubling puts seed s next to the seed that makes the pair add up to
    # the new size plus one. 2 -> [1, 2]; 4 -> [1, 4, 2, 3];
    # 8 -> [1, 8, 4, 5, 2, 7, 3, 6].
    [1] => order
    while len(order) < size:
        2 * len(order) + 1 => total
        [] => grown
        for s in order:
            grown + [s, total - s] => grown
        grown => order
    return order

def rounds(size):
    # [first match, number of matches] for each round.
    [] => out
    0 => first
    int(size / 2) => count
    while count >= 1:
        out + [[first, count]] => out
        first + count => first
        int(count / 2) => count
    return out

def decide(a, b, score):
    # The winner of one match, or -2 while it is not decided.
    if a == -1:
        return b
    if b == -1:
        return a
    if a < 0 or b < 0 or len(score) == 0:
        return -2
    if score[0] > score[1]:
        return a
    return b

def pairs(n, size, scores):
    # [first side, second side] of every match, and its winner:
    # returns [sides, winners].
    seed_order(size) => order
    rounds(size) => rs
    [] => sides
    [] => winners
    for i in [0:rs[0][1] - 1]:
        [order[2 * i] - 1, order[2 * i + 1] - 1] => pair
        for k in [0:1]:
            if pair[k] >= n:
                -1 => pair[k]
        sides + [pair] => sides
        winners + [decide(pair[0], pair[1], scores[i])] => winners
    for r in [1:len(rs) - 1]:
        rs[r - 1][0] => before
        for i in [0:rs[r][1] - 1]:
            [winners[before + 2 * i], winners[before + 2 * i + 1]] => pair
            sides + [pair] => sides
            winners + [decide(pair[0], pair[1], scores[rs[r][0] + i])] => winners
    return [sides, winners]

def feeders(size, m):
    # The two matches whose winners meet in match m, or [] for the first round.
    rounds(size) => rs
    for r in [1:len(rs) - 1]:
        if m >= rs[r][0] and m < rs[r][0] + rs[r][1]:
            rs[r - 1][0] + 2 * (m - rs[r][0]) => f
            return [f, f + 1]
    return []

def next_match(size, m):
    # The match the winner of m goes on to, or -1 after the final.
    rounds(size) => rs
    for r in [0:len(rs) - 2]:
        if m >= rs[r][0] and m < rs[r][0] + rs[r][1]:
            return rs[r + 1][0] + int((m - rs[r][0]) / 2)
    return -1

def open_matches(n, size, scores):
    # Matches with two players known and no score yet.
    pairs(n, size, scores)[0] => sides
    [] => out
    for m in [0:size - 2]:
        if sides[m][0] >= 0 and sides[m][1] >= 0 and len(scores[m]) == 0:
            out + [m] => out
    return out

def correctable(size, scores):
    # Played matches whose result can still change: the winner's next match
    # has not been played yet (or it was the final).
    [] => out
    for m in [0:size - 2]:
        if len(scores[m]) > 0:
            next_match(size, m) => after
            if after == -1 or len(scores[after]) == 0:
                out + [m] => out
    return out

def champion(n, size, scores):
    # The winner of the final, or -2 before it is played.
    return pairs(n, size, scores)[1][size - 2]
Python projection (bracket.py)
def size_for(n):
    size = 1
    while size < n:
        size = size * 2
    return size

def seed_order(size):
    order = [1]
    while len(order) < size:
        total = 2 * len(order) + 1
        grown = []
        for s in order:
            grown = grown + [s, total - s]
        order = grown
    return order

def rounds(size):
    out = []
    first = 0
    count = int(size / 2)
    while count >= 1:
        out = out + [[first, count]]
        first = first + count
        count = int(count / 2)
    return out

def decide(a, b, score):
    if a == -1:
        return b
    if b == -1:
        return a
    if a < 0 or b < 0 or len(score) == 0:
        return -2
    if score[0] > score[1]:
        return a
    return b

def pairs(n, size, scores):
    order = seed_order(size)
    rs = rounds(size)
    sides = []
    winners = []
    for i in range(0, rs[0][1]):
        pair = [order[2 * i] - 1, order[2 * i + 1] - 1]
        for k in range(0, 2):
            if pair[k] >= n:
                pair[k] = -1
        sides = sides + [pair]
        winners = winners + [decide(pair[0], pair[1], scores[i])]
    for r in range(1, len(rs)):
        before = rs[r - 1][0]
        for i in range(0, rs[r][1]):
            pair = [winners[before + 2 * i], winners[before + 2 * i + 1]]
            sides = sides + [pair]
            winners = winners + [decide(pair[0], pair[1], scores[rs[r][0] + i])]
    return [sides, winners]

def feeders(size, m):
    rs = rounds(size)
    for r in range(1, len(rs)):
        if m >= rs[r][0] and m < rs[r][0] + rs[r][1]:
            f = rs[r - 1][0] + 2 * (m - rs[r][0])
            return [f, f + 1]
    return []

def next_match(size, m):
    rs = rounds(size)
    for r in range(0, len(rs) - 2+1):
        if m >= rs[r][0] and m < rs[r][0] + rs[r][1]:
            return rs[r + 1][0] + int((m - rs[r][0]) / 2)
    return -1

def open_matches(n, size, scores):
    sides = pairs(n, size, scores)[0]
    out = []
    for m in range(0, size - 2+1):
        if sides[m][0] >= 0 and sides[m][1] >= 0 and len(scores[m]) == 0:
            out = out + [m]
    return out

def correctable(size, scores):
    out = []
    for m in range(0, size - 2+1):
        if len(scores[m]) > 0:
            after = next_match(size, m)
            if after == -1 or len(scores[after]) == 0:
                out = out + [m]
    return out

def champion(n, size, scores):
    return pairs(n, size, scores)[1][size - 2]

view.eml

eml
# P034 tournament bracket - the bracket on screen, round by round.
import bracket

def pad(s, width):
    while len(s) < width:
        s + " " => s
    return s

def without_trailing(s):
    len(s) => j
    while j > 0 and s[j - 1] == " ":
        j - 1 => j
    return s[0:j]

def round_name(players):
    if players == 2:
        return "Final"
    if players == 4:
        return "Semi-finals"
    if players == 8:
        return "Quarter-finals"
    return "Round of " + str(players)

def label(players, i):
    return "(" + str(i + 1) + ") " + players[i]

def score_text(score):
    return str(score[0]) + "-" + str(score[1])

def cell(players, size, m, k, side):
    # One side of match m: a player, a bye, or the match it waits for.
    if side >= 0:
        return label(players, side)
    if side == -1:
        return "bye"
    return "winner of M" + str(bracket.feeders(size, m)[k] + 1)

def show(players, size, scores):
    len(players) => n
    bracket.pairs(n, size, scores) => pw
    pw[0] => sides
    pw[1] => winners
    for r in bracket.rounds(size):
        round_name(2 * r[1]) ^0
        for m in [r[0]:r[0] + r[1] - 1]:
            sides[m] => s
            "v" => middle
            if s[1] == -1:
                "" => middle
            elif len(scores[m]) > 0:
                score_text(scores[m]) => middle
            ("  " + pad("M" + str(m + 1), 4) + pad(cell(players, size, m, 0, s[0]), 18) + pad(middle, 7) + pad(cell(players, size, m, 1, s[1]), 18)) => line
            winners[m] => w
            if w >= 0:
                line + "-> " + players[w] => line
                if s[0] >= 0 and s[1] >= 0 and w == max(s[0], s[1]):
                    line + " (upset)" => line
            without_trailing(line) ^0
    bracket.champion(n, size, scores) => c
    if c >= 0:
        ("Champion: " + label(players, c)) ^0

def match_line(players, size, scores, m):
    # "  M2  (4) Dee v (5) Eve", or with the score when it was played.
    bracket.pairs(len(players), size, scores)[0][m] => s
    " v " => middle
    if len(scores[m]) > 0:
        " " + score_text(scores[m]) + " " => middle
    return "  " + pad("M" + str(m + 1), 4) + label(players, s[0]) + middle + label(players, s[1])
Python projection (view.py)
import bracket

def pad(s, width):
    while len(s) < width:
        s = s + " "
    return s

def without_trailing(s):
    j = len(s)
    while j > 0 and s[j - 1] == " ":
        j = j - 1
    return s[0:j]

def round_name(players):
    if players == 2:
        return "Final"
    if players == 4:
        return "Semi-finals"
    if players == 8:
        return "Quarter-finals"
    return "Round of " + str(players)

def label(players, i):
    return "(" + str(i + 1) + ") " + players[i]

def score_text(score):
    return str(score[0]) + "-" + str(score[1])

def cell(players, size, m, k, side):
    if side >= 0:
        return label(players, side)
    if side == -1:
        return "bye"
    return "winner of M" + str(bracket.feeders(size, m)[k] + 1)

def show(players, size, scores):
    n = len(players)
    pw = bracket.pairs(n, size, scores)
    sides = pw[0]
    winners = pw[1]
    for r in bracket.rounds(size):
        print(round_name(2 * r[1]))
        for m in range(r[0], r[0] + r[1]):
            s = sides[m]
            middle = "v"
            if s[1] == -1:
                middle = ""
            elif len(scores[m]) > 0:
                middle = score_text(scores[m])
            line = "  " + pad("M" + str(m + 1), 4) + pad(cell(players, size, m, 0, s[0]), 18) + pad(middle, 7) + pad(cell(players, size, m, 1, s[1]), 18)
            w = winners[m]
            if w >= 0:
                line = line + "-> " + players[w]
                if s[0] >= 0 and s[1] >= 0 and w == max(s[0], s[1]):
                    line = line + " (upset)"
            print(without_trailing(line))
    c = bracket.champion(n, size, scores)
    if c >= 0:
        print("Champion: " + label(players, c))

def match_line(players, size, scores, m):
    s = bracket.pairs(len(players), size, scores)[0][m]
    middle = " v "
    if len(scores[m]) > 0:
        middle = " " + score_text(scores[m]) + " "
    return "  " + pad("M" + str(m + 1), 4) + label(players, s[0]) + middle + label(players, s[1])

Built on these corpus cases