Home > Software engineering >  Even distribution algorithm
Even distribution algorithm

Time:10-17

I am organizing a tournament where 12 players are going to meet each other on 10 board games. I want each player to play at least one time with the 11 others players over the 10 board games. For example :

BoardGame1 - Match1 - Player1   Player2   Player3
BoardGame1 - Match2 - Player4   Player5   Player6
BoardGame1 - Match3 - Player7   Player8   Player9
BoardGame1 - Match4 - Player10   Player11   Player12
[...]    
BoardGame10 - Match1 - Player1   Player11   Player9
BoardGame10 - Match2 - Player4   Player2   Player12
BoardGame10 - Match3 - Player7   Player5   Player3
BoardGame10 - Match4 - Player10   Player8   Player6

How do you create an algorithm where you distribute the players evenly? I'd like to do it with TDD approach, so I need to predict the expected result (meaning no random distribution).

CodePudding user response:

If all players play each other exactly once, then the resulting object would be a Kirkman Triple System. There is no KTS with the parameters you want, but since each player has twenty opponent slots and only eleven potential opponents, it should be easy to find a suitable schedule.

The code below generates the (11 choose 2) × (8 choose 2) × (5 choose 2) = 15400 possibilities for one game and repeatedly greedily chooses the one that spreads the pairings in the fairest manner.

import collections
import itertools
import pprint


def partitions(players, k):
    players = set(players)
    assert len(players) % k == 0
    if len(players) == 0:
        yield []
    else:
        players = players.copy()
        leader = min(players)
        players.remove(leader)
        for comb in itertools.combinations(sorted(players), k - 1):
            group = {leader} | set(comb)
            for part in partitions(players - group, k):
                yield [group]   part


def update_pair_counts(pair_counts, game):
    for match in game:
        pair_counts.update(itertools.combinations(sorted(match), 2))


def evaluate_game(pair_counts, game):
    pair_counts = pair_counts.copy()
    update_pair_counts(pair_counts, game)
    objective = [0] * max(pair_counts.values())
    for count in pair_counts.values():
        objective[count - 1]  = 1
    total = 0
    for i in range(len(objective) - 1, -1, -1):
        total  = objective[i]
        objective[i] = total
    return objective


def schedule(n_groups, n_players_per_group, n_games):
    games = list(partitions(range(n_groups * n_players_per_group), n_players_per_group))
    pair_counts = collections.Counter()
    for i in range(n_games):
        game = max(games, key=lambda game: evaluate_game(pair_counts, game))
        yield game
        update_pair_counts(pair_counts, game)


def main():
    pair_counts = collections.Counter()
    for game in schedule(4, 3, 10):
        pprint.pprint(game)
        update_pair_counts(pair_counts, game)
    print()
    pprint.pprint(pair_counts)


if __name__ == "__main__":
    main()

Sample output:

[{0, 1, 2}, {3, 4, 5}, {8, 6, 7}, {9, 10, 11}]
[{0, 3, 6}, {1, 4, 9}, {2, 10, 7}, {8, 11, 5}]
[{0, 4, 7}, {3, 1, 11}, {8, 9, 2}, {10, 5, 6}]
[{0, 1, 5}, {2, 11, 6}, {9, 3, 7}, {8, 10, 4}]
[{0, 8, 3}, {1, 10, 6}, {2, 11, 4}, {9, 5, 7}]
[{0, 10, 11}, {8, 1, 7}, {2, 3, 5}, {9, 4, 6}]
[{0, 9, 2}, {1, 10, 3}, {8, 4, 5}, {11, 6, 7}]
[{0, 4, 6}, {1, 2, 5}, {10, 3, 7}, {8, 9, 11}]
[{0, 5, 7}, {1, 11, 4}, {8, 2, 10}, {9, 3, 6}]
[{0, 3, 11}, {8, 1, 6}, {2, 4, 7}, {9, 10, 5}]

Counter({(0, 3): 3,
         (0, 1): 2,
         (0, 2): 2,
         (1, 2): 2,
         (3, 5): 2,
         (4, 5): 2,
         (6, 7): 2,
         (6, 8): 2,
         (7, 8): 2,
         (9, 10): 2,
         (9, 11): 2,
         (10, 11): 2,
         (0, 6): 2,
         (3, 6): 2,
         (1, 4): 2,
         (4, 9): 2,
         (2, 7): 2,
         (2, 10): 2,
         (7, 10): 2,
         (5, 8): 2,
         (8, 11): 2,
         (0, 4): 2,
         (0, 7): 2,
         (4, 7): 2,
         (1, 3): 2,
         (1, 11): 2,
         (3, 11): 2,
         (2, 8): 2,
         (2, 9): 2,
         (8, 9): 2,
         (5, 10): 2,
         (6, 10): 2,
         (0, 5): 2,
         (1, 5): 2,
         (2, 11): 2,
         (6, 11): 2,
         (3, 7): 2,
         (3, 9): 2,
         (7, 9): 2,
         (4, 8): 2,
         (8, 10): 2,
         (1, 6): 2,
         (1, 10): 2,
         (2, 4): 2,
         (4, 11): 2,
         (5, 7): 2,
         (5, 9): 2,
         (0, 11): 2,
         (1, 8): 2,
         (2, 5): 2,
         (4, 6): 2,
         (6, 9): 2,
         (3, 10): 2,
         (3, 4): 1,
         (1, 9): 1,
         (5, 11): 1,
         (5, 6): 1,
         (2, 6): 1,
         (4, 10): 1,
         (0, 8): 1,
         (3, 8): 1,
         (0, 10): 1,
         (1, 7): 1,
         (2, 3): 1,
         (0, 9): 1,
         (7, 11): 1})

CodePudding user response:

How do you create an algorithm where you distribute the players evenly? I'd like to do it with TDD approach, so I need to predict the expected result (meaning no random distribution).

TDD tends to be successful when your problem is that you know what the computer should do, and you know how to make the computer do that, but you don't know the best way to write the code.

When you don't know how to make the computer do what you want, TDD is a lot harder. There are two typical approaches taken here.

The first is to perform a "spike" - sit down and hack things until you understand how to make the computer do what you want. The key feature of spikes is that you don't get to keep the code changes at the end; instead, you discard your spiked code, keep what you have learned in your head, and start over by writing the tests that you need.

The second approach is to sort of sneak up on it - you do TDD for the very simple cases that you do understand, and keep adding tests that are just a little bit harder than what you have already done. See Robert Martin's Craftsman series for an example of this approach.


For this problem, you might begin by first thinking of an interface that you might use for accessing the algorithm. For instance, you might consider a design that accepts as input a number of players and a number of games, and returns you a sequence of tuples, where each tuple represents a single match.

Typically, this version of the interface will look like general purpose data structures as inputs (in this example: numbers), and general purpose data structures as outputs (the list of tuples).

Most commonly, we'll verify the behavior in each test by figuring out what the answer should be for a given set of inputs, and asserting that the actual data structure exactly matches the expected. For a list of tuples, that would look something like:

assert len(expected) == len(actual)

for x in range(actual):
    assert len(expected[x]) == len(actual[x])
    for y in range(actual[x]):
        assert expected[x][y] == actual[x][y]

Although of course you could refactor that into something that looks nicer

assert expected == actual

Another possibility is to think about the properties that a solution should have, and verify that the actual result is consistent with those properties. Here, you seem to have two properties that are required for every solution:

  • Each pair of players should appear exactly once in the list of matches
  • Every player, boardgame pair should appear exactly once in the list of matches

In this case, the answer is easy enough to check (iterate through all of the matches, count each pair, assert count equals one).


The test themselves we introduce by starting with the easiest example we can think of. Here, that might be the case where we have 2 players and 1 board, and our answer should be

BoardGame1 - Match1 - Player1   Player2

And so we write that test (RED), and hard code this specific answer (GREEN), and then (REFACTOR) the code so that it is clear to the reader why this is the correct answer for these inputs.

And when you are happy with that code, you look for the next example - an example where the current implementation returns the wrong answer, but the change that you need to make to get it to return the write answer is small/easy.

Often, what will happen is that we "pass" the next test using a branch:

if special_case(inputs):
    return answer_for_special_case
else:
    # ... real implementation here ...
    return answer_for_general_case

And then refactor the code until the two blocks are the same, then finally remove the if clause.

It will sometimes happen that the new test is too big, and we can't figure out how to extend the algorithm to cover the new case. Usually the play is to revert any changes we've made (keeping the tests passing), and use what we have learned to find a different test that might be easier to introduce to the code.

And you keep iterating on this process until you have solved "all" of the problem.

  • Related