update
This commit is contained in:
482
backend/state.go
Normal file
482
backend/state.go
Normal file
@@ -0,0 +1,482 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user