Add player ratings

Resolves #7.
This commit is contained in:
Trevor Slocum 2023-12-29 22:52:27 -08:00
parent a2fb60cf35
commit 5ec74dd3db
6 changed files with 127 additions and 13 deletions

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/gobwas/ws v1.3.1
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.5.1
github.com/jlouis/glicko2 v1.0.0
github.com/matcornic/hermes/v2 v2.1.0
)

2
go.sum
View file

@ -53,6 +53,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jlouis/glicko2 v1.0.0 h1:pSl/OTRclxdrhtqoqJpvO41GoUL1dmaTnxC2F6+W+y8=
github.com/jlouis/glicko2 v1.0.0/go.mod h1:5dzlxjhVPPLk+wiUwwF2oVyDwsNXMgnw7WrLRxuejBs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

View file

@ -21,22 +21,27 @@ import (
"code.rocket9labs.com/tslocum/bgammon"
"github.com/alexedwards/argon2id"
"github.com/jackc/pgx/v5"
"github.com/jlouis/glicko2"
"github.com/matcornic/hermes/v2"
)
const databaseSchema = `
CREATE TABLE account (
id serial PRIMARY KEY,
created bigint NOT NULL,
confirmed bigint NOT NULL DEFAULT 0,
active bigint NOT NULL,
reset bigint NOT NULL DEFAULT 0,
email text NOT NULL,
username text NOT NULL,
password text NOT NULL,
highlight smallint NOT NULL DEFAULT 1,
pips smallint NOT NULL DEFAULT 1,
moves smallint NOT NULL DEFAULT 0
id serial PRIMARY KEY,
created bigint NOT NULL,
confirmed bigint NOT NULL DEFAULT 0,
active bigint NOT NULL,
reset bigint NOT NULL DEFAULT 0,
email text NOT NULL,
username text NOT NULL,
password text NOT NULL,
casualsingle integer NOT NULL DEFAULT 150000,
casualmulti integer NOT NULL DEFAULT 150000,
ratedsingle integer NOT NULL DEFAULT 150000,
ratedmulti integer NOT NULL DEFAULT 150000,
highlight smallint NOT NULL DEFAULT 1,
pips smallint NOT NULL DEFAULT 1,
moves smallint NOT NULL DEFAULT 0
);
CREATE TABLE game (
id serial PRIMARY KEY,
@ -437,6 +442,68 @@ func recordGameResult(g *bgammon.Game, winType int, account1 int, account2 int,
return err
}
func recordMatchResult(g *bgammon.Game, matchType int, account1 int, account2 int) error {
dbLock.Lock()
defer dbLock.Unlock()
if db == nil || g.Started.IsZero() || g.Winner == 0 || account1 == 0 || account2 == 0 || account1 == account2 {
return nil
}
tx, err := begin()
if err != nil {
return err
}
defer tx.Commit(context.Background())
var columnName string
switch matchType {
case matchTypeCasual:
if g.Points == 1 {
columnName = "casualsingle"
} else {
columnName = "casualmulti"
}
case matchTypeRated:
if g.Points == 1 {
columnName = "ratedsingle"
} else {
columnName = "ratedmulti"
}
default:
log.Panicf("unknown match type: %d", matchType)
}
var rating1i int
err = tx.QueryRow(context.Background(), "SELECT "+columnName+" FROM account WHERE id = $1", account1).Scan(&rating1i)
if err != nil {
return err
}
rating1 := float64(rating1i) / 100
var rating2i int
err = tx.QueryRow(context.Background(), "SELECT "+columnName+" FROM account WHERE id = $1", account2).Scan(&rating2i)
if err != nil {
return err
}
rating2 := float64(rating2i) / 100
outcome1, outcome2 := 1.0, 0.0
if g.Winner == 2 {
outcome1, outcome2 = 0.0, 1.0
}
rating1New, _, _ := glicko2.Rank(rating1, 50, 0.06, []glicko2.Opponent{ratingPlayer{rating2, 30, 0.06, outcome1}}, 0.6)
rating2New, _, _ := glicko2.Rank(rating2, 50, 0.06, []glicko2.Opponent{ratingPlayer{rating1, 30, 0.06, outcome2}}, 0.6)
_, err = tx.Exec(context.Background(), "UPDATE account SET "+columnName+" = $1 WHERE id = $2", int(rating1New*100), account1)
if err != nil {
return err
}
_, err = tx.Exec(context.Background(), "UPDATE account SET "+columnName+" = $1 WHERE id = $2", int(rating2New*100), account2)
return err
}
func matchInfo(id int) (timestamp int64, player1 string, player2 string, replay []byte, err error) {
dbLock.Lock()
defer dbLock.Unlock()

View file

@ -1,5 +1,10 @@
package server
const (
matchTypeCasual = iota
matchTypeRated
)
type serverStatsEntry struct {
Date string
Games int

View file

@ -55,6 +55,10 @@ func recordGameResult(g *bgammon.Game, winType int, account1 int, account2 int,
return nil
}
func recordMatchResult(g *bgammon.Game, matchType int, account1 int, account2 int) error {
return nil
}
func matchHistory(username string) ([]*bgammon.HistoryMatch, error) {
return nil, nil
}

View file

@ -1059,6 +1059,13 @@ COMMANDS:
if err != nil {
log.Fatalf("failed to record game result: %s", err)
}
if !reset {
err := recordMatchResult(clientGame.Game, matchTypeCasual, clientGame.client1.account, clientGame.client2.account)
if err != nil {
log.Fatalf("failed to record match result: %s", err)
}
}
}
if reset {
@ -1365,7 +1372,12 @@ COMMANDS:
log.Fatalf("failed to record game result: %s", err)
}
if reset {
if !reset {
err := recordMatchResult(clientGame.Game, matchTypeCasual, clientGame.client1.account, clientGame.client2.account)
if err != nil {
log.Fatalf("failed to record match result: %s", err)
}
} else {
clientGame.Reset()
clientGame.replay = clientGame.replay[:0]
}
@ -1767,7 +1779,7 @@ COMMANDS:
clientGame.Turn = 1
clientGame.Roll1 = 6
clientGame.Roll2 = 6
clientGame.Board = []int{1, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, -1, 1, -1}
clientGame.Board = []int{1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0}
clientGame.eachClient(func(client *serverClient) {
clientGame.sendBoard(client)
@ -1786,3 +1798,26 @@ func RandInt(max int) int {
}
return int(i.Int64())
}
type ratingPlayer struct {
r float64
rd float64
sigma float64
outcome float64
}
func (p ratingPlayer) R() float64 {
return p.r
}
func (p ratingPlayer) RD() float64 {
return p.rd
}
func (p ratingPlayer) Sigma() float64 {
return p.sigma
}
func (p ratingPlayer) SJ() float64 {
return p.outcome
}