From 9997ed4fd200c9d2b8fd7ecab23db266108adc76 Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Sun, 5 Jan 2025 02:53:54 +0100 Subject: [PATCH] initial commit --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 22 +++++++++ Makefile | 32 +++++++++++++ README.md | 9 ++++ cmd/beerpong-elo.go | 16 +++++++ go.mod | 7 +++ go.sum | 13 +++++ internal/beerpong-elo.go | 45 +++++++++++++++++ internal/elo.go | 54 +++++++++++++++++++++ internal/model/game.go | 19 ++++++++ internal/model/gameResult.go | 7 +++ internal/model/inputGame.go | 16 +++++++ internal/model/player.go | 13 +++++ internal/repo/repo.go | 93 ++++++++++++++++++++++++++++++++++++ internal/web/web.go | 27 +++++++++++ 16 files changed, 375 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/beerpong-elo.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/beerpong-elo.go create mode 100644 internal/elo.go create mode 100644 internal/model/game.go create mode 100644 internal/model/gameResult.go create mode 100644 internal/model/inputGame.go create mode 100644 internal/model/player.go create mode 100644 internal/repo/repo.go create mode 100644 internal/web/web.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca622c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM --platform=$BUILDPLATFORM golang:1.21-alpine as build + +ADD . /app +WORKDIR /app + +ARG TARGETARCH +ARG TARGETOS + +RUN apk add --no-cache make +RUN make build BUILD_ARCH="$TARGETARCH" BUILD_OS="$TARGETOS" + +FROM --platform=$TARGETPLATFORM alpine:latest + +RUN apk add --no-cache make ca-certificates + +WORKDIR /data +COPY --from=build /app/build/beerpong-elo /app/beerpong-elo + +EXPOSE 3000 +VOLUME [ "/data" ] + +CMD [ "/app/beerpong-elo" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2cbb7ed --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +BINARY = beerpong-elo +BUILD_DIR = build +BUILD_ARCH = $(shell go env GOARCH) +BUILD_OS = $(shell go env GOOS) +DOCKER_BUILD_FLAGS = +DOCKER_BUILD_PLATFORM = linux/amd64,linux/arm64,linux/arm32v6,linux/arm32v7 +DOCKER_BUILD_TAG = djeeberjr/beerpong-elo + +.PHONY: all +all: clean build + +.PHONY:build +build: $(BUILD_DIR)/$(BINARY) + +$(BUILD_DIR)/$(BINARY): + GOARCH=$(BUILD_ARCH) GOOS=$(BUILD_OS) go build -o $(BUILD_DIR)/$(BINARY) cmd/beerpong-elo.go + +.PHONY:clean +clean: + rm -rf $(BUILD_DIR) + +.PHONY: dev +dev: + go run cmd/beerpong-elo.go + +.PHONY: docker-push +docker-push: + docker buildx build $(DOCKER_BUILD_FLAGS) --platform $(DOCKER_BUILD_PLATFORM) -t $(DOCKER_BUILD_TAG) . --push + +.PHONY: docker-local +docker-local: + docker buildx build $(DOCKER_BUILD_FLAGS) -t $(DOCKER_BUILD_TAG) . --load diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce1a9e4 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# beerpong-elo + +## Build + +`make build` + +## Build and push docker image + +To build the image localy run `make docker-local` and to build and push `make docker-push`. diff --git a/cmd/beerpong-elo.go b/cmd/beerpong-elo.go new file mode 100644 index 0000000..b87b343 --- /dev/null +++ b/cmd/beerpong-elo.go @@ -0,0 +1,16 @@ +package main + +import ( + beerpongelo "git.kapelle.org/niklas/beerpong-elo/internal" + "github.com/alexflint/go-arg" +) + +type args struct { +} + +func main() { + var args args + arg.MustParse(&args) + + beerpongelo.Start(beerpongelo.Config{}) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2de42b8 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.kapelle.org/niklas/beerpong-elo + +go 1.22.6 + +require github.com/alexflint/go-arg v1.5.1 + +require github.com/alexflint/go-scalar v1.2.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43a8011 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/beerpong-elo.go b/internal/beerpong-elo.go new file mode 100644 index 0000000..338efa1 --- /dev/null +++ b/internal/beerpong-elo.go @@ -0,0 +1,45 @@ +package beerpongelo + +import ( + "encoding/json" + "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 { +} + +func Start(config Config) { + + repo := repo.NewInMemoryRepo() + + loadFromFile(repo, "./data.json") + + mux := web.CreateWebserver(repo) + + http.ListenAndServe(":8080", mux) +} + +func loadFromFile(repo repo.Repo, path string) error { + content, err := os.ReadFile(path) + + if err != nil { + return err + } + + var payload []model.InputGame + err = json.Unmarshal(content, &payload) + if err != nil { + return err + } + + for _, game := range payload { + repo.AddGame(game) + } + + return nil +} diff --git a/internal/elo.go b/internal/elo.go new file mode 100644 index 0000000..7977572 --- /dev/null +++ b/internal/elo.go @@ -0,0 +1,54 @@ +package beerpongelo + +import ( + "fmt" + "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) + fmt.Printf("%d: %f\n", score, scoreMod) + + 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 +} diff --git a/internal/model/game.go b/internal/model/game.go new file mode 100644 index 0000000..4beaa5e --- /dev/null +++ b/internal/model/game.go @@ -0,0 +1,19 @@ +package model + +import ( + "time" +) + +type GameID string + +type Game struct { + ID GameID + 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"` +} diff --git a/internal/model/gameResult.go b/internal/model/gameResult.go new file mode 100644 index 0000000..f071b9a --- /dev/null +++ b/internal/model/gameResult.go @@ -0,0 +1,7 @@ +package model + +type GameResultID string + +type GameResult struct { + ID GameResultID +} diff --git a/internal/model/inputGame.go b/internal/model/inputGame.go new file mode 100644 index 0000000..187c485 --- /dev/null +++ b/internal/model/inputGame.go @@ -0,0 +1,16 @@ +package model + +import ( + "time" +) + +type InputGame struct { + Added time.Time `json:"added"` + Author string `json:"author"` + Team0Player0 string `json:"t0p0"` + Team0Player1 string `json:"t0p1"` + Team1Player0 string `json:"t1p0"` + Team1Player1 string `json:"t1p1"` + Score int `json:"score"` + Overtime bool `jsone:"ot"` +} diff --git a/internal/model/player.go b/internal/model/player.go new file mode 100644 index 0000000..ee80ee5 --- /dev/null +++ b/internal/model/player.go @@ -0,0 +1,13 @@ +package model + +type PlayerID string + +type Player struct { + ID PlayerID +} + +func NewPlayer(id PlayerID) Player { + return Player{ + ID: id, + } +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go new file mode 100644 index 0000000..7b1d6d9 --- /dev/null +++ b/internal/repo/repo.go @@ -0,0 +1,93 @@ +package repo + +import ( + "strconv" + + model "git.kapelle.org/niklas/beerpong-elo/internal/model" +) + +type Repo interface { + AddGame(game model.InputGame) model.GameID + GetGame(id model.GameID) model.Game + GetAllGames() []model.Game + + GetOrCreatePlayerID(name string) model.PlayerID + GetPlayer(id model.PlayerID) *model.Player +} + +type InMemoryRepo struct { + games []model.Game + players []model.Player +} + +func NewInMemoryRepo() Repo { + return &InMemoryRepo{ + games: []model.Game{}, + players: []model.Player{}, + } +} + +func (r *InMemoryRepo) AddGame(game model.InputGame) model.GameID { + id := len(r.games) + + authID := r.GetOrCreatePlayerID(game.Author) + t0p0ID := r.GetOrCreatePlayerID(game.Team0Player0) + t0p1ID := r.GetOrCreatePlayerID(game.Team0Player1) + t1p0ID := r.GetOrCreatePlayerID(game.Team1Player0) + t1p1ID := r.GetOrCreatePlayerID(game.Team1Player1) + + parsedGame := model.Game{ + ID: model.GameID(rune(id)), + Added: game.Added, + Author: authID, + Team0Player0: t0p0ID, + Team0Player1: t0p1ID, + Team1Player0: t1p1ID, + Team1Player1: t1p0ID, + Score: game.Score, + Overtime: game.Overtime, + } + + r.games = append(r.games, parsedGame) + + return model.GameID((rune(id))) +} + +func (r *InMemoryRepo) GetGame(id model.GameID) model.Game { + i, err := strconv.Atoi(string(id)) + + if err != nil { + panic(err) + } + + return r.games[i] +} + +func (r *InMemoryRepo) GetAllGames() []model.Game { + return r.games +} + +func (r *InMemoryRepo) GetOrCreatePlayerID(name string) model.PlayerID { + id := model.PlayerID(name) + + for _, player := range r.players { + if player.ID == id { + return id + } + } + + // No player found. Create one. + r.players = append(r.players, model.NewPlayer(id)) + + return id +} + +func (r *InMemoryRepo) GetPlayer(id model.PlayerID) *model.Player { + for _, player := range r.players { + if player.ID == id { + return &player + } + } + + return nil +} diff --git a/internal/web/web.go b/internal/web/web.go new file mode 100644 index 0000000..ff93822 --- /dev/null +++ b/internal/web/web.go @@ -0,0 +1,27 @@ +package web + +import ( + "encoding/json" + "fmt" + "net/http" + + "git.kapelle.org/niklas/beerpong-elo/internal/repo" +) + +func CreateWebserver(repo repo.Repo) *http.ServeMux { + router := http.NewServeMux() + + router.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path) + }) + + router.HandleFunc("GET /games", func(w http.ResponseWriter, r *http.Request) { + games := repo.GetAllGames() + + w.Header().Set("Content-Type", "application/json") + + json.NewEncoder(w).Encode(games) + }) + + return router +}