Files
cheesy-arena-lite/ranking.go
Patrick Fairbank abbca47a98 Updated data model for 2015 game.
Disabled some unit tests until logic is updated.
2015-01-19 19:04:51 -08:00

265 lines
7.2 KiB
Go

// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model and datastore CRUD methods for team ranking data at an event.
package main
import (
"math/rand"
"sort"
"strconv"
)
type Ranking struct {
TeamId int
Rank int
QualificationAverage float64
CoopertitionPoints int
AutoPoints int
ContainerPoints int
TotePoints int
LitterPoints int
Random float64
Disqualifications int
Played int
}
type Rankings []*Ranking
func (database *Database) CreateRanking(ranking *Ranking) error {
return database.rankingMap.Insert(ranking)
}
func (database *Database) GetRankingForTeam(teamId int) (*Ranking, error) {
ranking := new(Ranking)
err := database.rankingMap.Get(ranking, teamId)
if err != nil && err.Error() == "sql: no rows in result set" {
ranking = nil
err = nil
}
return ranking, err
}
func (database *Database) SaveRanking(ranking *Ranking) error {
_, err := database.rankingMap.Update(ranking)
return err
}
func (database *Database) DeleteRanking(ranking *Ranking) error {
_, err := database.rankingMap.Delete(ranking)
return err
}
func (database *Database) TruncateRankings() error {
return database.rankingMap.TruncateTables()
}
func (database *Database) GetAllRankings() ([]Ranking, error) {
var rankings []Ranking
err := database.rankingMap.Select(&rankings, "SELECT * FROM rankings ORDER BY rank")
return rankings, err
}
// Determines the rankings from the stored match results, and saves them to the database.
func (database *Database) CalculateRankings() error {
matches, err := database.GetMatchesByType("qualification")
if err != nil {
return err
}
rankings := make(map[int]*Ranking)
for _, match := range matches {
if match.Status != "complete" {
continue
}
matchResult, err := database.GetMatchResultForMatch(match.Id)
if err != nil {
return err
}
if !match.Red1IsSurrogate {
addMatchResultToRankings(rankings, match.Red1, matchResult, true)
}
if !match.Red2IsSurrogate {
addMatchResultToRankings(rankings, match.Red2, matchResult, true)
}
if !match.Red3IsSurrogate {
addMatchResultToRankings(rankings, match.Red3, matchResult, true)
}
if !match.Blue1IsSurrogate {
addMatchResultToRankings(rankings, match.Blue1, matchResult, false)
}
if !match.Blue2IsSurrogate {
addMatchResultToRankings(rankings, match.Blue2, matchResult, false)
}
if !match.Blue3IsSurrogate {
addMatchResultToRankings(rankings, match.Blue3, matchResult, false)
}
}
// Divide the total score by the number of matches played and truncate (floor) to two decimal places.
for _, ranking := range rankings {
ranking.QualificationAverage = float64(int(ranking.QualificationAverage*100/float64(ranking.Played))) / 100
}
sortedRankings := sortRankings(rankings)
// Stuff the rankings into the database in an atomic operation to prevent messing them up halfway.
transaction, err := database.rankingMap.Begin()
if err != nil {
return err
}
_, err = transaction.Exec("DELETE FROM rankings")
if err != nil {
return err
}
for rank, ranking := range sortedRankings {
ranking.Rank = rank + 1
err = transaction.Insert(ranking)
if err != nil {
return err
}
}
err = transaction.Commit()
if err != nil {
return err
}
return nil
}
// Checks all the match results for yellow and red cards, and updates the team model accordingly.
func (database *Database) CalculateTeamCards(matchType string) error {
teams, err := database.GetAllTeams()
if err != nil {
return err
}
teamsMap := make(map[string]Team)
for _, team := range teams {
team.YellowCard = false
teamsMap[strconv.Itoa(team.Id)] = team
}
matches, err := database.GetMatchesByType(matchType)
if err != nil {
return err
}
for _, match := range matches {
if match.Status != "complete" {
continue
}
matchResult, err := database.GetMatchResultForMatch(match.Id)
if err != nil {
return err
}
// Mark the team as having a yellow card if they got either a yellow or red in a previous match.
for teamId, card := range matchResult.RedCards {
if team, ok := teamsMap[teamId]; ok && card != "" {
team.YellowCard = true
teamsMap[teamId] = team
}
}
for teamId, card := range matchResult.BlueCards {
if team, ok := teamsMap[teamId]; ok && card != "" {
team.YellowCard = true
teamsMap[teamId] = team
}
}
}
// Save the teams to the database.
for _, team := range teamsMap {
err = db.SaveTeam(&team)
if err != nil {
return err
}
}
return nil
}
// Incrementally accounts for the given match result in the set of rankings that are being built.
func addMatchResultToRankings(rankings map[int]*Ranking, teamId int, matchResult *MatchResult, isRed bool) {
ranking := rankings[teamId]
if ranking == nil {
ranking = &Ranking{TeamId: teamId}
rankings[teamId] = ranking
}
ranking.Played += 1
// Don't award any points if the team was disqualified.
var cards map[string]string
if isRed {
cards = matchResult.RedCards
} else {
cards = matchResult.BlueCards
}
if card, ok := cards[strconv.Itoa(teamId)]; ok && card == "red" {
ranking.Disqualifications += 1
return
}
var score *ScoreSummary
if isRed {
score = matchResult.RedScoreSummary()
} else {
score = matchResult.BlueScoreSummary()
}
// Assign points.
ranking.QualificationAverage += float64(score.Score)
ranking.CoopertitionPoints += score.CoopertitionPoints
ranking.AutoPoints += score.AutoPoints
ranking.ContainerPoints += score.ContainerPoints
ranking.TotePoints += score.TotePoints
ranking.LitterPoints += score.LitterPoints
// Store a random value to be used as the last tiebreaker if necessary.
ranking.Random = rand.Float64()
}
func sortRankings(rankings map[int]*Ranking) Rankings {
var sortedRankings Rankings
for _, ranking := range rankings {
sortedRankings = append(sortedRankings, ranking)
}
sort.Sort(sortedRankings)
return sortedRankings
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Len() int {
return len(rankings)
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Less(i, j int) bool {
a := rankings[i]
b := rankings[j]
if a.QualificationAverage == b.QualificationAverage {
// Use cross-multiplication to keep it in integer math.
if a.CoopertitionPoints*b.Played == b.CoopertitionPoints*a.Played {
if a.AutoPoints*b.Played == b.AutoPoints*a.Played {
if a.ContainerPoints*b.Played == b.ContainerPoints*a.Played {
if a.TotePoints*b.Played == b.TotePoints*a.Played {
if a.LitterPoints*b.Played == b.LitterPoints*a.Played {
return a.Random > b.Random
}
return a.LitterPoints*b.Played > b.LitterPoints*a.Played
}
return a.TotePoints*b.Played > b.TotePoints*a.Played
}
return a.ContainerPoints*b.Played > b.ContainerPoints*a.Played
}
return a.AutoPoints*b.Played > b.AutoPoints*a.Played
}
return a.CoopertitionPoints*b.Played > b.CoopertitionPoints*a.Played
}
return a.QualificationAverage > b.QualificationAverage
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Swap(i, j int) {
rankings[i], rankings[j] = rankings[j], rankings[i]
}