{
  "id": "P071",
  "slug": "leaderboard",
  "key": "P071-leaderboard",
  "title": "Leaderboard",
  "summary": "A small league: games give points and move Elo ratings, worked out in whole numbers with an exact 400th root; the standings rank players level on points the five usual ways - competition, modified competition, dense, ordinal and fractional.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P071 - Leaderboard\n\nA small league. A game gives 3 points for a win and 1 each for a draw,\nand moves both players' Elo ratings. The standings show the five usual\nways to rank players who are level on points. The program can also say\nwhat a game between two players is worth before it is played. It starts\nwith seven players, three of them level on 7 points.\n\n- `main.eml` - the menu, the standings table, recording games, and new\n  players\n- `ranking.eml` - the five rankings, and Elo in whole numbers\n\nHow each part works:\n\n- Rankings, for the scores 9, 7, 7, 7, 4, 4, 1:\n  - competition, \"1224\": 1 + how many scores are higher, as in the corpus\n    case `rank-with-ties` - 1, 2, 2, 2, 5, 5, 7;\n  - modified competition, \"1334\": how many scores are at least this one -\n    1, 4, 4, 4, 6, 6, 7;\n  - dense, \"1223\": 1 + how many different scores are higher, also from\n    the corpus case - 1, 2, 2, 2, 3, 3, 4;\n  - ordinal, \"1234\": level players in the order they joined;\n  - fractional: the average of the places level players share, from the\n    competition rank to the modified one - 1, 3, 3, 3, 5.5, 5.5, 7.\n- Elo: A's expected score against B is E = 1 / (1 + 10^((B - A)/400)),\n  and A's rating moves by K x (score - E) with K = 32, rounded to a whole\n  number, halves away from zero; B moves by the opposite.\n- 10^(d/400) is worked out without floats: d = 400 k + r, and\n  floor(10^(12 + r/400)) is the whole number y with\n  y^400 <= 10^(4800 + r) < (y + 1)^400, found by halving the range from\n  10^12 to 10^13. That is 12 correct digits. The expected score (shown to\n  a tenth of a percent) and the change are then exact divisions of whole\n  numbers. Against 60-digit decimals, every rating difference from -3000\n  to 3000, with every result, comes out the same; no change there comes\n  within 0.0001 of a half, far more than the method's error.\n\nWhat is checked: menu choices 1 to 5; players who are in the league, and\nnot the same player twice; a result of A, B or draw; a new name of\nletters, up to 10, not already used; a starting rating from 100 to 3000.\nAn empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the standings: Ben, Chloe and Dev level on 7, Eli and Fay on 4;\n- what Ana against Gus is worth: 78.0%, so a win gains only 7 and a loss\n  costs 25;\n- Dev beating Ana (+22 each way), and Ben drawing with Chloe (+1 to Ben,\n  the lower rated);\n- the new standings, Hana joining at 1500, and what Hana against Ana is\n  worth.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- an unknown player, a player against themselves, and the result \"win\",\n  then a cancel;\n- a name already taken and \"Ana Lee\"; ratings 50, 3001 and abc, then a\n  cancel;\n- a cancelled question.\n\nBuilt on the verified corpus case `rank-with-ties` (competition and dense\nranking by counting, so ties need no special case).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P071 leaderboard: a small league. Games give points (3 for a win, 1 each\n# for a draw) and move Elo ratings; the standings show the five usual ways\n# to rank players who are level on points.\nimport ranking\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef number(s):\n    # A whole number of up to 4 digits, or -1.\n    if len(s) == 0 or len(s) > 4:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef is_name(s):\n    if len(s) == 0 or len(s) > 10:\n        return False\n    for c in s:\n        if not ((c >= \"a\" and c <= \"z\") or (c >= \"A\" and c <= \"Z\")):\n            return False\n    return True\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef lpad(s, n):\n    while len(s) < n:\n        \" \" + s => s\n    return s\n\ndef find(players, name):\n    for i in [0:len(players) - 1]:\n        if players[i][0] == name:\n            return i\n    return -1\n\ndef signed(n):\n    if n > 0:\n        return \"+\" + str(n)\n    return str(n)\n\ndef percent(permille):\n    # 640 -> \"64.0%\"\n    0 => whole\n    permille => r\n    while r >= 10:\n        r - 10 => r\n        whole + 1 => whole\n    return str(whole) + \".\" + str(r) + \"%\"\n\ndef by_points(players):\n    # Indices in table order: most points first; level players keep the\n    # order they joined the league.\n    [0] * len(players) => out\n    for i in [0:len(players) - 1]:\n        i => out[i]\n    for i in [1:len(out) - 1]:\n        out[i] => x\n        i - 1 => j\n        while j >= 0 and players[out[j]][1] < players[x][1]:\n            out[j] => out[j + 1]\n            j - 1 => j\n        x => out[j + 1]\n    return out\n\ndef standings(players):\n    [0] * len(players) => pts\n    for i in [0:len(players) - 1]:\n        players[i][1] => pts[i]\n    \"player      points  rating   1224  1334  1223  1234  fractional\" ^0\n    for i in by_points(players):\n        (pad(players[i][0], 12) + lpad(str(players[i][1]), 6) + lpad(str(players[i][2]), 8) + lpad(str(ranking.competition(pts, i)), 7) + lpad(str(ranking.modified(pts, i)), 6) + lpad(str(ranking.dense(pts, i)), 6) + lpad(str(ranking.ordinal(pts, i)), 6) + lpad(ranking.fractional(pts, i), 12)) ^0\n    \"1224 competition: level players share the better rank, and the ranks after them are skipped.\" ^0\n    \"1334 modified competition: level players share the worse rank. 1223 dense: no rank is skipped.\" ^0\n    \"1234 ordinal: level players in the order they joined. Fractional: the average of the places they share.\" ^0\n\ndef ask_player(players, prompt):\n    while True:\n        trim(input(prompt)) => name\n        if name == \"\":\n            return -1\n        find(players, name) => i\n        if i >= 0:\n            return i\n        (\"Nobody called \" + name + \" is in the league.\") ^0\n\ndef game(players):\n    ask_player(players, \"player A> \") => a\n    if a == -1:\n        \"Cancelled.\" ^0\n        return players\n    while True:\n        ask_player(players, \"player B> \") => b\n        if b == -1:\n            \"Cancelled.\" ^0\n            return players\n        if b != a:\n            while True:\n                trim(input(\"result (A, B or draw)> \")) => r\n                if r == \"\":\n                    \"Cancelled.\" ^0\n                    return players\n                -1 => s2\n                if r == \"A\" or r == \"a\":\n                    2 => s2\n                elif r == \"B\" or r == \"b\":\n                    0 => s2\n                elif r == \"draw\":\n                    1 => s2\n                if s2 >= 0:\n                    players[a] => pa\n                    players[b] => pb\n                    ranking.elo(pa[2], pb[2], s2, 32) => e\n                    (\"Before the game \" + pa[0] + \" (\" + str(pa[2]) + \") was expected to score \" + percent(e[0]) + \" against \" + pb[0] + \" (\" + str(pb[2]) + \").\") ^0\n                    0 => ga\n                    0 => gb\n                    if s2 == 2:\n                        3 => ga\n                    elif s2 == 0:\n                        3 => gb\n                    else:\n                        1 => ga\n                        1 => gb\n                    [pa[0], pa[1] + ga, pa[2] + e[1]] => players[a]\n                    [pb[0], pb[1] + gb, pb[2] - e[1]] => players[b]\n                    (pa[0] + \" \" + signed(e[1]) + \" to \" + str(pa[2] + e[1]) + \", \" + pb[0] + \" \" + signed(0 - e[1]) + \" to \" + str(pb[2] - e[1]) + \".\") ^0\n                    return players\n                \"Type A, B or draw.\" ^0\n        \"A player cannot play themselves.\" ^0\n\ndef join(players):\n    while True:\n        trim(input(\"name> \")) => name\n        if name == \"\":\n            \"Cancelled.\" ^0\n            return players\n        if not is_name(name):\n            \"Type a name of letters, up to 10.\" ^0\n        elif find(players, name) >= 0:\n            (name + \" is in the league already.\") ^0\n        else:\n            while True:\n                trim(input(\"starting rating (100 to 3000)> \")) => s\n                if s == \"\":\n                    \"Cancelled.\" ^0\n                    return players\n                number(s) => r\n                if r >= 100 and r <= 3000:\n                    (name + \" joins on 0 points, rated \" + str(r) + \".\") ^0\n                    return players + [[name, 0, r]]\n                \"Type a whole number from 100 to 3000.\" ^0\n\ndef odds(players):\n    ask_player(players, \"player A> \") => a\n    if a == -1:\n        \"Cancelled.\" ^0\n        return\n    while True:\n        ask_player(players, \"player B> \") => b\n        if b == -1:\n            \"Cancelled.\" ^0\n            return\n        if b != a:\n            players[a] => pa\n            players[b] => pb\n            ranking.elo(pa[2], pb[2], 2, 32) => win\n            ranking.elo(pa[2], pb[2], 1, 32) => draw\n            ranking.elo(pa[2], pb[2], 0, 32) => loss\n            (pa[0] + \" (\" + str(pa[2]) + \") is expected to score \" + percent(win[0]) + \" against \" + pb[0] + \" (\" + str(pb[2]) + \").\") ^0\n            (\"For \" + pa[0] + \": a win \" + signed(win[1]) + \", a draw \" + signed(draw[1]) + \", a loss \" + signed(loss[1]) + \".\") ^0\n            return\n        \"A player cannot play themselves.\" ^0\n\n[\n    [\"Ana\", 9, 1620],\n    [\"Ben\", 7, 1555],\n    [\"Chloe\", 7, 1580],\n    [\"Dev\", 7, 1490],\n    [\"Eli\", 4, 1450],\n    [\"Fay\", 4, 1530],\n    [\"Gus\", 1, 1400],\n] => players\n\n\"== Leaderboard ==\" ^0\n\"A small league: 3 points for a win, 1 each for a draw, and Elo ratings with K = 32.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        standings(players)\n    elif choice == \"2\":\n        game(players) => players\n    elif choice == \"3\":\n        odds(players)\n    elif choice == \"4\":\n        join(players) => players\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import ranking\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef number(s):\n    if len(s) == 0 or len(s) > 4:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef is_name(s):\n    if len(s) == 0 or len(s) > 10:\n        return False\n    for c in s:\n        if not (c >= \"a\" and c <= \"z\" or c >= \"A\" and c <= \"Z\"):\n            return False\n    return True\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef lpad(s, n):\n    while len(s) < n:\n        s = \" \" + s\n    return s\n\ndef find(players, name):\n    for i in range(0, len(players)):\n        if players[i][0] == name:\n            return i\n    return -1\n\ndef signed(n):\n    if n > 0:\n        return \"+\" + str(n)\n    return str(n)\n\ndef percent(permille):\n    whole = 0\n    r = permille\n    while r >= 10:\n        r = r - 10\n        whole = whole + 1\n    return str(whole) + \".\" + str(r) + \"%\"\n\ndef by_points(players):\n    out = [0] * len(players)\n    for i in range(0, len(players)):\n        out[i] = i\n    for i in range(1, len(out)):\n        x = out[i]\n        j = i - 1\n        while j >= 0 and players[out[j]][1] < players[x][1]:\n            out[j + 1] = out[j]\n            j = j - 1\n        out[j + 1] = x\n    return out\n\ndef standings(players):\n    pts = [0] * len(players)\n    for i in range(0, len(players)):\n        pts[i] = players[i][1]\n    print(\"player      points  rating   1224  1334  1223  1234  fractional\")\n    for i in by_points(players):\n        print(pad(players[i][0], 12) + lpad(str(players[i][1]), 6) + lpad(str(players[i][2]), 8) + lpad(str(ranking.competition(pts, i)), 7) + lpad(str(ranking.modified(pts, i)), 6) + lpad(str(ranking.dense(pts, i)), 6) + lpad(str(ranking.ordinal(pts, i)), 6) + lpad(ranking.fractional(pts, i), 12))\n    print(\"1224 competition: level players share the better rank, and the ranks after them are skipped.\")\n    print(\"1334 modified competition: level players share the worse rank. 1223 dense: no rank is skipped.\")\n    print(\"1234 ordinal: level players in the order they joined. Fractional: the average of the places they share.\")\n\ndef ask_player(players, prompt):\n    while True:\n        name = trim(input(prompt))\n        if name == \"\":\n            return -1\n        i = find(players, name)\n        if i >= 0:\n            return i\n        print(\"Nobody called \" + name + \" is in the league.\")\n\ndef game(players):\n    a = ask_player(players, \"player A> \")\n    if a == -1:\n        print(\"Cancelled.\")\n        return players\n    while True:\n        b = ask_player(players, \"player B> \")\n        if b == -1:\n            print(\"Cancelled.\")\n            return players\n        if b != a:\n            while True:\n                r = trim(input(\"result (A, B or draw)> \"))\n                if r == \"\":\n                    print(\"Cancelled.\")\n                    return players\n                s2 = -1\n                if r == \"A\" or r == \"a\":\n                    s2 = 2\n                elif r == \"B\" or r == \"b\":\n                    s2 = 0\n                elif r == \"draw\":\n                    s2 = 1\n                if s2 >= 0:\n                    pa = players[a]\n                    pb = players[b]\n                    e = ranking.elo(pa[2], pb[2], s2, 32)\n                    print(\"Before the game \" + pa[0] + \" (\" + str(pa[2]) + \") was expected to score \" + percent(e[0]) + \" against \" + pb[0] + \" (\" + str(pb[2]) + \").\")\n                    ga = 0\n                    gb = 0\n                    if s2 == 2:\n                        ga = 3\n                    elif s2 == 0:\n                        gb = 3\n                    else:\n                        ga = 1\n                        gb = 1\n                    players[a] = [pa[0], pa[1] + ga, pa[2] + e[1]]\n                    players[b] = [pb[0], pb[1] + gb, pb[2] - e[1]]\n                    print(pa[0] + \" \" + signed(e[1]) + \" to \" + str(pa[2] + e[1]) + \", \" + pb[0] + \" \" + signed(0 - e[1]) + \" to \" + str(pb[2] - e[1]) + \".\")\n                    return players\n                print(\"Type A, B or draw.\")\n        print(\"A player cannot play themselves.\")\n\ndef join(players):\n    while True:\n        name = trim(input(\"name> \"))\n        if name == \"\":\n            print(\"Cancelled.\")\n            return players\n        if not is_name(name):\n            print(\"Type a name of letters, up to 10.\")\n        elif find(players, name) >= 0:\n            print(name + \" is in the league already.\")\n        else:\n            while True:\n                s = trim(input(\"starting rating (100 to 3000)> \"))\n                if s == \"\":\n                    print(\"Cancelled.\")\n                    return players\n                r = number(s)\n                if r >= 100 and r <= 3000:\n                    print(name + \" joins on 0 points, rated \" + str(r) + \".\")\n                    return players + [[name, 0, r]]\n                print(\"Type a whole number from 100 to 3000.\")\n\ndef odds(players):\n    a = ask_player(players, \"player A> \")\n    if a == -1:\n        print(\"Cancelled.\")\n        return\n    while True:\n        b = ask_player(players, \"player B> \")\n        if b == -1:\n            print(\"Cancelled.\")\n            return\n        if b != a:\n            pa = players[a]\n            pb = players[b]\n            win = ranking.elo(pa[2], pb[2], 2, 32)\n            draw = ranking.elo(pa[2], pb[2], 1, 32)\n            loss = ranking.elo(pa[2], pb[2], 0, 32)\n            print(pa[0] + \" (\" + str(pa[2]) + \") is expected to score \" + percent(win[0]) + \" against \" + pb[0] + \" (\" + str(pb[2]) + \").\")\n            print(\"For \" + pa[0] + \": a win \" + signed(win[1]) + \", a draw \" + signed(draw[1]) + \", a loss \" + signed(loss[1]) + \".\")\n            return\n        print(\"A player cannot play themselves.\")\n\nplayers = [[\"Ana\", 9, 1620], [\"Ben\", 7, 1555], [\"Chloe\", 7, 1580], [\"Dev\", 7, 1490], [\"Eli\", 4, 1450], [\"Fay\", 4, 1530], [\"Gus\", 1, 1400]]\nprint(\"== Leaderboard ==\")\nprint(\"A small league: 3 points for a win, 1 each for a draw, and Elo ratings with K = 32.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        standings(players)\n    elif choice == \"2\":\n        players = game(players)\n    elif choice == \"3\":\n        odds(players)\n    elif choice == \"4\":\n        players = join(players)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "ranking.eml",
      "eml": "# P071 leaderboard - ranking with ties five ways, and Elo ratings worked\n# out in whole numbers.\n\ndef competition(scores, i):\n    # Standard competition ranking, \"1224\": 1 + how many scores are higher\n    # (the corpus case rank-with-ties). Tied players share the better rank\n    # and the next rank is skipped.\n    1 => r\n    for s in scores:\n        if s > scores[i]:\n            r + 1 => r\n    return r\n\ndef modified(scores, i):\n    # Modified competition ranking, \"1334\": how many scores are at least\n    # this one, so tied players share the worse rank.\n    0 => r\n    for s in scores:\n        if s >= scores[i]:\n            r + 1 => r\n    return r\n\ndef dense(scores, i):\n    # Dense ranking, \"1223\": 1 + how many DIFFERENT scores are higher.\n    {} => seen\n    1 => r\n    for s in scores:\n        if s > scores[i] and not s in seen:\n            True => seen[s]\n            r + 1 => r\n    return r\n\ndef ordinal(scores, i):\n    # Ordinal ranking, \"1234\": everyone a different rank; between equal\n    # scores, whoever is earlier in the list comes first.\n    1 => r\n    for k in [0:len(scores) - 1]:\n        if scores[k] > scores[i] or (scores[k] == scores[i] and k < i):\n            r + 1 => r\n    return r\n\ndef fractional(scores, i):\n    # Fractional ranking, \"1 2.5 2.5 4\": tied players share the average of\n    # the ordinal ranks they cover - competition rank c to modified rank m,\n    # so (c + m) / 2. Returned as text, since it can end in .5.\n    competition(scores, i) + modified(scores, i) => t\n    0 => half\n    t => left\n    while left >= 2:\n        left - 2 => left\n        half + 1 => half\n    if left == 1:\n        return str(half) + \".5\"\n    return str(half)\n\ndef power(b, e):\n    # b ** e for whole numbers, by repeated squaring.\n    1 => out\n    while e > 0:\n        if e % 2 == 1:\n            out * b => out\n        b * b => b\n        # e is a few thousand at most, far below 2^53, so the float\n        # division is exact\n        int((e - e % 2) / 2) => e\n    return out\n\ndef tenth_power_scaled(r):\n    # floor(10^(12 + r/400)) for 0 <= r < 400, exactly: the whole number y\n    # with y^400 <= 10^(4800 + r) < (y + 1)^400, found by halving the range\n    # 10^12 to 10^13.\n    power(10, 4800 + r) => target\n    power(10, 12) => lo\n    power(10, 13) => hi\n    while hi - lo > 1:\n        # lo + hi < 2 x 10^13, below 2^53: halving it as a float is exact\n        int((lo + hi - (lo + hi) % 2) / 2) => mid\n        if power(mid, 400) <= target:\n            mid => lo\n        else:\n            mid => hi\n    return lo\n\ndef ten_to(d):\n    # 10^(d/400) x 10^12 for a whole d >= 0, to 12 digits: d = 400 k + r.\n    0 => k\n    d => r\n    while r >= 400:\n        r - 400 => r\n        k + 1 => k\n    return tenth_power_scaled(r) * power(10, k)\n\ndef rounded(n, d):\n    # n / d to the nearest whole number, halves away from zero (d > 0).\n    0 => q\n    if n < 0:\n        return 0 - rounded(0 - n, d)\n    2 * n + d => a\n    2 * d => b\n    while a >= b:\n        b => m\n        1 => k\n        while m + m <= a:\n            m + m => m\n            k + k => k\n        a - m => a\n        q + k => q\n    return q\n\ndef elo(ra, rb, score2, k):\n    # A rated ra plays B rated rb; score2 is A's score doubled (2 win, 1\n    # draw, 0 loss). A's expected score is E = 1 / (1 + 10^((rb - ra)/400)),\n    # and A's rating moves by k x (score - E), B's by the opposite.\n    # Returns [A's expected score in tenths of a percent, A's change].\n    if rb < ra:\n        elo(rb, ra, 2 - score2, k) => back\n        return [1000 - back[0], 0 - back[1]]\n    power(10, 12) => one\n    ten_to(rb - ra) => x\n    # E = one / (one + x); change = k (score2 / 2 - E)\n    rounded(1000 * one, one + x) => permille\n    rounded(k * (score2 * (one + x) - 2 * one), 2 * (one + x)) => change\n    return [permille, change]\n",
      "python": "def competition(scores, i):\n    r = 1\n    for s in scores:\n        if s > scores[i]:\n            r = r + 1\n    return r\n\ndef modified(scores, i):\n    r = 0\n    for s in scores:\n        if s >= scores[i]:\n            r = r + 1\n    return r\n\ndef dense(scores, i):\n    seen = {}\n    r = 1\n    for s in scores:\n        if s > scores[i] and not s in seen:\n            seen[s] = True\n            r = r + 1\n    return r\n\ndef ordinal(scores, i):\n    r = 1\n    for k in range(0, len(scores)):\n        if scores[k] > scores[i] or scores[k] == scores[i] and k < i:\n            r = r + 1\n    return r\n\ndef fractional(scores, i):\n    t = competition(scores, i) + modified(scores, i)\n    half = 0\n    left = t\n    while left >= 2:\n        left = left - 2\n        half = half + 1\n    if left == 1:\n        return str(half) + \".5\"\n    return str(half)\n\ndef power(b, e):\n    out = 1\n    while e > 0:\n        if e % 2 == 1:\n            out = out * b\n        b = b * b\n        e = int((e - e % 2) / 2)\n    return out\n\ndef tenth_power_scaled(r):\n    target = power(10, 4800 + r)\n    lo = power(10, 12)\n    hi = power(10, 13)\n    while hi - lo > 1:\n        mid = int((lo + hi - (lo + hi) % 2) / 2)\n        if power(mid, 400) <= target:\n            lo = mid\n        else:\n            hi = mid\n    return lo\n\ndef ten_to(d):\n    k = 0\n    r = d\n    while r >= 400:\n        r = r - 400\n        k = k + 1\n    return tenth_power_scaled(r) * power(10, k)\n\ndef rounded(n, d):\n    q = 0\n    if n < 0:\n        return 0 - rounded(0 - n, d)\n    a = 2 * n + d\n    b = 2 * d\n    while a >= b:\n        m = b\n        k = 1\n        while m + m <= a:\n            m = m + m\n            k = k + k\n        a = a - m\n        q = q + k\n    return q\n\ndef elo(ra, rb, score2, k):\n    if rb < ra:\n        back = elo(rb, ra, 2 - score2, k)\n        return [1000 - back[0], 0 - back[1]]\n    one = power(10, 12)\n    x = ten_to(rb - ra)\n    permille = rounded(1000 * one, one + x)\n    change = rounded(k * (score2 * (one + x) - 2 * one), 2 * (one + x))\n    return [permille, change]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\nZed\nAna\nAna\nBen\nwin\n\n4\nAna\nAna Lee\nKim\n50\n3001\nabc\n\n3\n\n5\n",
      "screen": "== Leaderboard ==\nA small league: 3 points for a win, 1 each for a draw, and Elo ratings with K = 32.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 2\nplayer A> Zed\nNobody called Zed is in the league.\nplayer A> Ana\nplayer B> Ana\nA player cannot play themselves.\nplayer B> Ben\nresult (A, B or draw)> win\nType A, B or draw.\nresult (A, B or draw)> \nCancelled.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 4\nname> Ana\nAna is in the league already.\nname> Ana Lee\nType a name of letters, up to 10.\nname> Kim\nstarting rating (100 to 3000)> 50\nType a whole number from 100 to 3000.\nstarting rating (100 to 3000)> 3001\nType a whole number from 100 to 3000.\nstarting rating (100 to 3000)> abc\nType a whole number from 100 to 3000.\nstarting rating (100 to 3000)> \nCancelled.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 3\nplayer A> \nCancelled.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n3\nAna\nGus\n2\nDev\nAna\nA\n2\nBen\nChloe\ndraw\n1\n4\nHana\n1500\n3\nHana\nAna\n5\n",
      "screen": "== Leaderboard ==\nA small league: 3 points for a win, 1 each for a draw, and Elo ratings with K = 32.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 1\nplayer      points  rating   1224  1334  1223  1234  fractional\nAna              9    1620      1     1     1     1           1\nBen              7    1555      2     4     2     2           3\nChloe            7    1580      2     4     2     3           3\nDev              7    1490      2     4     2     4           3\nEli              4    1450      5     6     3     5         5.5\nFay              4    1530      5     6     3     6         5.5\nGus              1    1400      7     7     4     7           7\n1224 competition: level players share the better rank, and the ranks after them are skipped.\n1334 modified competition: level players share the worse rank. 1223 dense: no rank is skipped.\n1234 ordinal: level players in the order they joined. Fractional: the average of the places they share.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 3\nplayer A> Ana\nplayer B> Gus\nAna (1620) is expected to score 78.0% against Gus (1400).\nFor Ana: a win +7, a draw -9, a loss -25.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 2\nplayer A> Dev\nplayer B> Ana\nresult (A, B or draw)> A\nBefore the game Dev (1490) was expected to score 32.1% against Ana (1620).\nDev +22 to 1512, Ana -22 to 1598.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 2\nplayer A> Ben\nplayer B> Chloe\nresult (A, B or draw)> draw\nBefore the game Ben (1555) was expected to score 46.4% against Chloe (1580).\nBen +1 to 1556, Chloe -1 to 1579.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 1\nplayer      points  rating   1224  1334  1223  1234  fractional\nDev             10    1512      1     1     1     1           1\nAna              9    1598      2     2     2     2           2\nBen              8    1556      3     4     3     3         3.5\nChloe            8    1579      3     4     3     4         3.5\nEli              4    1450      5     6     4     5         5.5\nFay              4    1530      5     6     4     6         5.5\nGus              1    1400      7     7     5     7           7\n1224 competition: level players share the better rank, and the ranks after them are skipped.\n1334 modified competition: level players share the worse rank. 1223 dense: no rank is skipped.\n1234 ordinal: level players in the order they joined. Fractional: the average of the places they share.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 4\nname> Hana\nstarting rating (100 to 3000)> 1500\nHana joins on 0 points, rated 1500.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 3\nplayer A> Hana\nplayer B> Ana\nHana (1500) is expected to score 36.3% against Ana (1598).\nFor Hana: a win +20, a draw +4, a loss -12.\n\n1) standings  2) record a game  3) what a game is worth  4) new player  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "rank-with-ties",
      "caseId": "122-rank-with-ties",
      "title": "Ranking with ties"
    }
  ],
  "updated": "2026-10-14"
}
