From 965f2b630677269d4b99896cd659290519409d94 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:41:07 -0700 Subject: [PATCH 1/7] feat: add player stats for raffle Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 4 ++++ realm/stats.gno | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 realm/stats.gno diff --git a/realm/chess.gno b/realm/chess.gno index 52a09d2..e03002e 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -360,6 +360,10 @@ func MakeMove(gameID, from, to string, promote Piece) string { isBlack := len(g.Position.Moves)%2 == 1 caller := std.GetOrigCaller() + + stats := getPlayerStats(caller) + stats.Moves++ + if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { // either not a player involved; or not the caller's turn. diff --git a/realm/stats.gno b/realm/stats.gno new file mode 100644 index 0000000..bbe8c66 --- /dev/null +++ b/realm/stats.gno @@ -0,0 +1,49 @@ +package chess + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type playerStats struct { + Addr std.Address // Not stored when in avl.Tree, but lasily filled when returning a single or collection of player stats for other contracts. + Moves uint + StartedGames uint + WonGames uint + LostGames uint + TimedoutGames uint + ResignedGames uint + DrawnGames uint + SeriousGames uint // finished, or resigned/drawn after 20 full moves (40 turns) + // later we can add achievements: + // SuperFastAchievement // if a game is finished in less than N seconds. + // OnlyPawnsAchievement // winning with only pawns, etc. +} + +var allPlayerStats avl.Tree // std.Address -> *playerStats + +func getPlayerStats(addr std.Address) *playerStats { + addrStr := string(addr) + res, found := allPlayerStats.Get(addrStr) + if found { + return res.(*playerStats) + } + + newStats := playerStats{} + allPlayerStats.Set(addrStr, &newStats) + return &newStats +} + +func AllPlayerStats() []playerStats { + ret := []playerStats{} + + allPlayerStats.Iterate("", "", func(addrString string, v interface{}) bool { + stats := *(v.(*playerStats)) + stats.Addr = std.Address(addrString) + ret = append(ret, stats) + return false + }) + + return ret +} From b169c4c79a0265980c75f8d48dbb2be3ce8e0d44 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:47:13 -0700 Subject: [PATCH 2/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 4 ++-- realm/stats.gno | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/realm/chess.gno b/realm/chess.gno index e03002e..afc0d22 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -361,8 +361,8 @@ func MakeMove(gameID, from, to string, promote Piece) string { caller := std.GetOrigCaller() - stats := getPlayerStats(caller) - stats.Moves++ + // stats := getPlayerStats(caller) + // stats.Moves++ if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { diff --git a/realm/stats.gno b/realm/stats.gno index bbe8c66..b3c706e 100644 --- a/realm/stats.gno +++ b/realm/stats.gno @@ -7,7 +7,7 @@ import ( ) type playerStats struct { - Addr std.Address // Not stored when in avl.Tree, but lasily filled when returning a single or collection of player stats for other contracts. + Addr std.Address // Not stored when in avl.Tree, but lazily filled for public-facing helpers returning playerStats. Moves uint StartedGames uint WonGames uint @@ -15,7 +15,7 @@ type playerStats struct { TimedoutGames uint ResignedGames uint DrawnGames uint - SeriousGames uint // finished, or resigned/drawn after 20 full moves (40 turns) + SeriousGames uint // finished, or resigned/drawn after 20 full moves (40 turns), used for the raffle. // later we can add achievements: // SuperFastAchievement // if a game is finished in less than N seconds. // OnlyPawnsAchievement // winning with only pawns, etc. @@ -39,9 +39,9 @@ func AllPlayerStats() []playerStats { ret := []playerStats{} allPlayerStats.Iterate("", "", func(addrString string, v interface{}) bool { - stats := *(v.(*playerStats)) - stats.Addr = std.Address(addrString) - ret = append(ret, stats) + // stats := *(v.(*playerStats)) + // stats.Addr = std.Address(addrString) + // ret = append(ret, stats) return false }) From 3a76c5537cf3dcc60ffb678e1a69e1e3d95c5df9 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:53:49 -0700 Subject: [PATCH 3/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 4 ++-- realm/chess_test.gno | 22 +++++++++++++++++++++- realm/stats.gno | 24 ++++++++++++++++++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/realm/chess.gno b/realm/chess.gno index afc0d22..e03002e 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -361,8 +361,8 @@ func MakeMove(gameID, from, to string, promote Piece) string { caller := std.GetOrigCaller() - // stats := getPlayerStats(caller) - // stats.Moves++ + stats := getPlayerStats(caller) + stats.Moves++ if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { diff --git a/realm/chess_test.gno b/realm/chess_test.gno index a0543ef..a4ce170 100644 --- a/realm/chess_test.gno +++ b/realm/chess_test.gno @@ -22,6 +22,7 @@ func cleanup() { lobby = [tcLobbyMax][]lobbyPlayer{} lobbyPlayer2Game = avl.Tree{} playerRatings = [CategoryMax][]*PlayerRating{} + allPlayerStats = avl.Tree{} } func TestNewGame(t *testing.T) { @@ -141,7 +142,12 @@ var commandTests = [...]string{ # contains "address":"g1white" "position":0 "wins":1 "losses":0 "draws":0 player black # contains "address":"g1black" "position":1 "wins":0 "losses":1 "draws":0 + stats white + # contains addr:g1white moves:4 started:1 won:1 lost:0 timedout:0 resigned:0 drawn:0 serious:1 + stats black + # contains addr:g1white moves:3 started:1 won:0 lost:1 timedout:0 resigned:0 drawn:0 serious:1 `, + /* XXX: TEMPORARILY DISABLED ` name DrawByAgreement newgame move white e2e4 @@ -156,11 +162,12 @@ var commandTests = [...]string{ draw black # contains "drawn_by_agreement" "concluder":"g1black" "draw_offerer":"g1white" `, + */ ` name AbortFirstMove newgame abort white # contains "winner":"none" "concluder":"g1white" `, - + /* XXX: TEMPORARILY DISABLED ` name ThreefoldRepetition newgame @@ -177,6 +184,8 @@ var commandTests = [...]string{ draw black # contains "winner":"draw" "concluder":"g1black" # contains "state":"drawn_3_fold" `, + */ + /* XXX: TEMPORARILY DISABLED ` name FivefoldRepetition newgame @@ -204,6 +213,7 @@ var commandTests = [...]string{ move white g1f3 #panic contains game is already finished `, + */ ` name TimeoutAborted newgame white black 3 move white e2e4 #! contains "state":"open" @@ -334,6 +344,14 @@ func (tc *testCommandSleep) Run(t *testing.T, bufs map[string]string) { os_test.Sleep(tc.dur) } +type testCommandStats struct { + addr string +} + +func (tc *testCommandStats) Run(t *testing.T, bufs map[string]string) { + bufs["result"] = GetPlayerStats(std.Address(tc.addr)).String() +} + type testChecker struct { fn func(t *testing.T, bufs map[string]string, tc *testChecker) tf func(*testing.T, string, ...interface{}) @@ -441,6 +459,8 @@ func parseCommandTest(t *testing.T, command string) (funcs []testCommandRunner, funcs = append(funcs, newTestCommandColorID(ClaimTimeout, "timeout", command[1])) case "resign": funcs = append(funcs, newTestCommandColorID(Resign, "resign", command[1])) + case "stats": + funcs = append(funcs, &testCommandStats{"g1" + command[1]}) case "game": if len(command) > 2 { panic("invalid game command " + line) diff --git a/realm/stats.gno b/realm/stats.gno index b3c706e..8c6e68c 100644 --- a/realm/stats.gno +++ b/realm/stats.gno @@ -4,8 +4,11 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" ) +var allPlayerStats avl.Tree // std.Address -> *playerStats + type playerStats struct { Addr std.Address // Not stored when in avl.Tree, but lazily filled for public-facing helpers returning playerStats. Moves uint @@ -21,7 +24,13 @@ type playerStats struct { // OnlyPawnsAchievement // winning with only pawns, etc. } -var allPlayerStats avl.Tree // std.Address -> *playerStats +func (s playerStats) String() string { + return ufmt.Sprintf( + "addr:%s moves:%d started:%d won:%d lost:%d timedout:%d resigned:%d drawn:%d serious:%d", + s.Addr, s.Moves, s.StartedGames, s.WonGames, s.LostGames, s.TimedoutGames, + s.ResignedGames, s.DrawnGames, s.SeriousGames, + ) +} func getPlayerStats(addr std.Address) *playerStats { addrStr := string(addr) @@ -35,13 +44,20 @@ func getPlayerStats(addr std.Address) *playerStats { return &newStats } +func GetPlayerStats(addr std.Address) playerStats { + stats := getPlayerStats(addr) + cpy := *stats + cpy.Addr = addr + return cpy +} + func AllPlayerStats() []playerStats { ret := []playerStats{} allPlayerStats.Iterate("", "", func(addrString string, v interface{}) bool { - // stats := *(v.(*playerStats)) - // stats.Addr = std.Address(addrString) - // ret = append(ret, stats) + stats := *(v.(*playerStats)) + stats.Addr = std.Address(addrString) + ret = append(ret, stats) return false }) From 31a0c51194c56bdc02b923ac25bb49af1664970e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:24:43 -0700 Subject: [PATCH 4/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 28 +++++++++++++++++++++++++--- realm/chess_test.gno | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/realm/chess.gno b/realm/chess.gno index e03002e..e537c42 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -221,6 +221,8 @@ func xNewGame(opponentRaw string, seconds, increment int) string { opponent := parsePlayer(opponentRaw) caller := std.GetOrigCaller() + getPlayerStats(opponent).StartedGames++ + getPlayerStats(caller).StartedGames++ assertUserNotInLobby(caller) return newGame(caller, opponent, seconds, increment).json() @@ -361,8 +363,18 @@ func MakeMove(gameID, from, to string, promote Piece) string { caller := std.GetOrigCaller() - stats := getPlayerStats(caller) - stats.Moves++ + // FIXME: create better helpers to avoid if/else conditions. + whiteStats := getPlayerStats(g.White) + blackStats := getPlayerStats(g.Black) + var callerStats, opponentStats *playerStats + if caller == g.White { + callerStats = whiteStats + opponentStats = blackStats + } else { + callerStats = blackStats + opponentStats = whiteStats + } + callerStats.Moves++ if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { @@ -372,6 +384,9 @@ func MakeMove(gameID, from, to string, promote Piece) string { // game is time controlled? add move to time control if g.Time != nil { + whiteStats.TimedoutGames++ + blackStats.TimedoutGames++ + valid := g.Time.AddMove() if !valid && len(g.Position.Moves) < 2 { g.State = GameStateAborted @@ -424,13 +439,20 @@ func MakeMove(gameID, from, to string, promote Piece) string { case o == Checkmate && isBlack: g.State = GameStateCheckmated g.Winner = WinnerBlack + blackStats.WonGames++ + whiteStats.LostGames++ + blackStats.SeriousGames++ + whiteStats.SeriousGames++ case o == Checkmate && !isBlack: g.State = GameStateCheckmated g.Winner = WinnerWhite + whiteStats.WonGames++ + blackStats.LostGames++ + blackStats.SeriousGames++ + whiteStats.SeriousGames++ case o == Stalemate: g.State = GameStateStalemate g.Winner = WinnerDraw - case o == Drawn75Move: g.State = GameStateDrawn75Move g.Winner = WinnerDraw diff --git a/realm/chess_test.gno b/realm/chess_test.gno index a4ce170..f6c007a 100644 --- a/realm/chess_test.gno +++ b/realm/chess_test.gno @@ -145,7 +145,7 @@ var commandTests = [...]string{ stats white # contains addr:g1white moves:4 started:1 won:1 lost:0 timedout:0 resigned:0 drawn:0 serious:1 stats black - # contains addr:g1white moves:3 started:1 won:0 lost:1 timedout:0 resigned:0 drawn:0 serious:1 + # contains addr:g1black moves:3 started:1 won:0 lost:1 timedout:0 resigned:0 drawn:0 serious:1 `, /* XXX: TEMPORARILY DISABLED ` name DrawByAgreement From faa65eb2223b51200340dd65c5a59b0e2c143bbc Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:28:08 -0700 Subject: [PATCH 5/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 81 +++++++++++++++++++++++++++++--------------- realm/chess_test.gno | 22 ++++++------ realm/stats.gno | 27 +++++++++++++++ 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/realm/chess.gno b/realm/chess.gno index e537c42..85941a3 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -363,18 +363,9 @@ func MakeMove(gameID, from, to string, promote Piece) string { caller := std.GetOrigCaller() - // FIXME: create better helpers to avoid if/else conditions. - whiteStats := getPlayerStats(g.White) - blackStats := getPlayerStats(g.Black) - var callerStats, opponentStats *playerStats - if caller == g.White { - callerStats = whiteStats - opponentStats = blackStats - } else { - callerStats = blackStats - opponentStats = whiteStats - } - callerStats.Moves++ + // see stats.gno + stats := g.getStats(caller) + stats.caller.Moves++ if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { @@ -384,9 +375,6 @@ func MakeMove(gameID, from, to string, promote Piece) string { // game is time controlled? add move to time control if g.Time != nil { - whiteStats.TimedoutGames++ - blackStats.TimedoutGames++ - valid := g.Time.AddMove() if !valid && len(g.Position.Moves) < 2 { g.State = GameStateAborted @@ -439,17 +427,17 @@ func MakeMove(gameID, from, to string, promote Piece) string { case o == Checkmate && isBlack: g.State = GameStateCheckmated g.Winner = WinnerBlack - blackStats.WonGames++ - whiteStats.LostGames++ - blackStats.SeriousGames++ - whiteStats.SeriousGames++ + stats.black.WonGames++ + stats.white.LostGames++ + stats.black.SeriousGames++ + stats.white.SeriousGames++ case o == Checkmate && !isBlack: g.State = GameStateCheckmated g.Winner = WinnerWhite - whiteStats.WonGames++ - blackStats.LostGames++ - blackStats.SeriousGames++ - whiteStats.SeriousGames++ + stats.white.WonGames++ + stats.black.LostGames++ + stats.black.SeriousGames++ + stats.white.SeriousGames++ case o == Stalemate: g.State = GameStateStalemate g.Winner = WinnerDraw @@ -460,6 +448,16 @@ func MakeMove(gameID, from, to string, promote Piece) string { g.State = GameStateDrawn5Fold g.Winner = WinnerDraw } + + if g.Winner == WinnerDraw { + stats.white.DrawnGames++ + stats.black.DrawnGames++ + if g.isSerious() { + stats.white.SeriousGames++ + stats.black.SeriousGames++ + } + } + g.DrawOfferer = nil g.saveResult() @@ -467,6 +465,10 @@ func MakeMove(gameID, from, to string, promote Piece) string { } func (g *Game) claimTimeout() error { + if g.State == GameStateAborted { // already claimed + return nil + } + // no assert origin call or caller check: anyone can claim a game to have // finished in timeout. @@ -480,6 +482,17 @@ func (g *Game) claimTimeout() error { return errors.New("game is not timed out") } + // update stats if games wasn't already computed. + stats := g.getStats("") + if g.State != GameStateTimeout { + stats.white.TimedoutGames++ + stats.black.TimedoutGames++ + if g.isSerious() { + stats.white.SeriousGames++ + stats.black.SeriousGames++ + } + } + if nmov := len(g.Position.Moves); nmov < 2 { g.State = GameStateAborted if nmov == 1 { @@ -494,8 +507,12 @@ func (g *Game) claimTimeout() error { g.State = GameStateTimeout if len(g.Position.Moves)&1 == 0 { g.Winner = WinnerBlack + stats.black.WonGames++ + stats.white.LostGames++ } else { g.Winner = WinnerWhite + stats.black.LostGames++ + stats.white.WonGames++ } g.DrawOfferer = nil g.saveResult() @@ -628,14 +645,25 @@ func Draw(gameID string) string { panic("you are not involved in this game") } + // see stats.gno + stats := g.getStats(caller) + + updateStats := func() { + stats.white.DrawnGames++ + stats.black.DrawnGames++ + if g.isSerious() { + stats.white.SeriousGames++ + stats.black.SeriousGames++ + } + } + // accepted draw offer (do early to avoid gas for g.Position.IsFinished()) if g.DrawOfferer != nil && *g.DrawOfferer != caller { g.State = GameStateDrawnByAgreement g.Winner = WinnerDraw g.Concluder = &caller - g.saveResult() - + updateStats() return g.json() } @@ -653,8 +681,7 @@ func Draw(gameID string) string { g.Concluder = &caller g.Winner = WinnerDraw g.DrawOfferer = nil - g.saveResult() - + updateStats() return g.json() } diff --git a/realm/chess_test.gno b/realm/chess_test.gno index f6c007a..b30707b 100644 --- a/realm/chess_test.gno +++ b/realm/chess_test.gno @@ -142,12 +142,9 @@ var commandTests = [...]string{ # contains "address":"g1white" "position":0 "wins":1 "losses":0 "draws":0 player black # contains "address":"g1black" "position":1 "wins":0 "losses":1 "draws":0 - stats white - # contains addr:g1white moves:4 started:1 won:1 lost:0 timedout:0 resigned:0 drawn:0 serious:1 - stats black - # contains addr:g1black moves:3 started:1 won:0 lost:1 timedout:0 resigned:0 drawn:0 serious:1 + stats white # contains moves:4 started:1 won:1 lost:0 timedout:0 resigned:0 drawn:0 serious:1 + stats black # contains moves:3 started:1 won:0 lost:1 timedout:0 resigned:0 drawn:0 serious:1 `, - /* XXX: TEMPORARILY DISABLED ` name DrawByAgreement newgame move white e2e4 @@ -161,13 +158,15 @@ var commandTests = [...]string{ # contains "open" "concluder":null "draw_offerer":"g1white" draw black # contains "drawn_by_agreement" "concluder":"g1black" "draw_offerer":"g1white" + stats white # contains moves:2 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 + stats black # contains moves:2 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 `, - */ ` name AbortFirstMove newgame abort white # contains "winner":"none" "concluder":"g1white" + stats white # contains moves:0 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:0 serious:0 + stats black # contains moves:0 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:0 serious:0 `, - /* XXX: TEMPORARILY DISABLED ` name ThreefoldRepetition newgame @@ -183,9 +182,9 @@ var commandTests = [...]string{ draw black # contains "winner":"draw" "concluder":"g1black" # contains "state":"drawn_3_fold" + stats white # contains moves:4 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 + stats black # contains moves:4 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 `, - */ - /* XXX: TEMPORARILY DISABLED ` name FivefoldRepetition newgame @@ -212,8 +211,9 @@ var commandTests = [...]string{ # contains "winner":"draw" "concluder":null "state":"drawn_5_fold" move white g1f3 #panic contains game is already finished + stats white # contains moves:8 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 + stats black # contains moves:8 started:1 won:0 lost:0 timedout:0 resigned:0 drawn:1 serious:0 `, - */ ` name TimeoutAborted newgame white black 3 move white e2e4 #! contains "state":"open" @@ -223,6 +223,8 @@ var commandTests = [...]string{ # contains e2e4 # contains "aborted" # contains "concluder":"g1black" + stats white # contains moves:1 started:1 won:0 lost:0 timedout:1 resigned:0 drawn:0 serious:0 + stats black # contains moves:1 started:1 won:0 lost:0 timedout:1 resigned:0 drawn:0 serious:0 `, ` name TimeoutAbandoned newgame white black 1 diff --git a/realm/stats.gno b/realm/stats.gno index 8c6e68c..4b3852f 100644 --- a/realm/stats.gno +++ b/realm/stats.gno @@ -63,3 +63,30 @@ func AllPlayerStats() []playerStats { return ret } + +type gameStats struct { + caller, opponent, white, black *playerStats +} + +func (g Game) getStats(caller std.Address) gameStats { + stats := gameStats{ + white: getPlayerStats(g.White), + black: getPlayerStats(g.Black), + } + + if caller != "" { // if caller is empty, we just fill "black" and "white". + if caller == g.White { + stats.caller = stats.white + stats.opponent = stats.black + } else { + stats.caller = stats.black + stats.opponent = stats.white + } + } + + return stats +} + +func (g Game) isSerious() bool { + return len(g.Position.Moves) >= 40 +} From b94142439968f11e068283548d4ccfc4a22ee104 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:25:30 -0700 Subject: [PATCH 6/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess.gno | 63 +++++++------------------------------------- realm/chess_test.gno | 2 ++ realm/stats.gno | 38 ++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 56 deletions(-) diff --git a/realm/chess.gno b/realm/chess.gno index 85941a3..70bb765 100644 --- a/realm/chess.gno +++ b/realm/chess.gno @@ -364,8 +364,7 @@ func MakeMove(gameID, from, to string, promote Piece) string { caller := std.GetOrigCaller() // see stats.gno - stats := g.getStats(caller) - stats.caller.Moves++ + getPlayerStats(caller).Moves++ if (isBlack && g.Black != caller) || (!isBlack && g.White != caller) { @@ -380,6 +379,7 @@ func MakeMove(gameID, from, to string, promote Piece) string { g.State = GameStateAborted g.Concluder = &caller g.Winner = WinnerNone + g.updateEndgameStats() return g.json() } if !valid { @@ -389,6 +389,7 @@ func MakeMove(gameID, from, to string, promote Piece) string { } else { g.Winner = WinnerWhite } + g.updateEndgameStats() g.saveResult() return g.json() } @@ -427,17 +428,9 @@ func MakeMove(gameID, from, to string, promote Piece) string { case o == Checkmate && isBlack: g.State = GameStateCheckmated g.Winner = WinnerBlack - stats.black.WonGames++ - stats.white.LostGames++ - stats.black.SeriousGames++ - stats.white.SeriousGames++ case o == Checkmate && !isBlack: g.State = GameStateCheckmated g.Winner = WinnerWhite - stats.white.WonGames++ - stats.black.LostGames++ - stats.black.SeriousGames++ - stats.white.SeriousGames++ case o == Stalemate: g.State = GameStateStalemate g.Winner = WinnerDraw @@ -449,26 +442,14 @@ func MakeMove(gameID, from, to string, promote Piece) string { g.Winner = WinnerDraw } - if g.Winner == WinnerDraw { - stats.white.DrawnGames++ - stats.black.DrawnGames++ - if g.isSerious() { - stats.white.SeriousGames++ - stats.black.SeriousGames++ - } - } - g.DrawOfferer = nil g.saveResult() + g.updateEndgameStats() return g.json() } func (g *Game) claimTimeout() error { - if g.State == GameStateAborted { // already claimed - return nil - } - // no assert origin call or caller check: anyone can claim a game to have // finished in timeout. @@ -482,17 +463,6 @@ func (g *Game) claimTimeout() error { return errors.New("game is not timed out") } - // update stats if games wasn't already computed. - stats := g.getStats("") - if g.State != GameStateTimeout { - stats.white.TimedoutGames++ - stats.black.TimedoutGames++ - if g.isSerious() { - stats.white.SeriousGames++ - stats.black.SeriousGames++ - } - } - if nmov := len(g.Position.Moves); nmov < 2 { g.State = GameStateAborted if nmov == 1 { @@ -501,22 +471,19 @@ func (g *Game) claimTimeout() error { g.Concluder = &g.White } g.Winner = WinnerNone + g.updateEndgameStats() return nil } g.State = GameStateTimeout if len(g.Position.Moves)&1 == 0 { g.Winner = WinnerBlack - stats.black.WonGames++ - stats.white.LostGames++ } else { g.Winner = WinnerWhite - stats.black.LostGames++ - stats.white.WonGames++ } g.DrawOfferer = nil + g.updateEndgameStats() g.saveResult() - return nil } @@ -586,8 +553,8 @@ func resign(g *Game) error { return errors.New("you are not involved in this game") } g.DrawOfferer = nil + g.updateEndgameStats() g.saveResult() - return nil } @@ -645,25 +612,13 @@ func Draw(gameID string) string { panic("you are not involved in this game") } - // see stats.gno - stats := g.getStats(caller) - - updateStats := func() { - stats.white.DrawnGames++ - stats.black.DrawnGames++ - if g.isSerious() { - stats.white.SeriousGames++ - stats.black.SeriousGames++ - } - } - // accepted draw offer (do early to avoid gas for g.Position.IsFinished()) if g.DrawOfferer != nil && *g.DrawOfferer != caller { g.State = GameStateDrawnByAgreement g.Winner = WinnerDraw g.Concluder = &caller g.saveResult() - updateStats() + g.updateEndgameStats() return g.json() } @@ -682,6 +637,6 @@ func Draw(gameID string) string { g.Winner = WinnerDraw g.DrawOfferer = nil g.saveResult() - updateStats() + g.updateEndgameStats() return g.json() } diff --git a/realm/chess_test.gno b/realm/chess_test.gno index b30707b..1b0349a 100644 --- a/realm/chess_test.gno +++ b/realm/chess_test.gno @@ -233,6 +233,8 @@ var commandTests = [...]string{ sleep 61 timeout black # contains "state":"timeout" "winner":"black" + stats white # contains moves:1 started:1 won:0 lost:1 timedout:1 resigned:0 drawn:0 serious:0 + stats black # contains moves:1 started:1 won:1 lost:0 timedout:1 resigned:0 drawn:0 serious:0 `, } diff --git a/realm/stats.gno b/realm/stats.gno index 4b3852f..ace81e8 100644 --- a/realm/stats.gno +++ b/realm/stats.gno @@ -87,6 +87,40 @@ func (g Game) getStats(caller std.Address) gameStats { return stats } -func (g Game) isSerious() bool { - return len(g.Position.Moves) >= 40 +func (g Game) updateEndgameStats() { + stats := g.getStats("") + + // serious games + isSerious := false + switch { + case g.State == GameStateCheckmated: // checkmates + isSerious = true + case len(g.Position.Moves) >= 40: // long games + isSerious = true + } + if isSerious { + stats.black.SeriousGames++ + stats.white.SeriousGames++ + } + + // timeouts, aborted, etc + if g.State == GameStateTimeout || g.State == GameStateAborted { + stats.black.TimedoutGames++ + stats.white.TimedoutGames++ + } + + // winners, losers, draws + switch g.Winner { + case WinnerWhite: + stats.white.WonGames++ + stats.black.LostGames++ + case WinnerBlack: + stats.black.WonGames++ + stats.white.LostGames++ + case WinnerDraw: + stats.black.DrawnGames++ + stats.white.DrawnGames++ + } + + // XXX: resigned } From d57ec8789865bfe812d87ea2be070536eaa5bb71 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:29:12 -0700 Subject: [PATCH 7/7] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- realm/chess_test.gno | 2 ++ realm/stats.gno | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/realm/chess_test.gno b/realm/chess_test.gno index 1b0349a..5829802 100644 --- a/realm/chess_test.gno +++ b/realm/chess_test.gno @@ -117,6 +117,8 @@ var commandTests = [...]string{ newgame # contains "white":"g1black" "black":"g1white" #id equal 000000002 + stats white # contains moves:2 started:2 won:0 lost:1 timedout:0 resigned:1 drawn:0 serious:0 + stats black # contains moves:1 started:2 won:1 lost:0 timedout:0 resigned:0 drawn:0 serious:0 `, // Otherwise, invert from p1's history. ` name ColoursInvert3p diff --git a/realm/stats.gno b/realm/stats.gno index ace81e8..7d2385e 100644 --- a/realm/stats.gno +++ b/realm/stats.gno @@ -114,13 +114,17 @@ func (g Game) updateEndgameStats() { case WinnerWhite: stats.white.WonGames++ stats.black.LostGames++ + if g.State == GameStateResigned { + stats.black.ResignedGames++ + } case WinnerBlack: stats.black.WonGames++ stats.white.LostGames++ + if g.State == GameStateResigned { + stats.white.ResignedGames++ + } case WinnerDraw: stats.black.DrawnGames++ stats.white.DrawnGames++ } - - // XXX: resigned }