483 lines
12 KiB
Go
483 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (a *App) readState(includeAllProofs bool) (StateResponse, error) {
|
|
players, err := a.readPlayers()
|
|
if err != nil {
|
|
return StateResponse{}, err
|
|
}
|
|
|
|
if err := a.ensureScoreRows(players); err != nil {
|
|
return StateResponse{}, err
|
|
}
|
|
|
|
settings, err := a.readSettings()
|
|
if err != nil {
|
|
return StateResponse{}, err
|
|
}
|
|
|
|
scoreMap, err := a.readScores(players)
|
|
if err != nil {
|
|
return StateResponse{}, err
|
|
}
|
|
|
|
includeScoreProofs := includeAllProofs || settings.ViewProofInView
|
|
var scoreProofs map[string]map[int]string
|
|
if includeScoreProofs {
|
|
scoreProofs, err = a.readScoreProofs(players)
|
|
if err != nil {
|
|
return StateResponse{}, err
|
|
}
|
|
}
|
|
|
|
derived := computeDerived(players, scoreMap)
|
|
|
|
response := StateResponse{
|
|
Competition: CompetitionMeta{
|
|
TitleAr: "بطولة دويتوايلر للرماية",
|
|
TitleEn: "Datwyler Shooting Event",
|
|
},
|
|
Players: players,
|
|
Scores: scoreMapToJSON(scoreMap),
|
|
Settings: settings,
|
|
Derived: derived,
|
|
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if includeScoreProofs {
|
|
response.ScoreProofs = scoreProofMapToJSON(scoreProofs)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (a *App) readPlayers() ([]Player, error) {
|
|
rows, err := a.db.Query(`
|
|
SELECT id, name_ar, name_en, group_code, image_data
|
|
FROM players
|
|
ORDER BY id ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query players: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
players := []Player{}
|
|
for rows.Next() {
|
|
var p Player
|
|
if err := rows.Scan(&p.ID, &p.NameAr, &p.NameEn, &p.GroupCode, &p.ImageData); err != nil {
|
|
return nil, fmt.Errorf("scan player: %w", err)
|
|
}
|
|
players = append(players, p)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate players: %w", err)
|
|
}
|
|
return players, nil
|
|
}
|
|
|
|
func (a *App) readScoreProofs(players []Player) (map[string]map[int]string, error) {
|
|
proofs := map[string]map[int]string{}
|
|
for _, stage := range scoreStages {
|
|
proofs[stage] = map[int]string{}
|
|
}
|
|
for _, p := range players {
|
|
for _, stage := range scoreStages {
|
|
proofs[stage][p.ID] = ""
|
|
}
|
|
}
|
|
|
|
rows, err := a.db.Query(`SELECT stage, player_id, image_data FROM score_attachments`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query score attachments: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var stage string
|
|
var playerID int
|
|
var imageData string
|
|
if err := rows.Scan(&stage, &playerID, &imageData); err != nil {
|
|
return nil, fmt.Errorf("scan score attachment: %w", err)
|
|
}
|
|
stage = strings.ToLower(strings.TrimSpace(stage))
|
|
stageMap, ok := proofs[stage]
|
|
if !ok {
|
|
continue
|
|
}
|
|
stageMap[playerID] = imageData
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate score attachments: %w", err)
|
|
}
|
|
return proofs, nil
|
|
}
|
|
|
|
func scoreProofMapToJSON(proofMap map[string]map[int]string) map[string]map[string]string {
|
|
out := map[string]map[string]string{}
|
|
for stage, stageMap := range proofMap {
|
|
out[stage] = map[string]string{}
|
|
for playerID, imageData := range stageMap {
|
|
if strings.TrimSpace(imageData) == "" {
|
|
continue
|
|
}
|
|
out[stage][strconv.Itoa(playerID)] = imageData
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *App) ensureScoreRows(players []Player) error {
|
|
if len(players) == 0 {
|
|
return nil
|
|
}
|
|
tx, err := a.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("begin score row ensure tx: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
for _, p := range players {
|
|
for _, stage := range scoreStages {
|
|
if _, err := tx.Exec(`INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?, ?, 0)`, stage, p.ID); err != nil {
|
|
return fmt.Errorf("ensure score row (%s,%d): %w", stage, p.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit score row ensure tx: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) readScores(players []Player) (map[string]map[int]int, error) {
|
|
scores := map[string]map[int]int{}
|
|
for _, stage := range scoreStages {
|
|
scores[stage] = map[int]int{}
|
|
}
|
|
for _, p := range players {
|
|
for _, stage := range scoreStages {
|
|
scores[stage][p.ID] = 0
|
|
}
|
|
}
|
|
|
|
rows, err := a.db.Query(`SELECT stage, player_id, score FROM scores`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query scores: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var stage string
|
|
var playerID int
|
|
var score int
|
|
if err := rows.Scan(&stage, &playerID, &score); err != nil {
|
|
return nil, fmt.Errorf("scan score: %w", err)
|
|
}
|
|
stage = strings.ToLower(stage)
|
|
if _, ok := scores[stage]; !ok {
|
|
continue
|
|
}
|
|
scores[stage][playerID] = score
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate scores: %w", err)
|
|
}
|
|
|
|
return scores, nil
|
|
}
|
|
|
|
func scoreMapToJSON(scoreMap map[string]map[int]int) map[string]map[string]int {
|
|
out := map[string]map[string]int{}
|
|
for stage, stageMap := range scoreMap {
|
|
out[stage] = map[string]int{}
|
|
for playerID, value := range stageMap {
|
|
out[stage][strconv.Itoa(playerID)] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func computeDerived(players []Player, scores map[string]map[int]int) DerivedState {
|
|
playerByID := map[int]Player{}
|
|
for _, p := range players {
|
|
playerByID[p.ID] = p
|
|
}
|
|
|
|
preRows := []RankingRow{}
|
|
for _, p := range players {
|
|
preRows = append(preRows, RankingRow{
|
|
PlayerID: p.ID,
|
|
NameAr: p.NameAr,
|
|
NameEn: p.NameEn,
|
|
GroupCode: p.GroupCode,
|
|
ImageData: p.ImageData,
|
|
Score: scores["preliminary"][p.ID],
|
|
TieBreak: scores["prelim_tiebreak"][p.ID],
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(preRows, func(i, j int) bool {
|
|
if preRows[i].Score != preRows[j].Score {
|
|
return preRows[i].Score > preRows[j].Score
|
|
}
|
|
return preRows[i].PlayerID < preRows[j].PlayerID
|
|
})
|
|
assignDenseRankByScore(preRows)
|
|
|
|
preTie := TieBreakInfo{Required: false, Resolved: true, Slots: 0, PlayerIDs: []int{}}
|
|
finalists := []RankingRow{}
|
|
|
|
if len(preRows) <= 12 {
|
|
for i := range preRows {
|
|
row := preRows[i]
|
|
row.Seed = i + 1
|
|
finalists = append(finalists, row)
|
|
}
|
|
} else {
|
|
cutoff := preRows[11].Score
|
|
above := []RankingRow{}
|
|
atCutoff := []RankingRow{}
|
|
for _, row := range preRows {
|
|
if row.Score > cutoff {
|
|
above = append(above, row)
|
|
} else if row.Score == cutoff {
|
|
atCutoff = append(atCutoff, row)
|
|
}
|
|
}
|
|
slots := 12 - len(above)
|
|
if slots < 0 {
|
|
slots = 0
|
|
}
|
|
|
|
if len(atCutoff) <= slots {
|
|
finalists = append(finalists, above...)
|
|
finalists = append(finalists, atCutoff...)
|
|
} else {
|
|
preTie.Required = true
|
|
preTie.Slots = slots
|
|
preTie.Resolved = true
|
|
for _, row := range atCutoff {
|
|
preTie.PlayerIDs = append(preTie.PlayerIDs, row.PlayerID)
|
|
}
|
|
sort.Ints(preTie.PlayerIDs)
|
|
|
|
sort.SliceStable(atCutoff, func(i, j int) bool {
|
|
if atCutoff[i].TieBreak != atCutoff[j].TieBreak {
|
|
return atCutoff[i].TieBreak > atCutoff[j].TieBreak
|
|
}
|
|
return atCutoff[i].PlayerID < atCutoff[j].PlayerID
|
|
})
|
|
|
|
if slots > 0 {
|
|
boundary := atCutoff[slots-1].TieBreak
|
|
greater := 0
|
|
equal := 0
|
|
for _, row := range atCutoff {
|
|
if row.TieBreak > boundary {
|
|
greater++
|
|
} else if row.TieBreak == boundary {
|
|
equal++
|
|
}
|
|
}
|
|
if greater < slots && greater+equal > slots {
|
|
preTie.Resolved = false
|
|
}
|
|
}
|
|
|
|
finalists = append(finalists, above...)
|
|
if slots > len(atCutoff) {
|
|
slots = len(atCutoff)
|
|
}
|
|
finalists = append(finalists, atCutoff[:slots]...)
|
|
}
|
|
|
|
sort.SliceStable(finalists, func(i, j int) bool {
|
|
if finalists[i].Score != finalists[j].Score {
|
|
return finalists[i].Score > finalists[j].Score
|
|
}
|
|
if preTie.Required {
|
|
if finalists[i].TieBreak != finalists[j].TieBreak {
|
|
return finalists[i].TieBreak > finalists[j].TieBreak
|
|
}
|
|
}
|
|
return finalists[i].PlayerID < finalists[j].PlayerID
|
|
})
|
|
assignDenseRankBy(finalists, func(a, b RankingRow) bool {
|
|
if a.Score != b.Score {
|
|
return false
|
|
}
|
|
if preTie.Required {
|
|
return a.TieBreak == b.TieBreak
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
for i := range finalists {
|
|
finalists[i].Seed = i + 1
|
|
if i < 6 {
|
|
finalists[i].FinalGroup = 1
|
|
} else {
|
|
finalists[i].FinalGroup = 2
|
|
}
|
|
}
|
|
|
|
finalGroup1 := []RankingRow{}
|
|
finalGroup2 := []RankingRow{}
|
|
for _, row := range finalists {
|
|
if row.FinalGroup == 1 {
|
|
finalGroup1 = append(finalGroup1, row)
|
|
} else {
|
|
finalGroup2 = append(finalGroup2, row)
|
|
}
|
|
}
|
|
|
|
finalRows := []RankingRow{}
|
|
for _, finalist := range finalists {
|
|
p := playerByID[finalist.PlayerID]
|
|
finalRows = append(finalRows, RankingRow{
|
|
PlayerID: finalist.PlayerID,
|
|
NameAr: p.NameAr,
|
|
NameEn: p.NameEn,
|
|
GroupCode: p.GroupCode,
|
|
ImageData: p.ImageData,
|
|
Score: scores["final"][finalist.PlayerID],
|
|
TieBreak: scores["final_tiebreak"][finalist.PlayerID],
|
|
Seed: finalist.Seed,
|
|
FinalGroup: finalist.FinalGroup,
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(finalRows, func(i, j int) bool {
|
|
if finalRows[i].Score != finalRows[j].Score {
|
|
return finalRows[i].Score > finalRows[j].Score
|
|
}
|
|
return finalRows[i].Seed < finalRows[j].Seed
|
|
})
|
|
|
|
finalTie := TieBreakInfo{Required: false, Resolved: true, Slots: 0, PlayerIDs: []int{}}
|
|
tiedTop := map[int]bool{}
|
|
if len(finalRows) > 0 {
|
|
i := 0
|
|
for i < len(finalRows) {
|
|
j := i + 1
|
|
for j < len(finalRows) && finalRows[j].Score == finalRows[i].Score {
|
|
j++
|
|
}
|
|
if j-i > 1 && finalRows[i].Score > 0 {
|
|
startPos := i + 1
|
|
endPos := j
|
|
if startPos <= 3 || endPos <= 3 || (startPos < 3 && endPos > 3) {
|
|
for k := i; k < j; k++ {
|
|
tiedTop[finalRows[k].PlayerID] = true
|
|
}
|
|
}
|
|
}
|
|
i = j
|
|
}
|
|
|
|
if len(tiedTop) > 0 {
|
|
finalTie.Required = true
|
|
for id := range tiedTop {
|
|
finalTie.PlayerIDs = append(finalTie.PlayerIDs, id)
|
|
}
|
|
sort.Ints(finalTie.PlayerIDs)
|
|
|
|
sort.SliceStable(finalRows, func(i, j int) bool {
|
|
if finalRows[i].Score != finalRows[j].Score {
|
|
return finalRows[i].Score > finalRows[j].Score
|
|
}
|
|
iti := tiedTop[finalRows[i].PlayerID]
|
|
itj := tiedTop[finalRows[j].PlayerID]
|
|
if iti && itj && finalRows[i].TieBreak != finalRows[j].TieBreak {
|
|
return finalRows[i].TieBreak > finalRows[j].TieBreak
|
|
}
|
|
return finalRows[i].Seed < finalRows[j].Seed
|
|
})
|
|
}
|
|
}
|
|
|
|
if finalTie.Required {
|
|
assignDenseRankBy(finalRows, func(a, b RankingRow) bool {
|
|
if a.Score != b.Score {
|
|
return false
|
|
}
|
|
if tiedTop[a.PlayerID] && tiedTop[b.PlayerID] {
|
|
return a.TieBreak == b.TieBreak
|
|
}
|
|
return true
|
|
})
|
|
if len(finalRows) >= 3 {
|
|
third := finalRows[2]
|
|
greater := 0
|
|
equal := 0
|
|
for _, row := range finalRows {
|
|
if row.Score > third.Score {
|
|
greater++
|
|
continue
|
|
}
|
|
if row.Score == third.Score {
|
|
itied := tiedTop[row.PlayerID]
|
|
ttied := tiedTop[third.PlayerID]
|
|
if itied && ttied {
|
|
if row.TieBreak > third.TieBreak {
|
|
greater++
|
|
} else if row.TieBreak == third.TieBreak {
|
|
equal++
|
|
}
|
|
} else {
|
|
equal++
|
|
}
|
|
}
|
|
}
|
|
if greater < 3 && greater+equal > 3 {
|
|
finalTie.Resolved = false
|
|
}
|
|
}
|
|
} else {
|
|
assignDenseRankByScore(finalRows)
|
|
}
|
|
|
|
podium := []RankingRow{}
|
|
for i := 0; i < len(finalRows) && i < 3; i++ {
|
|
podium = append(podium, finalRows[i])
|
|
}
|
|
|
|
return DerivedState{
|
|
PreliminaryRanking: RankingBundle{Rows: preRows, TieBreak: preTie, Unresolved: preTie.Required && !preTie.Resolved},
|
|
Finalists: finalists,
|
|
FinalGroups: FinalGroups{Group1: finalGroup1, Group2: finalGroup2},
|
|
FinalRanking: RankingBundle{Rows: finalRows, TieBreak: finalTie, Unresolved: finalTie.Required && !finalTie.Resolved},
|
|
Podium: podium,
|
|
}
|
|
}
|
|
|
|
func assignDenseRankByScore(rows []RankingRow) {
|
|
assignDenseRankBy(rows, func(a, b RankingRow) bool {
|
|
return a.Score == b.Score
|
|
})
|
|
}
|
|
|
|
func assignDenseRankBy(rows []RankingRow, isEqual func(a, b RankingRow) bool) {
|
|
if len(rows) == 0 {
|
|
return
|
|
}
|
|
|
|
currentRank := 1
|
|
rows[0].Rank = currentRank
|
|
for i := 1; i < len(rows); i++ {
|
|
if !isEqual(rows[i], rows[i-1]) {
|
|
currentRank = i + 1
|
|
}
|
|
rows[i].Rank = currentRank
|
|
}
|
|
}
|