diff --git a/go.mod b/go.mod index 0d793ab..747fb1e 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 4928b23..4d0494d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/server/database.go b/pkg/server/database.go index 83cd3b8..3ac4bb9 100644 --- a/pkg/server/database.go +++ b/pkg/server/database.go @@ -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() diff --git a/pkg/server/database_common.go b/pkg/server/database_common.go index d4274e3..542bf5e 100644 --- a/pkg/server/database_common.go +++ b/pkg/server/database_common.go @@ -1,5 +1,10 @@ package server +const ( + matchTypeCasual = iota + matchTypeRated +) + type serverStatsEntry struct { Date string Games int diff --git a/pkg/server/database_disabled.go b/pkg/server/database_disabled.go index 861fcac..df98849 100644 --- a/pkg/server/database_disabled.go +++ b/pkg/server/database_disabled.go @@ -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 } diff --git a/pkg/server/server.go b/pkg/server/server.go index 7154978..f5d02bf 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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 +}