initial commit
This commit is contained in:
commit
9997ed4fd2
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -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" ]
|
32
Makefile
Normal file
32
Makefile
Normal file
@ -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
|
9
README.md
Normal file
9
README.md
Normal file
@ -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`.
|
16
cmd/beerpong-elo.go
Normal file
16
cmd/beerpong-elo.go
Normal file
@ -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{})
|
||||||
|
}
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -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
|
13
go.sum
Normal file
13
go.sum
Normal file
@ -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=
|
45
internal/beerpong-elo.go
Normal file
45
internal/beerpong-elo.go
Normal file
@ -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
|
||||||
|
}
|
54
internal/elo.go
Normal file
54
internal/elo.go
Normal file
@ -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
|
||||||
|
}
|
19
internal/model/game.go
Normal file
19
internal/model/game.go
Normal file
@ -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"`
|
||||||
|
}
|
7
internal/model/gameResult.go
Normal file
7
internal/model/gameResult.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type GameResultID string
|
||||||
|
|
||||||
|
type GameResult struct {
|
||||||
|
ID GameResultID
|
||||||
|
}
|
16
internal/model/inputGame.go
Normal file
16
internal/model/inputGame.go
Normal file
@ -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"`
|
||||||
|
}
|
13
internal/model/player.go
Normal file
13
internal/model/player.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type PlayerID string
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ID PlayerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayer(id PlayerID) Player {
|
||||||
|
return Player{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
}
|
93
internal/repo/repo.go
Normal file
93
internal/repo/repo.go
Normal file
@ -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
|
||||||
|
}
|
27
internal/web/web.go
Normal file
27
internal/web/web.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user