-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added basic tournament generator for match day basic round robin tour…
…naments
- Loading branch information
1 parent
220689f
commit ac9333f
Showing
6 changed files
with
196 additions
and
2 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
Sources/TournamentKit/Logic/Creation/MatchDayBasedRoundRobinTournamentGenerator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// | ||
// MatchDayBasedRoundRobinTournamentGenerator.swift | ||
// TournamentKit | ||
// | ||
// Created by Jan Wittler on 02.11.20. | ||
// Copyright © 2020 Jan Wittler. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/** | ||
A tournament generator which generates a round robin tournament schedule using match day based scheduling. This means that each match of a match day is assumed to be held in parallel such that each participation has equally long waiting times. | ||
|
||
Each generated match is a 1vs1 match. | ||
|
||
For one match day each participation is participating in one match. An exeption to this is an odd number of participations, in which case each match day another participation is pausing. | ||
*/ | ||
struct MatchDayBasedRoundRobinTournamentGenerator<MatchType: TournamentKit.MatchType>: TournamentGenerator { | ||
/// The match type for all matches of the generated tournament descriptions. | ||
let matchType: MatchType | ||
|
||
/** | ||
Initializes the generator with the given match type. | ||
- parameters: | ||
- matchType: The match type for all matches of the generated tournament descriptions. | ||
*/ | ||
init(matchType: MatchType) { | ||
self.matchType = matchType | ||
} | ||
|
||
func generateTournament<Participation: MatchParticipation>(participations: [Participation]) -> TournamentCreationDescription<MatchType, Participation> { | ||
precondition(participations.count >= 2, "invalid number of participations provided. At least 2 are required.") | ||
let allPairings = generateAllPairings(participations: participations) | ||
//TODO: normalize home <-> away pairings | ||
let matches = allPairings.map { day in | ||
day.shuffled().map { TournamentCreationDescription<MatchType, Participation>.MatchDescription(matchType: matchType, participations: [$0.0, $0.1]) } | ||
} | ||
return .init(matches: matches) | ||
} | ||
|
||
private func generateAllPairings<Participation: MatchParticipation>(participations: [Participation]) -> [[(Participation, Participation)]] { | ||
//using https://en.wikipedia.org/wiki/Round-robin_tournament#Circle_method | ||
var participations: [Participation?] = participations | ||
if participations.count % 2 == 1 { | ||
participations.insert(nil, at: 0) | ||
} | ||
var firstHalf = participations[0 ..< participations.count / 2] | ||
var secondHalf = participations[participations.count / 2 ..< participations.count] | ||
var result: [[(Participation, Participation)]] = [] | ||
for _ in 0 ..< participations.count - 1{ | ||
result.append(zip(firstHalf, secondHalf).compactMap { $0 == nil || $1 == nil ? nil : ($0!, $1!) }) | ||
firstHalf.insert(secondHalf.removeFirst(), at: 1) | ||
secondHalf.append(firstHalf.removeLast()) | ||
} | ||
return result | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
Sources/TournamentKit/Logic/Creation/TournamentCreationDescription.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// | ||
// TournamentCreationDescription.swift | ||
// TournamentKit | ||
// | ||
// Created by Jan Wittler on 27.10.20. | ||
// Copyright © 2020 Jan Wittler. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// An object to describe the structure of an arbitrary tournament. | ||
public struct TournamentCreationDescription<MatchType: TournamentKit.MatchType, Participation: MatchParticipation>: CustomStringConvertible { | ||
/// The matches of the tournament grouped by match day. | ||
public let matches: [[MatchDescription]] | ||
|
||
public struct MatchDescription: CustomStringConvertible { | ||
/// The type of the match. | ||
public let matchType: MatchType | ||
|
||
/// The participations of the match. | ||
public let participations: [Participation] | ||
|
||
public var description: String { | ||
return participations.map { $0.name }.joined(separator: " - ") | ||
} | ||
} | ||
|
||
public var description: String { | ||
return matches.enumerated().map { (index, matches) in | ||
return "\(index + 1)\n" + matches.map { " \($0)" }.joined(separator: "\n") | ||
}.joined(separator: "\n\n") | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
Sources/TournamentKit/Logic/Creation/TournamentGenerator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// | ||
// TournamentGenerator.swift | ||
// | ||
// | ||
// Created by Jan Wittler on 02.11.20. | ||
// | ||
|
||
import Foundation | ||
|
||
/// A tournament generator generates tournament descriptions using the provided participations. | ||
protocol TournamentGenerator { | ||
/** | ||
The associated `MatchType` type. | ||
- note: If possible, the implementing class should be generic over this type to support any provided match type. | ||
*/ | ||
associatedtype MatchType: TournamentKit.MatchType | ||
|
||
/** | ||
Generates a tournament description using the generator's strategy. | ||
- precondition: `\participations.count >= 2` | ||
- parameters: | ||
- participations: The participations to participate in the newly generated tournament description. | ||
- returns: Returns a newly generated tournament description. | ||
*/ | ||
func generateTournament<Participation: MatchParticipation>(participations: [Participation]) -> TournamentCreationDescription<MatchType, Participation> | ||
} |
73 changes: 73 additions & 0 deletions
73
...rnamentKitTests/Structs/CreationTests/MatchDayBasedRoundRobinTournamentCreatorTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// | ||
// MatchDayBasedRoundRobinTournamentCreatorTests.swift | ||
// TournamentKitTests | ||
// | ||
// Created by Jan Wittler on 02.11.20. | ||
// Copyright © 2020 Jan Wittler. All rights reserved. | ||
// | ||
|
||
import XCTest | ||
@testable import TournamentKit | ||
|
||
class MatchDayBasedRoundRobinTournamentCreatorTests: XCTestCase { | ||
var tournaments: [[Player] : TournamentCreationDescription<MatchType, Player>]! | ||
|
||
override func setUp() { | ||
let availablePlayers = (1...32).map { Player(stringLiteral: "\($0)") } | ||
tournaments = (2..<availablePlayers.count).reduce(into: [:]) { (result, playerCount) in | ||
let players = Array(availablePlayers[..<availablePlayers.index(availablePlayers.startIndex, offsetBy: playerCount)]) | ||
let creator = MatchDayBasedRoundRobinTournamentGenerator(matchType: MatchType.highestScore) | ||
result![players] = creator.generateTournament(participations: players) | ||
} | ||
} | ||
|
||
func testMatchDaysComplete() { | ||
tournaments.forEach { (players, tournament) in | ||
tournament.matches.forEach { matches in | ||
XCTAssert(matches.count == players.count / 2, "missing match") | ||
} | ||
} | ||
} | ||
|
||
func testEachPlayerMaxOncePerMatchDay() { | ||
tournaments.forEach { (players, tournament) in | ||
tournament.matches.forEach { matches in | ||
let allPlayers = matches.map { $0.participations }.joined() | ||
XCTAssert(Set(allPlayers).count == allPlayers.count) | ||
} | ||
} | ||
} | ||
|
||
func testEachPlayerAgainstEachOther() { | ||
tournaments.forEach { (players, tournament) in | ||
players.forEach { player in | ||
let opponents = tournament.matches.joined().map { $0.participations }.filter { $0.contains(player) }.joined().filter { $0 != player } | ||
XCTAssert(opponents.count == players.count - 1, "missing opponent") | ||
XCTAssert(Set(opponents).count == opponents.count, "duplicated opponent") | ||
XCTAssert(Set(players).intersection(opponents) == Set(opponents), "invalid opponent") | ||
} | ||
} | ||
} | ||
|
||
//TODO: not yet implemented | ||
func testEachPlayerEquallyHomeAndAway() { | ||
tournaments.forEach { (players, tournament) in | ||
players.forEach { player in | ||
let ownMatchIndexes = tournament.matches.joined().compactMap { $0.participations.firstIndex(of: player) } | ||
let groupedIndexCounts = Dictionary(grouping: ownMatchIndexes, by: { $0 }).mapValues { $0.count } | ||
let homeCount = groupedIndexCounts[0] ?? 0 | ||
let awayCount = groupedIndexCounts[1] ?? 0 | ||
XCTAssert(abs(homeCount - awayCount) <= (players.count % 2 == 0 ? 0 : 1), "invalid home / away balance") | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension MatchDayBasedRoundRobinTournamentCreatorTests { | ||
static var allTests = [ | ||
("testMatchDaysComplete", testMatchDaysComplete), | ||
("testEachPlayerMaxOncePerMatchDay", testEachPlayerMaxOncePerMatchDay), | ||
("testEachPlayerAgainstEachOther", testEachPlayerAgainstEachOther), | ||
("testEachPlayerEquallyHomeAndAway", testEachPlayerEquallyHomeAndAway) | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters