Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add player stats for raffle #173

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions realm/chess.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -360,6 +362,10 @@ func MakeMove(gameID, from, to string, promote Piece) string {
isBlack := len(g.Position.Moves)%2 == 1

caller := std.GetOrigCaller()

// see stats.gno
getPlayerStats(caller).Moves++

if (isBlack && g.Black != caller) ||
(!isBlack && g.White != caller) {
// either not a player involved; or not the caller's turn.
Expand All @@ -373,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 {
Expand All @@ -382,6 +389,7 @@ func MakeMove(gameID, from, to string, promote Piece) string {
} else {
g.Winner = WinnerWhite
}
g.updateEndgameStats()
g.saveResult()
return g.json()
}
Expand Down Expand Up @@ -426,16 +434,17 @@ func MakeMove(gameID, from, to string, promote Piece) string {
case o == Stalemate:
g.State = GameStateStalemate
g.Winner = WinnerDraw

case o == Drawn75Move:
g.State = GameStateDrawn75Move
g.Winner = WinnerDraw
case o == Drawn5Fold:
g.State = GameStateDrawn5Fold
g.Winner = WinnerDraw
}

g.DrawOfferer = nil
g.saveResult()
g.updateEndgameStats()

return g.json()
}
Expand All @@ -462,6 +471,7 @@ func (g *Game) claimTimeout() error {
g.Concluder = &g.White
}
g.Winner = WinnerNone
g.updateEndgameStats()
return nil
}

Expand All @@ -472,8 +482,8 @@ func (g *Game) claimTimeout() error {
g.Winner = WinnerWhite
}
g.DrawOfferer = nil
g.updateEndgameStats()
g.saveResult()

return nil
}

Expand Down Expand Up @@ -543,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
}

Expand Down Expand Up @@ -607,9 +617,8 @@ func Draw(gameID string) string {
g.State = GameStateDrawnByAgreement
g.Winner = WinnerDraw
g.Concluder = &caller

g.saveResult()

g.updateEndgameStats()
return g.json()
}

Expand All @@ -627,8 +636,7 @@ func Draw(gameID string) string {
g.Concluder = &caller
g.Winner = WinnerDraw
g.DrawOfferer = nil

g.saveResult()

g.updateEndgameStats()
return g.json()
}
28 changes: 27 additions & 1 deletion realm/chess_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func cleanup() {
lobby = [tcLobbyMax][]lobbyPlayer{}
lobbyPlayer2Game = avl.Tree{}
playerRatings = [CategoryMax][]*PlayerRating{}
allPlayerStats = avl.Tree{}
}

func TestNewGame(t *testing.T) {
Expand Down Expand Up @@ -116,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
Expand All @@ -141,6 +144,8 @@ 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 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
`,
` name DrawByAgreement
newgame
Expand All @@ -155,12 +160,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
`,

` name ThreefoldRepetition
newgame

Expand All @@ -176,6 +184,8 @@ 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
`,
` name FivefoldRepetition
newgame
Expand Down Expand Up @@ -203,6 +213,8 @@ 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
Expand All @@ -213,6 +225,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
Expand All @@ -221,6 +235,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
`,
}

Expand Down Expand Up @@ -334,6 +350,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{})
Expand Down Expand Up @@ -441,6 +465,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)
Expand Down
130 changes: 130 additions & 0 deletions realm/stats.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package chess

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
StartedGames uint
WonGames uint
LostGames uint
TimedoutGames uint
ResignedGames uint
DrawnGames uint
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.
}

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)
res, found := allPlayerStats.Get(addrStr)
if found {
return res.(*playerStats)
}

newStats := playerStats{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not

newStats := playerStats{Addr: addr} 

?

And so why doing that in the public function instead (line 50) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to save on storage since the address is already known by the avl.Tree; but I'm okay to move to your solution too. Feel free to patch my PR if you prefer 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that's fine, it's also good to save storage!

allPlayerStats.Set(addrStr, &newStats)
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)
return false
})

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) 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++
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++
}
}