Project P058

Teams and round robin

Split up to 16 rated players into two teams as even as possible - a backtracking search that drops any branch past half - or into three or four by a quick rule; then a league where everyone meets everyone once, scheduled by the circle method, with results entered as they come and a table sorted by points, goal difference and goals.

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

Split up to 16 rated players into teams of even strength, then play a league where every entrant meets every other once. The schedule comes from the circle method, results are entered as they come in, and the table follows the usual rules.

  • main.eml - the menu, players, teams, the league and its messages
  • teams.eml - splitting players into two teams by search, or into three or four by a quick rule
  • league.eml - the schedule and the table

How each part works:

  • Two teams are split as evenly as possible. One side's rating sum must not pass half the total and should come as close to it as it can. Backtracking finds it, as in the corpus case subset-sum-backtracking: each player goes in or stays out, and any branch whose sum has passed half is dropped at once. That is only safe because every rating is positive. With an odd count the lighter side may have fewer players or more, so both team sizes are searched.
  • Three or four teams use a quick rule instead: the strongest players first, each to the team with the fewest players and then the lowest sum. It is close, but not always the best split, and the program says so.
  • The schedule uses the circle method. Entrants sit around a circle with the first fixed, pairs face each other across it, and the others turn one place each round. Everyone meets everyone once in n - 1 rounds. With an odd count a rest takes the extra seat, so each entrant rests once and there are n rounds.
  • The table gives 3 points for a win and 1 for a draw. Ties are broken by goal difference, then goals scored, then name.

What is checked: menu choices 1 to 8; players as a name of up to 12 characters, a space and a rating from 1 to 100, each name once, at most 16; 2 to 4 teams with at least two players each; 3 to 8 league entrants, each name once; a round and a match that exist and are not a rest; scores 0 to 99 written like 2-1. An empty answer cancels.

Sessions: sessions/basic.in loads ten sample players. - Two teams of exactly 325 each. - Three teams by the quick rule, 34 apart, the four-player team the strongest. - A league of those three teams: one match a round, one team resting. - A league of five named teams, four results, the schedule with the scores, and the table.

sessions/bad-input.in gives: - menu choices 0 and x; - teams, a league, a result and the table before any exist; - player lines: "Ben", "Cy 0", "Dee 101", a long name, Ana twice, 5 and x; - two teams of four players, 127 against 131, the closest there is; - leagues of two entrants, and with A twice, then A, B, C; - rounds 0, 4 and x; match 3; the rest in round 1; and the scores "2-", a-b and 100-1; - the empty table.

Built on the verified corpus case subset-sum-backtracking (subsets that reach a target, with branches pruned once they overshoot).

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
== Teams and round robin ==
Split players into even teams, then play a league where everyone meets everyone once.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 0
Pick a number from 1 to 8.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> x
Pick a number from 1 to 8.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 2
Add at least 4 players first.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> 4
A league needs 3 to 8 entrants.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
No league yet.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 6
No league yet.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 1
One player a line, as a name and a rating from 1 to 100, like Ana 72; nothing to stop.
player> Ana 72
player> Ben
Type a name, a space and a rating from 1 to 100.
player> Cy 0
Type a name, a space and a rating from 1 to 100.
player> Dee 101
Type a name, a space and a rating from 1 to 100.
player> A very long name here 50
Keep names to 12 characters.
player> Ana 60
There is a player called Ana already.
player> Eve 55
player> Fay 61
player> 
3 players.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 2
Add at least 4 players first.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 1
One player a line, as a name and a rating from 1 to 100, like Ana 72; nothing to stop.
player> 5
Type a name, a space and a rating from 1 to 100.
player> x
Type a name, a space and a rating from 1 to 100.
player> 
3 players.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 1
One player a line, as a name and a rating from 1 to 100, like Ana 72; nothing to stop.
player> Gil 70
player> 
4 players.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 2
how many teams (2 to 4)> 2
The most even split there is, found by trying the choices and dropping those that pass half:
Team 1   127  Ana 72, Eve 55
Team 2   131  Fay 61, Gil 70
Strongest team minus weakest: 4.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> A, B
A league needs 3 to 8 entrants.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> A, B, A
A is there twice.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> A, B, C
3 entrants: 3 rounds, each with 1 match and one entrant resting.
Round 1
  1. A rests
  2. B            - C
Round 2
  1. C            - A
  2. B rests
Round 3
  1. A            - B
  2. C rests

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 3)> 0
Type a round from 1 to 3.
round (1 to 3)> 4
Type a round from 1 to 3.
round (1 to 3)> x
Type a round from 1 to 3.
round (1 to 3)> 
Cancelled.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 3)> 1
match (1 to 2)> 3
Type a match from 1 to 2.
match (1 to 2)> 
Cancelled.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 3)> 1
match (1 to 2)> 1
That one is a rest, with nothing to score.
match (1 to 2)> 2
B - C, score (like 2-1)> 2-
Type the two scores with - between, like 2-1.
B - C, score (like 2-1)> a-b
Type the two scores with - between, like 2-1.
B - C, score (like 2-1)> 100-1
Type the two scores with - between, like 2-1.
B - C, score (like 2-1)> 
Cancelled.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 6
     team          P  W  D  L   GF  GA   GD  Pts
  1. A             0  0  0  0    0   0    0    0
  2. B             0  0  0  0    0   0    0    0
  3. C             0  0  0  0    0   0    0    0
0 of 3 matches played. Win 3, draw 1; ties go to goal difference, then goals scored.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 8
Bye.
What was typed (52 lines)
0
x
2
3
4
5
6
1
Ana 72
Ben
Cy 0
Dee 101
A very long name here 50
Ana 60
Eve 55
Fay 61

2
1
5
x

1
Gil 70

2
2
3
A, B
3
A, B, A
3
A, B, C
5
0
4
x

5
1
3

5
1
1
2
2-
a-b
100-1

6
8

basic

interpreter: byte-equal
== Teams and round robin ==
Split players into even teams, then play a league where everyone meets everyone once.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 7
Ten sample players, rated 38 to 90.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 2
how many teams (2 to 4)> 2
The most even split there is, found by trying the choices and dropping those that pass half:
Team 1   325  Ana 72, Ben 55, Dev 64, Ivan 58, Jo 76
Team 2   325  Chloe 81, Ema 47, Finn 90, Gus 38, Hana 69
Strongest team minus weakest: 0.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 2
how many teams (2 to 4)> 3
Strongest first, each to the weakest team so far - close, though not always the best:
Team 1   239  Finn 90, Dev 64, Ema 47, Gus 38
Team 2   205  Chloe 81, Hana 69, Ben 55
Team 3   206  Jo 76, Ana 72, Ivan 58
Strongest team minus weakest: 34.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> 
3 entrants: 3 rounds, each with 1 match and one entrant resting.
Round 1
  1. Team 1 rests
  2. Team 2       - Team 3
Round 2
  1. Team 3       - Team 1
  2. Team 2 rests
Round 3
  1. Team 1       - Team 2
  2. Team 3 rests

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 3
entrants, separated by commas (nothing to use the teams)> Lions, Tigers, Bears, Wolves, Hawks
5 entrants: 5 rounds, each with 2 matches and one entrant resting.
Round 1
  1. Lions rests
  2. Tigers       - Hawks
  3. Bears        - Wolves
Round 2
  1. Hawks        - Lions
  2. Wolves rests
  3. Tigers       - Bears
Round 3
  1. Lions        - Wolves
  2. Hawks        - Bears
  3. Tigers rests
Round 4
  1. Bears        - Lions
  2. Wolves       - Tigers
  3. Hawks rests
Round 5
  1. Lions        - Tigers
  2. Bears rests
  3. Wolves       - Hawks

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 5)> 1
match (1 to 3)> 2
Tigers - Hawks, score (like 2-1)> 2-1
Round 1: Tigers 2-1 Hawks.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 5)> 1
match (1 to 3)> 3
Bears - Wolves, score (like 2-1)> 0-0
Round 1: Bears 0-0 Wolves.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 5)> 2
match (1 to 3)> 1
Hawks - Lions, score (like 2-1)> 3-1
Round 2: Hawks 3-1 Lions.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 5
round (1 to 5)> 2
match (1 to 3)> 3
Tigers - Bears, score (like 2-1)> 1-1
Round 2: Tigers 1-1 Bears.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 4
Round 1
  1. Lions rests
  2. Tigers       - Hawks         2-1
  3. Bears        - Wolves        0-0
Round 2
  1. Hawks        - Lions         3-1
  2. Wolves rests
  3. Tigers       - Bears         1-1
Round 3
  1. Lions        - Wolves
  2. Hawks        - Bears
  3. Tigers rests
Round 4
  1. Bears        - Lions
  2. Wolves       - Tigers
  3. Hawks rests
Round 5
  1. Lions        - Tigers
  2. Bears rests
  3. Wolves       - Hawks

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 6
     team          P  W  D  L   GF  GA   GD  Pts
  1. Tigers        2  1  1  0    3   2   +1    4
  2. Hawks         2  1  0  1    4   3   +1    3
  3. Bears         2  0  2  0    1   1    0    2
  4. Wolves        1  0  1  0    0   0    0    1
  5. Lions         1  0  0  1    1   3   -2    0
4 of 10 matches played. Win 3, draw 1; ties go to goal difference, then goals scored.

1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit
choice> 8
Bye.
What was typed (28 lines)
7
2
2
2
3
3

3
Lions, Tigers, Bears, Wolves, Hawks
5
1
2
2-1
5
1
3
0-0
5
2
1
3-1
5
2
3
1-1
4
6
8

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
# P058 teams and round robin: split players into teams of even strength,
# then play a league where everyone meets everyone once - a schedule by the
# circle method, results as they come in, and the table.
import teams
import league

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 number(s):
    if s == "" or len(s) > 6:
        return -1
    0 => n
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    return n

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

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

def add_players(state):
    "One player a line, as a name and a rating from 1 to 100, like Ana 72; nothing to stop." ^0
    while len(state[0]) < 16:
        trim(input("player> ")) => line
        if line == "":
            break
        len(line) - 1 => k
        while k > 0 and line[k] != " ":
            k - 1 => k
        trim(line[0:k]) => name
        number(line[k + 1:len(line)]) => rating
        False => taken
        for p in state[0]:
            if p[0] == name:
                True => taken
        if k <= 0 or name == "" or rating < 1 or rating > 100:
            "Type a name, a space and a rating from 1 to 100." ^0
        elif len(name) > 12:
            "Keep names to 12 characters." ^0
        elif taken:
            ("There is a player called " + name + " already.") ^0
        else:
            state[0] + [[name, rating]] => state[0]
    if len(state[0]) == 16:
        "16 players is the most." ^0
    (str(len(state[0])) + " players.") ^0

def show_team(label, members):
    "" => s
    for p in members:
        if s != "":
            s + ", " => s
        s + p[0] + " " + str(p[1]) => s
    (padded(label, 8) + right(str(teams.total(members)), 4) + "  " + s) ^0

def make_teams(state):
    if len(state[0]) < 4:
        "Add at least 4 players first." ^0
        return
    while True:
        trim(input("how many teams (2 to 4)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        number(answer) => k
        if k >= 2 and k <= 4 and k * 2 <= len(state[0]):
            break
        "Type 2, 3 or 4 - at least two players a team." ^0
    if k == 2:
        teams.two_teams(state[0]) => ts
        "The most even split there is, found by trying the choices and dropping those that pass half:" ^0
    else:
        teams.many_teams(state[0], k) => ts
        "Strongest first, each to the weakest team so far - close, though not always the best:" ^0
    ts => state[1]
    for t in [0:len(ts) - 1]:
        show_team("Team " + str(t + 1), ts[t])
    teams.total(ts[0]) => lo
    lo => hi
    for t in ts:
        if teams.total(t) < lo:
            teams.total(t) => lo
        if teams.total(t) > hi:
            teams.total(t) => hi
    ("Strongest team minus weakest: " + str(hi - lo) + ".") ^0

def start_league(state):
    trim(input("entrants, separated by commas (nothing to use the teams)> ")) => line
    [] => names
    if line == "":
        if len(state[1]) == 0:
            "There are no teams yet - make some, or type the entrants." ^0
            return
        for t in [1:len(state[1])]:
            names + ["Team " + str(t)] => names
    else:
        "" => word
        for c in line + ",":
            if c == ",":
                trim(word) => w
                if w != "":
                    names + [w] => names
                "" => word
            else:
                word + c => word
    for i in [0:len(names) - 1]:
        if len(names[i]) > 12:
            "Keep names to 12 characters." ^0
            return
        for j in [0:i - 1]:
            if names[j] == names[i]:
                (names[i] + " is there twice.") ^0
                return
    if len(names) < 3 or len(names) > 8:
        "A league needs 3 to 8 entrants." ^0
        return
    names => state[2]
    league.schedule(names) => state[3]
    {} => state[4]
    int(len(names) / 2) => per_round
    str(len(names)) + " entrants: " + str(len(state[3])) + " rounds, each with " + str(per_round) + " match" => s
    if per_round > 1:
        s + "es" => s
    if len(names) % 2 == 1:
        s + " and one entrant resting" => s
    (s + ".") ^0
    show_schedule(state)

def entrant(state, i):
    if i == -1:
        return "(rest)"
    return state[2][i]

def show_schedule(state):
    if len(state[2]) == 0:
        "No league yet." ^0
        return
    for r in [0:len(state[3]) - 1]:
        ("Round " + str(r + 1)) ^0
        for m in [0:len(state[3][r]) - 1]:
            state[3][r][m] => match
            "" => score
            r * 10 + m => key
            if key in state[4]:
                "  " + str(state[4][key][0]) + "-" + str(state[4][key][1]) => score
            if match[0] == -1 or match[1] == -1:
                ("  " + str(m + 1) + ". " + entrant(state, match[0] + match[1] + 1) + " rests") ^0
            else:
                entrant(state, match[1]) => away
                if score != "":
                    padded(away, 12) + score => away
                ("  " + str(m + 1) + ". " + padded(entrant(state, match[0]), 12) + " - " + away) ^0

def enter_result(state):
    if len(state[2]) == 0:
        "No league yet." ^0
        return
    while True:
        trim(input("round (1 to " + str(len(state[3])) + ")> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        number(answer) => r
        if r >= 1 and r <= len(state[3]):
            break
        ("Type a round from 1 to " + str(len(state[3])) + ".") ^0
    state[3][r - 1] => matches
    while True:
        trim(input("match (1 to " + str(len(matches)) + ")> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        number(answer) => m
        if m >= 1 and m <= len(matches):
            if matches[m - 1][0] == -1 or matches[m - 1][1] == -1:
                "That one is a rest, with nothing to score." ^0
            else:
                break
        else:
            ("Type a match from 1 to " + str(len(matches)) + ".") ^0
    matches[m - 1] => match
    while True:
        trim(input(entrant(state, match[0]) + " - " + entrant(state, match[1]) + ", score (like 2-1)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        0 => k
        while k < len(answer) and answer[k] != "-":
            k + 1 => k
        number(trim(answer[0:k])) => a
        -1 => b
        if k < len(answer):
            number(trim(answer[k + 1:len(answer)])) => b
        if a >= 0 and b >= 0 and a <= 99 and b <= 99:
            break
        "Type the two scores with - between, like 2-1." ^0
    [a, b] => state[4][(r - 1) * 10 + m - 1]
    ("Round " + str(r) + ": " + entrant(state, match[0]) + " " + str(a) + "-" + str(b) + " " + entrant(state, match[1]) + ".") ^0

def table(state):
    if len(state[2]) == 0:
        "No league yet." ^0
        return
    [] => results
    for r in [0:len(state[3]) - 1]:
        for m in [0:len(state[3][r]) - 1]:
            r * 10 + m => key
            if key in state[4]:
                state[3][r][m] => match
                results + [[match[0], match[1], state[4][key][0], state[4][key][1]]] => results
    league.standings(state[2], results) => rows
    "     team          P  W  D  L   GF  GA   GD  Pts" ^0
    for i in [0:len(rows) - 1]:
        rows[i] => row
        str(row[5] - row[6]) => gd
        if row[5] - row[6] > 0:
            "+" + gd => gd
        (right(str(i + 1), 3) + ". " + padded(row[0], 12) + right(str(row[1]), 3) + right(str(row[2]), 3) + right(str(row[3]), 3) + right(str(row[4]), 3) + right(str(row[5]), 5) + right(str(row[6]), 4) + right(gd, 5) + right(str(row[7]), 5)) ^0
    (str(len(results)) + " of " + str(int(len(state[2]) * (len(state[2]) - 1) / 2)) + " matches played. Win 3, draw 1; ties go to goal difference, then goals scored.") ^0

def sample(state):
    [["Ana", 72], ["Ben", 55], ["Chloe", 81], ["Dev", 64], ["Ema", 47],
     ["Finn", 90], ["Gus", 38], ["Hana", 69], ["Ivan", 58], ["Jo", 76]] => state[0]
    [] => state[1]
    "Ten sample players, rated 38 to 90." ^0

"== Teams and round robin ==" ^0
"Split players into even teams, then play a league where everyone meets everyone once." ^0
# [players, teams, league entrants, schedule, results by round * 10 + match]
[[], [], [], [], {}] => state
True => running
while running:
    "" ^0
    "1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        add_players(state)
    elif choice == "2":
        make_teams(state)
    elif choice == "3":
        start_league(state)
    elif choice == "4":
        show_schedule(state)
    elif choice == "5":
        enter_result(state)
    elif choice == "6":
        table(state)
    elif choice == "7":
        sample(state)
    elif choice == "8":
        False => running
    else:
        "Pick a number from 1 to 8." ^0
"Bye." ^0
Python projection (main.py)
import teams
import league

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 number(s):
    if s == "" or len(s) > 6:
        return -1
    n = 0
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    return n

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

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

def add_players(state):
    print("One player a line, as a name and a rating from 1 to 100, like Ana 72; nothing to stop.")
    while len(state[0]) < 16:
        line = trim(input("player> "))
        if line == "":
            break
        k = len(line) - 1
        while k > 0 and line[k] != " ":
            k = k - 1
        name = trim(line[0:k])
        rating = number(line[k + 1:len(line)])
        taken = False
        for p in state[0]:
            if p[0] == name:
                taken = True
        if k <= 0 or name == "" or rating < 1 or rating > 100:
            print("Type a name, a space and a rating from 1 to 100.")
        elif len(name) > 12:
            print("Keep names to 12 characters.")
        elif taken:
            print("There is a player called " + name + " already.")
        else:
            state[0] = state[0] + [[name, rating]]
    if len(state[0]) == 16:
        print("16 players is the most.")
    print(str(len(state[0])) + " players.")

def show_team(label, members):
    s = ""
    for p in members:
        if s != "":
            s = s + ", "
        s = s + p[0] + " " + str(p[1])
    print(padded(label, 8) + right(str(teams.total(members)), 4) + "  " + s)

def make_teams(state):
    if len(state[0]) < 4:
        print("Add at least 4 players first.")
        return
    while True:
        answer = trim(input("how many teams (2 to 4)> "))
        if answer == "":
            print("Cancelled.")
            return
        k = number(answer)
        if k >= 2 and k <= 4 and k * 2 <= len(state[0]):
            break
        print("Type 2, 3 or 4 - at least two players a team.")
    if k == 2:
        ts = teams.two_teams(state[0])
        print("The most even split there is, found by trying the choices and dropping those that pass half:")
    else:
        ts = teams.many_teams(state[0], k)
        print("Strongest first, each to the weakest team so far - close, though not always the best:")
    state[1] = ts
    for t in range(0, len(ts)):
        show_team("Team " + str(t + 1), ts[t])
    lo = teams.total(ts[0])
    hi = lo
    for t in ts:
        if teams.total(t) < lo:
            lo = teams.total(t)
        if teams.total(t) > hi:
            hi = teams.total(t)
    print("Strongest team minus weakest: " + str(hi - lo) + ".")

def start_league(state):
    line = trim(input("entrants, separated by commas (nothing to use the teams)> "))
    names = []
    if line == "":
        if len(state[1]) == 0:
            print("There are no teams yet - make some, or type the entrants.")
            return
        for t in range(1, len(state[1])+1):
            names = names + ["Team " + str(t)]
    else:
        word = ""
        for c in line + ",":
            if c == ",":
                w = trim(word)
                if w != "":
                    names = names + [w]
                word = ""
            else:
                word = word + c
    for i in range(0, len(names)):
        if len(names[i]) > 12:
            print("Keep names to 12 characters.")
            return
        for j in range(0, i):
            if names[j] == names[i]:
                print(names[i] + " is there twice.")
                return
    if len(names) < 3 or len(names) > 8:
        print("A league needs 3 to 8 entrants.")
        return
    state[2] = names
    state[3] = league.schedule(names)
    state[4] = {}
    per_round = int(len(names) / 2)
    s = str(len(names)) + " entrants: " + str(len(state[3])) + " rounds, each with " + str(per_round) + " match"
    if per_round > 1:
        s = s + "es"
    if len(names) % 2 == 1:
        s = s + " and one entrant resting"
    print(s + ".")
    show_schedule(state)

def entrant(state, i):
    if i == -1:
        return "(rest)"
    return state[2][i]

def show_schedule(state):
    if len(state[2]) == 0:
        print("No league yet.")
        return
    for r in range(0, len(state[3])):
        print("Round " + str(r + 1))
        for m in range(0, len(state[3][r])):
            match = state[3][r][m]
            score = ""
            key = r * 10 + m
            if key in state[4]:
                score = "  " + str(state[4][key][0]) + "-" + str(state[4][key][1])
            if match[0] == -1 or match[1] == -1:
                print("  " + str(m + 1) + ". " + entrant(state, match[0] + match[1] + 1) + " rests")
            else:
                away = entrant(state, match[1])
                if score != "":
                    away = padded(away, 12) + score
                print("  " + str(m + 1) + ". " + padded(entrant(state, match[0]), 12) + " - " + away)

def enter_result(state):
    if len(state[2]) == 0:
        print("No league yet.")
        return
    while True:
        answer = trim(input("round (1 to " + str(len(state[3])) + ")> "))
        if answer == "":
            print("Cancelled.")
            return
        r = number(answer)
        if r >= 1 and r <= len(state[3]):
            break
        print("Type a round from 1 to " + str(len(state[3])) + ".")
    matches = state[3][r - 1]
    while True:
        answer = trim(input("match (1 to " + str(len(matches)) + ")> "))
        if answer == "":
            print("Cancelled.")
            return
        m = number(answer)
        if m >= 1 and m <= len(matches):
            if matches[m - 1][0] == -1 or matches[m - 1][1] == -1:
                print("That one is a rest, with nothing to score.")
            else:
                break
        else:
            print("Type a match from 1 to " + str(len(matches)) + ".")
    match = matches[m - 1]
    while True:
        answer = trim(input(entrant(state, match[0]) + " - " + entrant(state, match[1]) + ", score (like 2-1)> "))
        if answer == "":
            print("Cancelled.")
            return
        k = 0
        while k < len(answer) and answer[k] != "-":
            k = k + 1
        a = number(trim(answer[0:k]))
        b = -1
        if k < len(answer):
            b = number(trim(answer[k + 1:len(answer)]))
        if a >= 0 and b >= 0 and a <= 99 and b <= 99:
            break
        print("Type the two scores with - between, like 2-1.")
    state[4][(r - 1) * 10 + m - 1] = [a, b]
    print("Round " + str(r) + ": " + entrant(state, match[0]) + " " + str(a) + "-" + str(b) + " " + entrant(state, match[1]) + ".")

def table(state):
    if len(state[2]) == 0:
        print("No league yet.")
        return
    results = []
    for r in range(0, len(state[3])):
        for m in range(0, len(state[3][r])):
            key = r * 10 + m
            if key in state[4]:
                match = state[3][r][m]
                results = results + [[match[0], match[1], state[4][key][0], state[4][key][1]]]
    rows = league.standings(state[2], results)
    print("     team          P  W  D  L   GF  GA   GD  Pts")
    for i in range(0, len(rows)):
        row = rows[i]
        gd = str(row[5] - row[6])
        if row[5] - row[6] > 0:
            gd = "+" + gd
        print(right(str(i + 1), 3) + ". " + padded(row[0], 12) + right(str(row[1]), 3) + right(str(row[2]), 3) + right(str(row[3]), 3) + right(str(row[4]), 3) + right(str(row[5]), 5) + right(str(row[6]), 4) + right(gd, 5) + right(str(row[7]), 5))
    print(str(len(results)) + " of " + str(int(len(state[2]) * (len(state[2]) - 1) / 2)) + " matches played. Win 3, draw 1; ties go to goal difference, then goals scored.")

def sample(state):
    state[0] = [["Ana", 72], ["Ben", 55], ["Chloe", 81], ["Dev", 64], ["Ema", 47], ["Finn", 90], ["Gus", 38], ["Hana", 69], ["Ivan", 58], ["Jo", 76]]
    state[1] = []
    print("Ten sample players, rated 38 to 90.")

print("== Teams and round robin ==")
print("Split players into even teams, then play a league where everyone meets everyone once.")
state = [[], [], [], [], {}]
running = True
while running:
    print("")
    print("1) add players  2) teams  3) start a league  4) schedule  5) result  6) table  7) sample  8) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        add_players(state)
    elif choice == "2":
        make_teams(state)
    elif choice == "3":
        start_league(state)
    elif choice == "4":
        show_schedule(state)
    elif choice == "5":
        enter_result(state)
    elif choice == "6":
        table(state)
    elif choice == "7":
        sample(state)
    elif choice == "8":
        running = False
    else:
        print("Pick a number from 1 to 8.")
print("Bye.")

teams.eml

eml
# P058 teams and round robin - splitting players into teams of even
# strength. A player is [name, rating], rating 1 to 100.

def total(players):
    0 => s
    for p in players:
        s + p[1] => s
    return s

def best_half(ratings, size):
    # Which `size` of the ratings to put in the first team so that its sum
    # comes as close to half the total as it can without passing it - the
    # other team then has the same sum or a little more. Backtracking, as in
    # the corpus case subset-sum-backtracking: at each player branch on in
    # or out, and drop a branch once its sum has passed half, which only
    # works because every rating is positive. Returns [chosen indexes, sum].
    0 => all
    for r in ratings:
        all + r => all
    int(all / 2) => cap
    # best[0]: the chosen indexes so far, best[1]: their sum
    [[], -1] => best
    search(ratings, size, cap, 0, [], 0, best)
    return best

def search(ratings, size, cap, i, chosen, sum, best):
    if sum > cap:
        return
    if len(chosen) == size:
        if sum > best[1]:
            chosen => best[0]
            sum => best[1]
        return
    if i == len(ratings) or len(chosen) + len(ratings) - i < size:
        return
    if best[1] == cap:
        return
    search(ratings, size, cap, i + 1, chosen + [i], sum + ratings[i], best)
    search(ratings, size, cap, i + 1, chosen, sum, best)

def two_teams(players):
    # [team one, team two], as even as the ratings allow: team one is the
    # side whose sum is at most half, as close to half as it can be. With an
    # odd count that side may be the one with fewer players or the one with
    # more, so both sizes are searched.
    [] => ratings
    for p in players:
        ratings + [p[1]] => ratings
    best_half(ratings, int(len(players) / 2)) => b
    if len(players) % 2 == 1:
        best_half(ratings, int(len(players) / 2) + 1) => b2
        if b2[1] > b[1]:
            b2 => b
    [] => one
    [] => two
    for i in [0:len(players) - 1]:
        False => mine
        for j in b[0]:
            if j == i:
                True => mine
        if mine:
            one + [players[i]] => one
        else:
            two + [players[i]] => two
    return [one, two]

def many_teams(players, k):
    # k teams by a quick rule, not a search: strongest player first, each
    # to the team with the lowest sum so far among those with the fewest
    # players. Close, but not always the best split.
    players[0:len(players)] => order
    # sort by rating, highest first (insertion sort; equal ratings keep
    # their order)
    for i in [1:len(order) - 1]:
        order[i] => p
        i => j
        while j > 0 and order[j - 1][1] < p[1]:
            order[j - 1] => order[j]
            j - 1 => j
        p => order[j]
    [] => out
    for t in [1:k]:
        out + [[]] => out
    for p in order:
        0 => pick
        for t in [1:k - 1]:
            if len(out[t]) < len(out[pick]) or (len(out[t]) == len(out[pick]) and total(out[t]) < total(out[pick])):
                t => pick
        out[pick] + [p] => out[pick]
    return out
Python projection (teams.py)
def total(players):
    s = 0
    for p in players:
        s = s + p[1]
    return s

def best_half(ratings, size):
    all = 0
    for r in ratings:
        all = all + r
    cap = int(all / 2)
    best = [[], -1]
    search(ratings, size, cap, 0, [], 0, best)
    return best

def search(ratings, size, cap, i, chosen, sum, best):
    if sum > cap:
        return
    if len(chosen) == size:
        if sum > best[1]:
            best[0] = chosen
            best[1] = sum
        return
    if i == len(ratings) or len(chosen) + len(ratings) - i < size:
        return
    if best[1] == cap:
        return
    search(ratings, size, cap, i + 1, chosen + [i], sum + ratings[i], best)
    search(ratings, size, cap, i + 1, chosen, sum, best)

def two_teams(players):
    ratings = []
    for p in players:
        ratings = ratings + [p[1]]
    b = best_half(ratings, int(len(players) / 2))
    if len(players) % 2 == 1:
        b2 = best_half(ratings, int(len(players) / 2) + 1)
        if b2[1] > b[1]:
            b = b2
    one = []
    two = []
    for i in range(0, len(players)):
        mine = False
        for j in b[0]:
            if j == i:
                mine = True
        if mine:
            one = one + [players[i]]
        else:
            two = two + [players[i]]
    return [one, two]

def many_teams(players, k):
    order = players[0:len(players)]
    for i in range(1, len(order)):
        p = order[i]
        j = i
        while j > 0 and order[j - 1][1] < p[1]:
            order[j] = order[j - 1]
            j = j - 1
        order[j] = p
    out = []
    for t in range(1, k+1):
        out = out + [[]]
    for p in order:
        pick = 0
        for t in range(1, k):
            if len(out[t]) < len(out[pick]) or len(out[t]) == len(out[pick]) and total(out[t]) < total(out[pick]):
                pick = t
        out[pick] = out[pick] + [p]
    return out

league.eml

eml
# P058 teams and round robin - the schedule and the table.

def schedule(names):
    # The circle method: put the entrants round a circle with the first one
    # fixed in place, pair them across, and turn the others one step for
    # each round. With an odd count a "bye" joins the circle, and whoever
    # meets it rests that round. Everyone meets everyone once in n - 1
    # rounds. Returns rounds of matches [home, away] as indexes, -1 = bye.
    [] => idx
    for i in [0:len(names) - 1]:
        idx + [i] => idx
    if len(idx) % 2 == 1:
        idx + [-1] => idx
    len(idx) => n
    [] => rounds
    for r in [1:n - 1]:
        [] => matches
        for m in [0:int(n / 2) - 1]:
            idx[m] => a
            idx[n - 1 - m] => b
            # the fixed entrant switches home and away each round
            if m == 0 and r % 2 == 0:
                matches + [[b, a]] => matches
            else:
                matches + [[a, b]] => matches
        rounds + [matches] => rounds
        # turn: the last moves to second place, the rest shift along
        [idx[0], idx[n - 1]] + idx[1:n - 1] => idx
    return rounds

def standings(names, results):
    # results: [home, away, home goals, away goals] for each match played.
    # Rows [name, played, won, drawn, lost, for, against, points], sorted by
    # points, then goal difference, then goals scored, then name.
    [] => rows
    for nm in names:
        rows + [[nm, 0, 0, 0, 0, 0, 0, 0]] => rows
    for res in results:
        record(rows[res[0]], res[2], res[3])
        record(rows[res[1]], res[3], res[2])
    for i in [1:len(rows) - 1]:
        rows[i] => row
        i => j
        while j > 0 and ahead(row, rows[j - 1]):
            rows[j - 1] => rows[j]
            j - 1 => j
        row => rows[j]
    return rows

def record(row, scored, conceded):
    row[1] + 1 => row[1]
    row[5] + scored => row[5]
    row[6] + conceded => row[6]
    if scored > conceded:
        row[2] + 1 => row[2]
        row[7] + 3 => row[7]
    elif scored == conceded:
        row[3] + 1 => row[3]
        row[7] + 1 => row[7]
    else:
        row[4] + 1 => row[4]

def ahead(a, b):
    if a[7] != b[7]:
        return a[7] > b[7]
    if a[5] - a[6] != b[5] - b[6]:
        return a[5] - a[6] > b[5] - b[6]
    if a[5] != b[5]:
        return a[5] > b[5]
    return a[0] < b[0]
Python projection (league.py)
def schedule(names):
    idx = []
    for i in range(0, len(names)):
        idx = idx + [i]
    if len(idx) % 2 == 1:
        idx = idx + [-1]
    n = len(idx)
    rounds = []
    for r in range(1, n):
        matches = []
        for m in range(0, int(n / 2)):
            a = idx[m]
            b = idx[n - 1 - m]
            if m == 0 and r % 2 == 0:
                matches = matches + [[b, a]]
            else:
                matches = matches + [[a, b]]
        rounds = rounds + [matches]
        idx = [idx[0], idx[n - 1]] + idx[1:n - 1]
    return rounds

def standings(names, results):
    rows = []
    for nm in names:
        rows = rows + [[nm, 0, 0, 0, 0, 0, 0, 0]]
    for res in results:
        record(rows[res[0]], res[2], res[3])
        record(rows[res[1]], res[3], res[2])
    for i in range(1, len(rows)):
        row = rows[i]
        j = i
        while j > 0 and ahead(row, rows[j - 1]):
            rows[j] = rows[j - 1]
            j = j - 1
        rows[j] = row
    return rows

def record(row, scored, conceded):
    row[1] = row[1] + 1
    row[5] = row[5] + scored
    row[6] = row[6] + conceded
    if scored > conceded:
        row[2] = row[2] + 1
        row[7] = row[7] + 3
    elif scored == conceded:
        row[3] = row[3] + 1
        row[7] = row[7] + 1
    else:
        row[4] = row[4] + 1

def ahead(a, b):
    if a[7] != b[7]:
        return a[7] > b[7]
    if a[5] - a[6] != b[5] - b[6]:
        return a[5] - a[6] > b[5] - b[6]
    if a[5] != b[5]:
        return a[5] > b[5]
    return a[0] < b[0]

Built on these corpus cases