Files
shooting-event/backend/state.go

729 lines
19 KiB
Go
Raw Normal View History

2026-04-01 11:47:03 +04:00
package main
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
)
2026-05-18 13:22:11 +04:00
const (
positionBoardPreliminary = "preliminary"
positionBoardFinal = "final"
positionBoardPrelimTie = "prelim_tiebreak"
positionBoardFinalTie = "final_tiebreak"
positionBoardUnassigned = "UNASSIGNED"
)
var allowedPositionBoards = map[string]bool{
positionBoardPreliminary: true,
positionBoardFinal: true,
positionBoardPrelimTie: true,
positionBoardFinalTie: true,
}
2026-04-01 11:47:03 +04:00
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)
2026-05-18 13:22:11 +04:00
positionBoards, err := a.readPositionBoards(players, derived)
if err != nil {
return StateResponse{}, err
}
currentStage, err := a.readCurrentStage()
if err != nil {
return StateResponse{}, err
}
lastStage, err := a.readLastStage()
if err != nil {
return StateResponse{}, err
}
2026-04-01 11:47:03 +04:00
response := StateResponse{
Competition: CompetitionMeta{
TitleAr: "بطولة دويتوايلر للرماية",
TitleEn: "Datwyler Shooting Event",
},
2026-05-18 13:22:11 +04:00
Players: players,
Scores: scoreMapToJSON(scoreMap),
PositionBoards: positionBoardMapToJSON(positionBoards),
Settings: settings,
Derived: derived,
CurrentStage: currentStage,
LastStage: lastStage,
ServerTime: time.Now().UTC().Format(time.RFC3339),
2026-04-01 11:47:03 +04:00
}
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
}
2026-05-18 13:22:11 +04:00
func positionBoardMapToJSON(positionMap map[string]map[int]PositionSlot) map[string]map[string]PositionSlot {
out := map[string]map[string]PositionSlot{}
for board := range allowedPositionBoards {
out[board] = map[string]PositionSlot{}
}
for board, boardMap := range positionMap {
if _, ok := out[board]; !ok {
out[board] = map[string]PositionSlot{}
}
for playerID, slot := range boardMap {
out[board][strconv.Itoa(playerID)] = slot
}
}
return out
}
func (a *App) readPositionBoards(players []Player, derived DerivedState) (map[string]map[int]PositionSlot, error) {
defaults := defaultPositionBoards(players, derived)
merged := map[string]map[int]PositionSlot{}
for board, boardMap := range defaults {
merged[board] = map[int]PositionSlot{}
for playerID, slot := range boardMap {
merged[board][playerID] = slot
}
}
rows, err := a.db.Query(`SELECT board, player_id, group_key, position FROM player_positions`)
if err != nil {
return nil, fmt.Errorf("query player positions: %w", err)
}
defer rows.Close()
for rows.Next() {
var boardRaw string
var playerID int
var groupKey string
var position int
if err := rows.Scan(&boardRaw, &playerID, &groupKey, &position); err != nil {
return nil, fmt.Errorf("scan player position: %w", err)
}
board, ok := normalizePositionBoard(boardRaw)
if !ok {
continue
}
boardMap := merged[board]
if boardMap == nil {
continue
}
defaultSlot, exists := boardMap[playerID]
if !exists {
continue
}
pos := normalizePositionValue(position, defaultSlot.Position)
group := normalizePositionGroupKey(board, groupKey, defaultSlot.GroupKey)
boardMap[playerID] = PositionSlot{
GroupKey: group,
Position: pos,
}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate player positions: %w", err)
}
return merged, nil
}
func defaultPositionBoards(players []Player, derived DerivedState) map[string]map[int]PositionSlot {
out := map[string]map[int]PositionSlot{
positionBoardPreliminary: {},
positionBoardFinal: {},
positionBoardPrelimTie: {},
positionBoardFinalTie: {},
}
preRows := make([]Player, 0, len(players))
preRows = append(preRows, players...)
sort.SliceStable(preRows, func(i, j int) bool {
ga := normalizePreliminaryGroupKey(preRows[i].GroupCode)
gb := normalizePreliminaryGroupKey(preRows[j].GroupCode)
if ga != gb {
return ga < gb
}
return preRows[i].ID < preRows[j].ID
})
preGroupCounts := map[string]int{}
for _, player := range preRows {
group := normalizePreliminaryGroupKey(player.GroupCode)
preGroupCounts[group]++
out[positionBoardPreliminary][player.ID] = PositionSlot{
GroupKey: group,
Position: preGroupCounts[group],
}
}
finalGroupCounts := map[string]int{}
for _, row := range derived.FinalGroups.Group1 {
group := "1"
finalGroupCounts[group]++
out[positionBoardFinal][row.PlayerID] = PositionSlot{GroupKey: group, Position: finalGroupCounts[group]}
}
for _, row := range derived.FinalGroups.Group2 {
group := "2"
finalGroupCounts[group]++
out[positionBoardFinal][row.PlayerID] = PositionSlot{GroupKey: group, Position: finalGroupCounts[group]}
}
preRowsByID := map[int]RankingRow{}
for _, row := range derived.PreliminaryRanking.Rows {
preRowsByID[row.PlayerID] = row
}
preTieRows := make([]RankingRow, 0, len(derived.PreliminaryRanking.TieBreak.PlayerIDs))
for _, id := range derived.PreliminaryRanking.TieBreak.PlayerIDs {
row, ok := preRowsByID[id]
if !ok {
continue
}
preTieRows = append(preTieRows, row)
}
sort.SliceStable(preTieRows, func(i, j int) bool {
if preTieRows[i].TieBreak != preTieRows[j].TieBreak {
return preTieRows[i].TieBreak > preTieRows[j].TieBreak
}
if preTieRows[i].Score != preTieRows[j].Score {
return preTieRows[i].Score > preTieRows[j].Score
}
return preTieRows[i].PlayerID < preTieRows[j].PlayerID
})
for idx, row := range preTieRows {
groupKey := strconv.Itoa((idx / 6) + 1)
position := (idx % 6) + 1
out[positionBoardPrelimTie][row.PlayerID] = PositionSlot{GroupKey: groupKey, Position: position}
}
finalRowsByID := map[int]RankingRow{}
for _, row := range derived.FinalRanking.Rows {
finalRowsByID[row.PlayerID] = row
}
finalTieRows := make([]RankingRow, 0, len(derived.FinalRanking.TieBreak.PlayerIDs))
for _, id := range derived.FinalRanking.TieBreak.PlayerIDs {
row, ok := finalRowsByID[id]
if !ok {
continue
}
finalTieRows = append(finalTieRows, row)
}
sort.SliceStable(finalTieRows, func(i, j int) bool {
if finalTieRows[i].TieBreak != finalTieRows[j].TieBreak {
return finalTieRows[i].TieBreak > finalTieRows[j].TieBreak
}
if finalTieRows[i].Score != finalTieRows[j].Score {
return finalTieRows[i].Score > finalTieRows[j].Score
}
return finalTieRows[i].PlayerID < finalTieRows[j].PlayerID
})
for idx, row := range finalTieRows {
groupKey := strconv.Itoa((idx / 6) + 1)
position := (idx % 6) + 1
out[positionBoardFinalTie][row.PlayerID] = PositionSlot{GroupKey: groupKey, Position: position}
}
return out
}
func normalizePositionBoard(board string) (string, bool) {
normalized := strings.ToLower(strings.TrimSpace(board))
if !allowedPositionBoards[normalized] {
return "", false
}
return normalized, true
}
func normalizePreliminaryGroupKey(groupCode string) string {
normalized := strings.ToUpper(strings.TrimSpace(groupCode))
if normalized == "" {
return positionBoardUnassigned
}
return normalized
}
func normalizePositionGroupKey(board, groupKey, fallback string) string {
if board == positionBoardPreliminary {
normalized := normalizePreliminaryGroupKey(groupKey)
if normalized == "" {
return fallback
}
return normalized
}
normalized := strings.TrimSpace(groupKey)
if normalized == "" {
return fallback
}
return normalized
}
func normalizePositionValue(position int, fallback int) int {
if position > 0 {
return position
}
if fallback > 0 {
return fallback
}
return 1
}
2026-04-01 11:47:03 +04:00
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,
2026-04-03 09:55:36 +04:00
Score: scoreTotal(scores, p.ID, preliminaryRoundStages),
2026-04-01 11:47:03 +04:00
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,
2026-04-03 09:55:36 +04:00
Score: scoreTotal(scores, finalist.PlayerID, finalRoundStages),
2026-04-01 11:47:03 +04:00
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,
}
}
2026-04-03 09:55:36 +04:00
func scoreTotal(scores map[string]map[int]int, playerID int, stages []string) int {
total := 0
for _, stage := range stages {
stageMap, ok := scores[stage]
if !ok {
continue
}
total += stageMap[playerID]
}
return total
}
2026-04-01 11:47:03 +04:00
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
}
}