Skip to content

Commit

Permalink
added basic tournament generator for match day basic round robin tour…
Browse files Browse the repository at this point in the history
…naments
  • Loading branch information
JanWittler committed Nov 2, 2020
1 parent 220689f commit ac9333f
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 2 deletions.
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
}
}
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 Sources/TournamentKit/Logic/Creation/TournamentGenerator.swift
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>
}
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)
]
}
6 changes: 5 additions & 1 deletion Tests/TournamentKitTests/Structs/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
import Foundation
import TournamentKit

struct Player: TournamentKit.Player, ExpressibleByStringLiteral {
struct Player: TournamentKit.Player, ExpressibleByStringLiteral, Equatable, Hashable {
let name: String

init(stringLiteral value: StringLiteralType) {
self.name = String(value)
}
}

extension Player: CustomStringConvertible {
var description: String { return "Player \(name)"}
}
3 changes: 2 additions & 1 deletion Tests/TournamentKitTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import XCTest
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(TournamentKitTests.allTests),
testCase(TournamentKitWrongResultsTests.allTests)
testCase(TournamentKitWrongResultsTests.allTests),
testCase(MatchDayBasedRoundRobinTournamentCreatorTests.allTests)
]
}
#endif

0 comments on commit ac9333f

Please sign in to comment.