initial commit
This commit is contained in:
71
internal/beerpong-elo.go
Normal file
71
internal/beerpong-elo.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/web"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBHost string
|
||||
DBUsername string
|
||||
DBPassword string
|
||||
DBName string
|
||||
Address string
|
||||
}
|
||||
|
||||
func Start(config Config) {
|
||||
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
|
||||
mux := web.CreateWebserver(repo)
|
||||
|
||||
http.ListenAndServe(config.Address, mux)
|
||||
}
|
||||
|
||||
func Import(config Config, file string) {
|
||||
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to import file %s: %s", file, err)
|
||||
}
|
||||
|
||||
controller := newController(repo)
|
||||
err = loadFromFile(controller, file)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to import file %s: %s", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadFromFile(c Controller, path string) error {
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload []model.Game
|
||||
err = json.Unmarshal(content, &payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, game := range payload {
|
||||
_, err = c.AddGame(game)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Imported %d games", len(payload))
|
||||
return nil
|
||||
}
|
||||
142
internal/controller.go
Normal file
142
internal/controller.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
repo repo.Repo
|
||||
}
|
||||
|
||||
func newController(repo repo.Repo) Controller {
|
||||
return Controller{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) AddGame(game model.Game) (model.GameID, error) {
|
||||
parsedGame := model.Game{
|
||||
Added: game.Added,
|
||||
Score: game.Score,
|
||||
Overtime: game.Overtime,
|
||||
}
|
||||
|
||||
author, err := c.repo.GetOrCreatePlayerID(string(game.Author))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Author = author
|
||||
|
||||
t0p0, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player0))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team0Player0 = t0p0
|
||||
|
||||
t0p1, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team0Player1 = t0p1
|
||||
|
||||
t1p0, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player0))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team1Player0 = t1p0
|
||||
|
||||
t1p1, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team1Player1 = t1p1
|
||||
|
||||
id, err := c.repo.AddGame(parsedGame)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parsedGame.ID = id
|
||||
|
||||
err = c.createGameResult(parsedGame)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *Controller) createGameResult(game model.Game) error {
|
||||
t0p0, err := c.repo.GetPlayer(game.Team0Player0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t0p1, err := c.repo.GetPlayer(game.Team0Player1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t1p0, err := c.repo.GetPlayer(game.Team1Player0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t1p1, err := c.repo.GetPlayer(game.Team1Player1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newT0p0, newT0p1, newT1p0, newT1p1 := NewRating(float64(t0p0.Elo), float64(t0p1.Elo), float64(t1p0.Elo), float64(t1p1.Elo), game.Score)
|
||||
|
||||
t0p0Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t0p0.ID,
|
||||
StartElo: t0p0.Elo,
|
||||
EndElo: int(math.Round(newT0p0)),
|
||||
}
|
||||
|
||||
t0p1Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t0p1.ID,
|
||||
StartElo: t0p1.Elo,
|
||||
EndElo: int(math.Round(newT0p1)),
|
||||
}
|
||||
|
||||
t1p0Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t1p0.ID,
|
||||
StartElo: t1p0.Elo,
|
||||
EndElo: int(math.Round(newT1p0)),
|
||||
}
|
||||
|
||||
t1p1Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t1p1.ID,
|
||||
StartElo: t1p1.Elo,
|
||||
EndElo: int(math.Round(newT1p1)),
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t0p0Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t0p1Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t1p0Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t1p1Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
52
internal/elo.go
Normal file
52
internal/elo.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func winProbPlayer(p, e1, e2 float64) float64 {
|
||||
const MAGIC float64 = 500
|
||||
|
||||
lhs := 1 / (1 + math.Pow(10., (p-e1)/MAGIC))
|
||||
rhs := 1 / (1 + math.Pow(10., (p-e2)/MAGIC))
|
||||
return (lhs + rhs) / 2.
|
||||
}
|
||||
|
||||
func NewRating(t0p0, t0p1, t1p0, t1p1 float64, score int) (float64, float64, float64, float64) {
|
||||
const K float64 = 50
|
||||
const SCORE_BIAS float64 = 1
|
||||
const SCORE_SCALE float64 = 0.4
|
||||
const MAX_SCORE float64 = 10
|
||||
|
||||
et0p0 := winProbPlayer(t0p0, t1p0, t1p1)
|
||||
et0p1 := winProbPlayer(t0p1, t1p0, t1p1)
|
||||
et1p0 := winProbPlayer(t1p0, t0p0, t0p1)
|
||||
et1p1 := winProbPlayer(t1p1, t0p0, t0p1)
|
||||
|
||||
et0 := (et0p0 + et0p1) / 2.
|
||||
et1 := (et1p0 + et1p1) / 2.
|
||||
|
||||
ct0p0 := t0p0 / (t0p0 + t0p1)
|
||||
ct0p1 := t0p1 / (t0p0 + t0p1)
|
||||
ct1p0 := t1p0 / (t1p0 + t1p1)
|
||||
ct1p1 := t1p1 / (t1p0 + t1p1)
|
||||
|
||||
var st0 float64
|
||||
var st1 float64
|
||||
if score > 0 {
|
||||
st0 = 1
|
||||
st1 = 0
|
||||
} else {
|
||||
st0 = 0
|
||||
st1 = 1
|
||||
}
|
||||
|
||||
scoreMod := SCORE_SCALE * math.Log(math.Abs(float64(score))+SCORE_BIAS) / (math.Log(MAX_SCORE) + SCORE_BIAS)
|
||||
|
||||
newt0p0 := t0p0 + K*scoreMod*((st0*ct0p0)-(et0*ct0p0))
|
||||
newt0p1 := t0p1 + K*scoreMod*((st0*ct0p1)-(et0*ct0p1))
|
||||
newt1p0 := t1p0 + K*scoreMod*((st1*ct1p0)-(et1*ct1p0))
|
||||
newt1p1 := t1p1 + K*scoreMod*((st1*ct1p1)-(et1*ct1p1))
|
||||
|
||||
return newt0p0, newt0p1, newt1p0, newt1p1
|
||||
}
|
||||
33
internal/model/game.go
Normal file
33
internal/model/game.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GameID string
|
||||
|
||||
type Game struct {
|
||||
ID GameID `json:"id"`
|
||||
Added time.Time `json:"added"`
|
||||
Author PlayerID `json:"author"`
|
||||
Team0Player0 PlayerID `json:"t0p0"`
|
||||
Team0Player1 PlayerID `json:"t0p1"`
|
||||
Team1Player0 PlayerID `json:"t1p0"`
|
||||
Team1Player1 PlayerID `json:"t1p1"`
|
||||
Score int `json:"score"`
|
||||
Overtime bool `jsone:"ot"`
|
||||
}
|
||||
|
||||
func (id *GameID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = GameID(fmt.Sprintf("%d", v))
|
||||
return nil
|
||||
case string:
|
||||
*id = GameID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for GameID", src)
|
||||
}
|
||||
}
|
||||
26
internal/model/gameResult.go
Normal file
26
internal/model/gameResult.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GameResultID string
|
||||
|
||||
type GameResult struct {
|
||||
ID GameResultID
|
||||
Game GameID
|
||||
Player PlayerID
|
||||
StartElo int
|
||||
EndElo int
|
||||
}
|
||||
|
||||
func (id *GameResultID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = GameResultID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
|
||||
return nil
|
||||
case string:
|
||||
*id = GameResultID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for GameResultID", src)
|
||||
}
|
||||
}
|
||||
31
internal/model/player.go
Normal file
31
internal/model/player.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type PlayerID string
|
||||
|
||||
type Player struct {
|
||||
ID PlayerID
|
||||
Name string
|
||||
Elo int
|
||||
}
|
||||
|
||||
func NewPlayer(name string) Player {
|
||||
return Player{
|
||||
Name: name,
|
||||
Elo: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
func (id *PlayerID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = PlayerID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
|
||||
return nil
|
||||
case string:
|
||||
*id = PlayerID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for PlayerID", src)
|
||||
}
|
||||
}
|
||||
40
internal/repo/repo.go
Normal file
40
internal/repo/repo.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
model "git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
)
|
||||
|
||||
type Repo interface {
|
||||
// Add a new game to the repo. ID needs to be nil on argument. PlayerID are fetched beforehand.
|
||||
AddGame(model.Game) (model.GameID, error)
|
||||
|
||||
// Get a game by ID. Returns nil if not found.
|
||||
GetGame(model.GameID) (*model.Game, error)
|
||||
|
||||
// Get ID of a player. Creates one if not found.
|
||||
GetOrCreatePlayerID(string) (model.PlayerID, error)
|
||||
|
||||
// Get player by id. Returns nil if not found.
|
||||
GetPlayer(model.PlayerID) (*model.Player, error)
|
||||
|
||||
// Adds a game result. ID needs to be nil on argument.
|
||||
AddGameResult(model.GameResult) (model.GameResultID, error)
|
||||
|
||||
// Get a game result by ID. Returns nil if not found.
|
||||
GetGameResult(model.GameResultID) (*model.GameResult, error)
|
||||
|
||||
// Get a game result for a player in a game
|
||||
GetGameResultForPlayerAndGame(model.GameID, model.PlayerID) (*model.GameResult, error)
|
||||
|
||||
// Get all game results for a player
|
||||
GetGameResultsForPlayer(model.PlayerID) ([]*model.GameResult, error)
|
||||
|
||||
// Get games for a player
|
||||
GetGamesForPlayer(model.PlayerID) ([]*model.Game, error)
|
||||
|
||||
// Get all players
|
||||
GetPlayers() ([]*model.Player, error)
|
||||
|
||||
// Get all games
|
||||
GetGames() ([]*model.Game, error)
|
||||
}
|
||||
279
internal/repo/sqlRepo.go
Normal file
279
internal/repo/sqlRepo.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type SQLRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQLRepo(host, username, password, dbName string) (Repo, error) {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", username, password, host, dbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(time.Minute * 3)
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(10)
|
||||
|
||||
return &SQLRepo{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) AddGame(game model.Game) (model.GameID, error) {
|
||||
stmt, err := s.db.Prepare("INSERT INTO Games(added,score,overtime,author,team0player0,team0player1,team1player0,team1player1) VALUES (?,?,?,?,?,?,?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := stmt.Exec(game.Added, game.Score, game.Overtime, game.Author, game.Team0Player0, game.Team0Player1, game.Team1Player0, game.Team1Player1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gameID model.GameID
|
||||
gameID.Scan(id)
|
||||
|
||||
return gameID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) AddGameResult(gameResult model.GameResult) (model.GameResultID, error) {
|
||||
stms, err := s.db.Prepare("INSERT INTO GameResults(game,player,startElo,endElo) VALUES (?,?,?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := stms.Exec(gameResult.Game, gameResult.Player, gameResult.StartElo, gameResult.EndElo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gameResultID model.GameResultID
|
||||
gameResultID.Scan(id)
|
||||
|
||||
return gameResultID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGame(id model.GameID) (*model.Game, error) {
|
||||
rows := s.db.QueryRow("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games WHERE id = ? ", id)
|
||||
|
||||
var game model.Game
|
||||
|
||||
if err := rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &game, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResult(id model.GameResultID) (*model.GameResult, error) {
|
||||
rows := s.db.QueryRow("SELECT game,player,startElo,endElo FROM GameResults WHERE id = ? ", id)
|
||||
|
||||
var gameResult model.GameResult
|
||||
gameResult.ID = id
|
||||
|
||||
if err := rows.Scan(&gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gameResult, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) playerWithNameExists(name string) (*model.PlayerID, error) {
|
||||
rows := s.db.QueryRow("SELECT id FROM Players WHERE name = ?", name)
|
||||
|
||||
var id model.PlayerID
|
||||
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetOrCreatePlayerID(name string) (model.PlayerID, error) {
|
||||
foundID, err := s.playerWithNameExists(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if foundID != nil {
|
||||
return *foundID, nil
|
||||
}
|
||||
|
||||
stmt, err := s.db.Prepare("INSERT INTO Players(name,elo) VALUES (?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newPlayer := model.NewPlayer(name)
|
||||
|
||||
res, err := stmt.Exec(newPlayer.Name, newPlayer.Elo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var playerID model.PlayerID
|
||||
playerID.Scan(id)
|
||||
|
||||
return playerID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetPlayer(id model.PlayerID) (*model.Player, error) {
|
||||
rows := s.db.QueryRow("SELECT name,elo from Players WHERE id = ?", id)
|
||||
|
||||
var player model.Player
|
||||
player.ID = id
|
||||
|
||||
if err := rows.Scan(&player.Name, &player.Elo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &player, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGamesForPlayer(id model.PlayerID) ([]*model.Game, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1
|
||||
FROM Games
|
||||
WHERE team0player0 = ? OR team0player1 = ? OR team1player0 = ? OR team1player1 = ?
|
||||
`, id, id, id, id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
rtn := []*model.Game{}
|
||||
|
||||
for rows.Next() {
|
||||
var game model.Game
|
||||
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &game)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResultForPlayerAndGame(gameID model.GameID, playerID model.PlayerID) (*model.GameResult, error) {
|
||||
rows := s.db.QueryRow("SELECT id,game,player,startElo,endElo FROM GameResults WHERE game = ? AND player = ? ", gameID, playerID)
|
||||
|
||||
var gameResult model.GameResult
|
||||
|
||||
if err := rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gameResult, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResultsForPlayer(id model.PlayerID) ([]*model.GameResult, error) {
|
||||
rows, err := s.db.Query("SELECT id,game,player,startElo,endElo FROM GameResults WHERE player = ? ", id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.GameResult{}
|
||||
|
||||
for rows.Next() {
|
||||
var gameResult model.GameResult
|
||||
|
||||
err = rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &gameResult)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetPlayers() ([]*model.Player, error) {
|
||||
rows, err := s.db.Query("SELECT id, name, elo FROM Players")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.Player{}
|
||||
|
||||
for rows.Next() {
|
||||
var player model.Player
|
||||
|
||||
err = rows.Scan(&player.ID, &player.Name, &player.Elo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &player)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGames() ([]*model.Game, error) {
|
||||
rows, err := s.db.Query("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.Game{}
|
||||
|
||||
for rows.Next() {
|
||||
var game model.Game
|
||||
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &game)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
44
internal/repo/sqlSchema.sql
Normal file
44
internal/repo/sqlSchema.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
CREATE TABLE Players (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
elo INT
|
||||
);
|
||||
|
||||
CREATE TABLE Games (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
added DATETIME NOT NULL,
|
||||
score INT NOT NULL,
|
||||
overtime BOOL NOT NULL,
|
||||
author INT,
|
||||
team0player0 INT,
|
||||
team0player1 INT,
|
||||
team1player0 INT,
|
||||
team1player1 INT,
|
||||
FOREIGN KEY (author) REFERENCES Players(id) on DELETE SET NULL,
|
||||
FOREIGN KEY (team0player0) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team0player1) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team1player0) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team1player1) REFERENCES Players(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE GameResults (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
game INT,
|
||||
player INT,
|
||||
startElo INT,
|
||||
endElo INT,
|
||||
FOREIGN KEY (game) REFERENCES Games(id) on DELETE SET NULL,
|
||||
FOREIGN KEY (player) REFERENCES Players(id) on DELETE SET NULL
|
||||
)
|
||||
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER UpdateCurrentELO
|
||||
AFTER INSERT ON GameResults
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE Players
|
||||
SET elo = NEW.endElo
|
||||
WHERE id = NEW.player;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
369
internal/web/gqlSchema.go
Normal file
369
internal/web/gqlSchema.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"github.com/graphql-go/graphql"
|
||||
)
|
||||
|
||||
func createShema(repo repo.Repo) graphql.Schema {
|
||||
|
||||
player := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Player",
|
||||
Description: "A player. Can also be authors of games",
|
||||
Fields: graphql.Fields{
|
||||
"ID": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"name": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.String),
|
||||
},
|
||||
"elo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
|
||||
// Field "games" added below
|
||||
// Field "history" added below
|
||||
},
|
||||
})
|
||||
|
||||
game := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Game",
|
||||
Description: "A game played",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"added": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.DateTime),
|
||||
},
|
||||
"overtime": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Boolean),
|
||||
},
|
||||
"score": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"author": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'author' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Author)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team0player0": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team0player0' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team0Player0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team0player1": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team0player1' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team0Player1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team1player0": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team1player0' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team1Player0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team1player1": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team1player1' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team1Player1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
player.AddFieldConfig("games", &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(game)),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
player, ok := p.Source.(*model.Player)
|
||||
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'games' on 'Player' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
games, err := repo.GetGamesForPlayer(player.ID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return games, nil
|
||||
},
|
||||
})
|
||||
|
||||
gameResult := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "GameResult",
|
||||
Description: "The ELO change for a player in a game",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"player": &graphql.Field{
|
||||
Type: player,
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'player' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(gameResult.Player)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"game": &graphql.Field{
|
||||
Type: graphql.NewNonNull(game),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'game' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
game, err := repo.GetGame(gameResult.Game)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return game, nil
|
||||
},
|
||||
},
|
||||
"startElo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"endElo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"delta": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'delta' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
delta := gameResult.EndElo - gameResult.StartElo
|
||||
return delta, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
player.AddFieldConfig("history", &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(gameResult)),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"game": &graphql.ArgumentConfig{
|
||||
Type: graphql.ID,
|
||||
Description: "Filter by Game ID",
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
player, ok := p.Source.(*model.Player)
|
||||
if !ok {
|
||||
log.Printf("Can't get source from field 'history' on 'player' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if p.Args["game"] != nil {
|
||||
// Find result for single game
|
||||
gameID, ok := p.Args["game"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse 'game' at field 'history' on 'player' is %v", p.Args["game"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gameResult, err := repo.GetGameResultForPlayerAndGame(model.GameID(gameID), player.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []*model.GameResult{gameResult}, nil
|
||||
}
|
||||
|
||||
// Find all gameResults for a player
|
||||
|
||||
gameResults, err := repo.GetGameResultsForPlayer(player.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gameResults, nil
|
||||
},
|
||||
})
|
||||
|
||||
queryFields := graphql.Fields{
|
||||
"player": &graphql.Field{
|
||||
Type: player,
|
||||
Description: "Get player by ID",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at player: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(model.PlayerID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"game": &graphql.Field{
|
||||
Type: game,
|
||||
Description: "Get game by ID",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at game: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
game, err := repo.GetGame(model.GameID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return game, nil
|
||||
},
|
||||
},
|
||||
"gameResult": &graphql.Field{
|
||||
Type: gameResult,
|
||||
Description: "Result of a game on a players ELO",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at gameResult: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gameResult, err := repo.GetGameResult(model.GameResultID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gameResult, nil
|
||||
},
|
||||
},
|
||||
"players": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(player)),
|
||||
Description: "Get all players",
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
players, err := repo.GetPlayers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return players, nil
|
||||
},
|
||||
},
|
||||
"games": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(game)),
|
||||
Description: "Get all games",
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
games, err := repo.GetGames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return games, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
querySchema := graphql.ObjectConfig{
|
||||
Name: "Query",
|
||||
Fields: queryFields,
|
||||
}
|
||||
|
||||
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(querySchema)}
|
||||
schema, err := graphql.NewSchema(schemaConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create new schema, error: %v", err)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
28
internal/web/staticFiles.go
Normal file
28
internal/web/staticFiles.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
type spaFileSystem struct {
|
||||
root http.FileSystem
|
||||
}
|
||||
|
||||
func (spa *spaFileSystem) Open(name string) (http.File, error) {
|
||||
f, err := spa.root.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
return spa.root.Open("index.html")
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func initStatic(mux *http.ServeMux) {
|
||||
staticFS, _ := fs.Sub(staticFiles, "static")
|
||||
mux.Handle("/", http.FileServer(&spaFileSystem{http.FS(staticFS)}))
|
||||
}
|
||||
24
internal/web/web.go
Normal file
24
internal/web/web.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"github.com/graphql-go/handler"
|
||||
)
|
||||
|
||||
func CreateWebserver(repo repo.Repo) *http.ServeMux {
|
||||
router := http.NewServeMux()
|
||||
schema := createShema(repo)
|
||||
|
||||
gqlHandler := handler.New(&handler.Config{
|
||||
Schema: &schema,
|
||||
Pretty: true,
|
||||
GraphiQL: true,
|
||||
})
|
||||
|
||||
router.Handle("/graphql", gqlHandler)
|
||||
initStatic(router)
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user