This commit is contained in:
2026-04-01 11:47:03 +04:00
parent cb68451c1c
commit 2465bc2ec3
43 changed files with 8210 additions and 0 deletions

482
backend/state.go Normal file
View 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
}
}