Create MVP ELO tracker with front-end app & Containerfile
parent
d8c20fd4cc
commit
2ace9688ff
|
|
@ -10,6 +10,9 @@ spec/
|
||||||
.venv-pyserver
|
.venv-pyserver
|
||||||
.venv-pyclient
|
.venv-pyclient
|
||||||
|
|
||||||
|
# Compiled elm files
|
||||||
|
elo_tracker/static/elo_tracker.js
|
||||||
|
|
||||||
# ---> Elm
|
# ---> Elm
|
||||||
# elm-package generated files
|
# elm-package generated files
|
||||||
elm-stuff
|
elm-stuff
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
.venv-pyserver
|
.venv-pyserver
|
||||||
.venv-pyclient
|
.venv-pyclient
|
||||||
|
|
||||||
|
# Compiled elm files
|
||||||
|
elo_tracker/static/elo_tracker.js
|
||||||
|
|
||||||
# ---> Elm
|
# ---> Elm
|
||||||
# elm-package generated files
|
# elm-package generated files
|
||||||
elm-stuff
|
elm-stuff
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
module Api.Elo exposing
|
||||||
|
( EloStat
|
||||||
|
, HealthCheck
|
||||||
|
, Leaderboard
|
||||||
|
, Match
|
||||||
|
, Participant
|
||||||
|
, Players
|
||||||
|
, getHealth
|
||||||
|
, getLeaderboard
|
||||||
|
, getMatches
|
||||||
|
, getPlayers
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# ELO API
|
||||||
|
|
||||||
|
This module contains functions to address the API of the ELO tracker.
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Http
|
||||||
|
import Json.Decode as D
|
||||||
|
|
||||||
|
|
||||||
|
type alias EloStat =
|
||||||
|
{ draws : Int
|
||||||
|
, elo : Int
|
||||||
|
, losses : Int
|
||||||
|
, name : String
|
||||||
|
, url : String
|
||||||
|
, wins : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias HealthCheck =
|
||||||
|
{ ok : Bool, periodicMatches : Bool }
|
||||||
|
|
||||||
|
|
||||||
|
type alias Leaderboard =
|
||||||
|
{ name : String, players : Players }
|
||||||
|
|
||||||
|
|
||||||
|
type alias Match =
|
||||||
|
{ name : String
|
||||||
|
, participants : List Participant
|
||||||
|
, timestamp : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Participant =
|
||||||
|
{ name : String
|
||||||
|
, result : String
|
||||||
|
, url : String
|
||||||
|
, version : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Players =
|
||||||
|
List EloStat
|
||||||
|
|
||||||
|
|
||||||
|
eloStatDecoder : D.Decoder EloStat
|
||||||
|
eloStatDecoder =
|
||||||
|
D.map6 EloStat
|
||||||
|
(D.field "draws" D.int)
|
||||||
|
(D.field "elo" D.int)
|
||||||
|
(D.field "losses" D.int)
|
||||||
|
(D.field "name" D.string)
|
||||||
|
(D.field "url" D.string)
|
||||||
|
(D.field "wins" D.int)
|
||||||
|
|
||||||
|
|
||||||
|
getHealth :
|
||||||
|
{ baseUrl : String
|
||||||
|
, toMsg : Result Http.Error HealthCheck -> msg
|
||||||
|
}
|
||||||
|
-> Cmd msg
|
||||||
|
getHealth data =
|
||||||
|
Http.get
|
||||||
|
{ url = data.baseUrl ++ "/health"
|
||||||
|
, expect = Http.expectJson data.toMsg healthCheckDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getLeaderboard :
|
||||||
|
{ baseUrl : String
|
||||||
|
, toMsg : Result Http.Error Leaderboard -> msg
|
||||||
|
}
|
||||||
|
-> Cmd msg
|
||||||
|
getLeaderboard data =
|
||||||
|
Http.get
|
||||||
|
{ url = data.baseUrl ++ "/leaderboard"
|
||||||
|
, expect = Http.expectJson data.toMsg leaderboardDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getMatches :
|
||||||
|
{ baseUrl : String
|
||||||
|
, toMsg : Result Http.Error (List Match) -> msg
|
||||||
|
}
|
||||||
|
-> Cmd msg
|
||||||
|
getMatches data =
|
||||||
|
Http.get
|
||||||
|
{ url = data.baseUrl ++ "/matches"
|
||||||
|
, expect = Http.expectJson data.toMsg (D.list matchDecoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getPlayers :
|
||||||
|
{ baseUrl : String
|
||||||
|
, toMsg : Result Http.Error Players -> msg
|
||||||
|
}
|
||||||
|
-> Cmd msg
|
||||||
|
getPlayers data =
|
||||||
|
Http.get
|
||||||
|
{ url = data.baseUrl ++ "/players"
|
||||||
|
, expect = Http.expectJson data.toMsg playersDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
healthCheckDecoder : D.Decoder HealthCheck
|
||||||
|
healthCheckDecoder =
|
||||||
|
D.map2 HealthCheck
|
||||||
|
(D.field "ok" D.bool)
|
||||||
|
(D.field "periodic_matches" D.bool)
|
||||||
|
|
||||||
|
|
||||||
|
leaderboardDecoder : D.Decoder Leaderboard
|
||||||
|
leaderboardDecoder =
|
||||||
|
D.map2 Leaderboard
|
||||||
|
(D.field "name" D.string)
|
||||||
|
(D.field "players" playersDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
matchDecoder : D.Decoder Match
|
||||||
|
matchDecoder =
|
||||||
|
D.map3 Match
|
||||||
|
(D.field "name" D.string)
|
||||||
|
(D.field "participants" <| D.list participantDecoder)
|
||||||
|
(D.field "timestamp" D.string)
|
||||||
|
|
||||||
|
|
||||||
|
participantDecoder : D.Decoder Participant
|
||||||
|
participantDecoder =
|
||||||
|
D.map4 Participant
|
||||||
|
(D.field "name" D.string)
|
||||||
|
(D.field "result" D.string)
|
||||||
|
(D.field "url" D.string)
|
||||||
|
(D.maybe <| D.field "version" D.string)
|
||||||
|
|
||||||
|
|
||||||
|
playersDecoder : D.Decoder Players
|
||||||
|
playersDecoder =
|
||||||
|
D.list eloStatDecoder
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
module EloTracker exposing (main)
|
||||||
|
|
||||||
|
import Api.Elo as Elo
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as D
|
||||||
|
import Program
|
||||||
|
import Time
|
||||||
|
import Widget.Icon
|
||||||
|
import Layout
|
||||||
|
import Theme
|
||||||
|
import Screen.Leaderboard
|
||||||
|
import Element.Font
|
||||||
|
import Pixels
|
||||||
|
import Element.Background
|
||||||
|
|
||||||
|
|
||||||
|
main =
|
||||||
|
Program.document
|
||||||
|
{ flagsDecoder = D.field "base_url" D.string
|
||||||
|
, headers = headers
|
||||||
|
, init = init
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
, title = title
|
||||||
|
, update = update
|
||||||
|
, view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- MODEL
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ baseUrl : String
|
||||||
|
, leaderboard : Result Int Elo.Leaderboard
|
||||||
|
, matches : Result Int (List Elo.Match)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= OnLeaderboard (Result Http.Error Elo.Leaderboard)
|
||||||
|
| OnMatches (Result Http.Error (List Elo.Match))
|
||||||
|
| RefreshLeaderboard
|
||||||
|
| RefreshMatches
|
||||||
|
|
||||||
|
|
||||||
|
init : Result D.Error String -> ( Model, Cmd Msg )
|
||||||
|
init flag =
|
||||||
|
let
|
||||||
|
baseUrl =
|
||||||
|
case flag of
|
||||||
|
Err _ ->
|
||||||
|
"http://localhost:5000"
|
||||||
|
|
||||||
|
Ok v ->
|
||||||
|
v
|
||||||
|
|
||||||
|
model =
|
||||||
|
{ baseUrl = baseUrl
|
||||||
|
, leaderboard = Err 0
|
||||||
|
, matches = Err 0
|
||||||
|
}
|
||||||
|
in
|
||||||
|
( model
|
||||||
|
, Cmd.batch
|
||||||
|
[ getLeaderboard model
|
||||||
|
, getMatches model
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- UPDATE
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
OnLeaderboard (Err _) ->
|
||||||
|
case model.leaderboard of
|
||||||
|
Err n ->
|
||||||
|
( { model | leaderboard = Err (n + 1) }, Cmd.none )
|
||||||
|
|
||||||
|
Ok _ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
OnLeaderboard (Ok v) ->
|
||||||
|
( { model | leaderboard = Ok v }, Cmd.none )
|
||||||
|
|
||||||
|
OnMatches (Err _) ->
|
||||||
|
case model.matches of
|
||||||
|
Err n ->
|
||||||
|
( { model | matches = Err (n + 1) }, Cmd.none )
|
||||||
|
|
||||||
|
Ok _ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
OnMatches (Ok v) ->
|
||||||
|
( { model | matches = Ok v }, Cmd.none )
|
||||||
|
|
||||||
|
RefreshLeaderboard ->
|
||||||
|
( model, getLeaderboard model )
|
||||||
|
|
||||||
|
RefreshMatches ->
|
||||||
|
( model, getMatches model )
|
||||||
|
|
||||||
|
|
||||||
|
getLeaderboard : Model -> Cmd Msg
|
||||||
|
getLeaderboard model =
|
||||||
|
Elo.getLeaderboard { baseUrl = model.baseUrl, toMsg = OnLeaderboard }
|
||||||
|
|
||||||
|
|
||||||
|
getMatches : Model -> Cmd Msg
|
||||||
|
getMatches model =
|
||||||
|
Elo.getMatches { baseUrl = model.baseUrl, toMsg = OnMatches }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- SUBSCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
|
subscriptions : Model -> Sub Msg
|
||||||
|
subscriptions _ =
|
||||||
|
Sub.batch
|
||||||
|
[ Time.every (5 * 1000) (always RefreshLeaderboard)
|
||||||
|
, Time.every (1 * 60 * 1000) (always RefreshMatches)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
headers : Model -> List { icon : Widget.Icon.Icon msg, onPress : msg }
|
||||||
|
headers _ =
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
title : Model -> String
|
||||||
|
title model =
|
||||||
|
case model.leaderboard of
|
||||||
|
Err _ ->
|
||||||
|
"EloTracker | Loading..."
|
||||||
|
|
||||||
|
Ok _ ->
|
||||||
|
"EloTracker | Leaderboard"
|
||||||
|
|
||||||
|
|
||||||
|
view : Program.ViewBox Model -> Element Msg
|
||||||
|
view data =
|
||||||
|
case data.model.leaderboard of
|
||||||
|
Err 0 ->
|
||||||
|
Element.column
|
||||||
|
[ Element.width <| Element.px <| Pixels.inPixels data.size.width
|
||||||
|
, Element.height <| Element.px <| Pixels.inPixels data.size.height
|
||||||
|
]
|
||||||
|
[ Element.column
|
||||||
|
[ Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
, Element.Background.color (Theme.mantleUI data.flavor)
|
||||||
|
, Element.padding 15
|
||||||
|
, Element.spacing 10
|
||||||
|
]
|
||||||
|
[ Element.text "Loading leaderboard..."
|
||||||
|
, Layout.loadingIndicator { color = Theme.lavender data.flavor }
|
||||||
|
|> Element.el [ Element.centerX ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Err n ->
|
||||||
|
Element.column
|
||||||
|
[ Element.width <| Element.px <| Pixels.inPixels data.size.width
|
||||||
|
, Element.height <| Element.px <| Pixels.inPixels data.size.height
|
||||||
|
]
|
||||||
|
[ Element.column
|
||||||
|
[ Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
, Element.Background.color (Theme.mantleUI data.flavor)
|
||||||
|
, Element.padding 15
|
||||||
|
, Element.spacing 10
|
||||||
|
]
|
||||||
|
[ Element.text "Loading leaderboard..."
|
||||||
|
, Element.el [ Element.centerX, Element.Font.color (Theme.redUI data.flavor) ] (Element.text ("Failed " ++ (String.fromInt n) ++ " times"))
|
||||||
|
, Layout.loadingIndicator { color = Theme.lavender data.flavor }
|
||||||
|
|> Element.el [ Element.centerX ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Ok leaderboard ->
|
||||||
|
Screen.Leaderboard.view
|
||||||
|
{ flavor = data.flavor, model = leaderboard, size = data.size }
|
||||||
|
|
||||||
|
|
@ -156,32 +156,43 @@ view :
|
||||||
-> Html.Html (Msg msg)
|
-> Html.Html (Msg msg)
|
||||||
view data model =
|
view data model =
|
||||||
let
|
let
|
||||||
preferredNavBarHeight =
|
navBarIconHeight =
|
||||||
Pixels.pixels 40
|
Pixels.pixels 40
|
||||||
|
|
||||||
|
navBarHeight =
|
||||||
|
Quantity.twice navBarIconHeight
|
||||||
|
|
||||||
showNavBar =
|
showNavBar =
|
||||||
preferredNavBarHeight
|
navBarHeight
|
||||||
|> Quantity.multiplyBy 6
|
|> Quantity.multiplyBy 6
|
||||||
|> Quantity.lessThanOrEqualTo model.size.height
|
|> Quantity.lessThanOrEqualTo model.size.height
|
||||||
|
|
||||||
contentHeight =
|
contentHeight =
|
||||||
if showNavBar then
|
if showNavBar then
|
||||||
model.size.height |> Quantity.minus preferredNavBarHeight
|
model.size.height |> Quantity.minus navBarHeight
|
||||||
|
|
||||||
else
|
else
|
||||||
model.size.height
|
model.size.height
|
||||||
in
|
in
|
||||||
[ viewNavBar
|
[ if showNavBar then
|
||||||
{ headers = data.headers model.content
|
viewNavBar
|
||||||
, iconHeight = preferredNavBarHeight
|
{ headers = data.headers model.content
|
||||||
, model = model
|
, iconHeight = navBarIconHeight
|
||||||
}
|
, model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
Element.none
|
||||||
, data.body
|
, data.body
|
||||||
{ flavor = model.flavor
|
{ flavor = model.flavor
|
||||||
, model = model.content
|
, model = model.content
|
||||||
, size = { height = contentHeight, width = model.size.width }
|
, size =
|
||||||
|
{ height = contentHeight --|> Quantity.minus (Pixels.pixels 25)
|
||||||
|
, width = model.size.width --|> Quantity.minus (Pixels.pixels 25)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|> Element.map OnContent
|
|> Element.map OnContent
|
||||||
|
|> Element.el []
|
||||||
]
|
]
|
||||||
|> Element.column [ Element.width Element.fill ]
|
|> Element.column [ Element.width Element.fill ]
|
||||||
|> Element.layout
|
|> Element.layout
|
||||||
|
|
|
||||||
|
|
@ -2,52 +2,60 @@ module Screen.CreateGame exposing (..)
|
||||||
|
|
||||||
-- MODEL
|
-- MODEL
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ baseUrl : String
|
{ baseUrl : String
|
||||||
, players : List String
|
, players : List String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= OnBaseUrl String
|
= OnBaseUrl String
|
||||||
| OnPlayer Int String
|
| OnPlayer Int String
|
||||||
| RemovePlayer Int
|
| RemovePlayer Int
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- UPDATE
|
-- UPDATE
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
OnBaseUrl url ->
|
OnBaseUrl url ->
|
||||||
( { model | baseUrl = url }, Cmd.none )
|
( { model | baseUrl = url }, Cmd.none )
|
||||||
|
|
||||||
OnPlayer n p ->
|
OnPlayer n p ->
|
||||||
let
|
let
|
||||||
newIndex = List.length model.players == n
|
newIndex =
|
||||||
|
List.length model.players == n
|
||||||
|
|
||||||
newPlayers =
|
newPlayers =
|
||||||
if newIndex && mayCreateNewPlayer model.players then
|
if newIndex && mayCreateNewPlayer model.players then
|
||||||
List.append model.players [ p ]
|
List.append model.players [ p ]
|
||||||
|
|
||||||
else
|
else
|
||||||
List.indexedMap
|
List.indexedMap
|
||||||
(\i player ->
|
(\i player ->
|
||||||
if n == i then
|
if n == i then
|
||||||
p
|
p
|
||||||
|
|
||||||
else
|
else
|
||||||
player
|
player
|
||||||
)
|
)
|
||||||
model.players
|
model.players
|
||||||
|
|
||||||
in
|
in
|
||||||
( { model | players = newPlayers }, Cmd.none )
|
( { model | players = newPlayers }, Cmd.none )
|
||||||
|
|
||||||
RemovePlayer n ->
|
RemovePlayer n ->
|
||||||
( { model
|
( { model
|
||||||
| players =
|
| players =
|
||||||
model.players
|
model.players
|
||||||
|> List.indexedMap
|
|> List.indexedMap
|
||||||
(\i player ->
|
(\i player ->
|
||||||
if n == i then
|
if n == i then
|
||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
else
|
else
|
||||||
Just player
|
Just player
|
||||||
)
|
)
|
||||||
|
|
@ -56,10 +64,12 @@ update msg model =
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
-- SUBSCRIPTIONS
|
|
||||||
|
|
||||||
|
|
||||||
|
-- SUBSCRIPTIONS
|
||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
mayCreateNewPlayer : List String -> Bool
|
mayCreateNewPlayer : List String -> Bool
|
||||||
mayCreateNewPlayer =
|
mayCreateNewPlayer =
|
||||||
List.all (not << String.isEmpty)
|
List.all (not << String.isEmpty)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
module Screen.Leaderboard exposing (..)
|
||||||
|
|
||||||
|
{-| Widget module for a leaderboard page.
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Api.Elo exposing (EloStat, Leaderboard)
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Element.Font
|
||||||
|
import Layout
|
||||||
|
import Pixels exposing (Pixels)
|
||||||
|
import Program exposing (ViewBox)
|
||||||
|
import Quantity exposing (Quantity)
|
||||||
|
import Theme
|
||||||
|
import Element.Background
|
||||||
|
import Element.Border
|
||||||
|
import Color
|
||||||
|
|
||||||
|
|
||||||
|
view : ViewBox Leaderboard -> Element msg
|
||||||
|
view data =
|
||||||
|
let
|
||||||
|
boardWidth =
|
||||||
|
data.size.width
|
||||||
|
|> Quantity.toFloatQuantity
|
||||||
|
|> Quantity.multiplyBy (2/3)
|
||||||
|
|> Quantity.floor
|
||||||
|
|> Quantity.clamp (Pixels.pixels 600) (Pixels.pixels 1800)
|
||||||
|
|> Pixels.inPixels
|
||||||
|
|> Element.px
|
||||||
|
|> Element.width
|
||||||
|
in
|
||||||
|
data.model.players
|
||||||
|
|> List.map (viewEloStat { flavor = data.flavor })
|
||||||
|
|> Element.column [ Element.centerX, boardWidth ]
|
||||||
|
|> List.singleton
|
||||||
|
|> (::) (
|
||||||
|
Element.text data.model.name
|
||||||
|
|> Element.el
|
||||||
|
[ Element.centerX, Element.Font.size 50 ]
|
||||||
|
)
|
||||||
|
|> Element.column
|
||||||
|
[ Element.padding 15
|
||||||
|
, Element.spacing 30
|
||||||
|
, Element.width <| Element.px <| Pixels.inPixels data.size.width
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewEloStat : { flavor : Theme.Flavor } -> EloStat -> Element msg
|
||||||
|
viewEloStat data stat =
|
||||||
|
Element.row
|
||||||
|
[ Element.Background.color (Theme.mantleUI data.flavor)
|
||||||
|
, Element.Border.rounded 5
|
||||||
|
, Element.padding 10
|
||||||
|
, Element.spacing 5
|
||||||
|
, Element.width Element.fill
|
||||||
|
]
|
||||||
|
[ Element.column
|
||||||
|
[ Element.centerY, Element.spacing 5, Element.width Element.fill ]
|
||||||
|
[ Element.text stat.name
|
||||||
|
|> Element.el [ Element.Font.size 30 ]
|
||||||
|
, Element.el
|
||||||
|
[ Element.Font.color (Theme.subtext0UI data.flavor)
|
||||||
|
, Element.Font.size 15
|
||||||
|
]
|
||||||
|
(Element.text stat.url)
|
||||||
|
]
|
||||||
|
|
||||||
|
-- Bar to push the rest to the right
|
||||||
|
, Element.column [ Element.width Element.fill ] []
|
||||||
|
, field { text = "ELO", value = stat.elo, color = Theme.lavenderUI data.flavor }
|
||||||
|
|> Element.el [ Element.paddingEach { top = 0, bottom = 0, left = 0, right = 15 } ]
|
||||||
|
, field { text = "WINS", value = stat.wins, color = Theme.greenUI data.flavor }
|
||||||
|
, field { text = "DRAWS", value = stat.draws, color = Theme.subtext0UI data.flavor }
|
||||||
|
, field { text = "LOSSES", value = stat.losses, color = Theme.redUI data.flavor }
|
||||||
|
]
|
||||||
|
|
||||||
|
field : { text : String, value : Int, color : Element.Color } -> Element msg
|
||||||
|
field { text, value, color } =
|
||||||
|
Element.column
|
||||||
|
[ Element.Font.color color
|
||||||
|
, Element.spacing 3
|
||||||
|
, Element.width (Element.px 70)
|
||||||
|
]
|
||||||
|
[ String.fromInt value
|
||||||
|
|> Element.text
|
||||||
|
|> Element.el [ Element.centerX, Element.Font.size 25 ]
|
||||||
|
, text
|
||||||
|
|> Element.text
|
||||||
|
|> Element.el [ Element.centerX, Element.Font.size 15 ]
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
FROM alpine:3.10 AS elm-builder
|
||||||
|
|
||||||
|
ARG ELM_VERSION=0.19.1
|
||||||
|
ARG ELM_URL=https://github.com/elm/compiler/releases/download/${ELM_VERSION}/binary-for-linux-64-bit.gz
|
||||||
|
|
||||||
|
# Download internet packages + Node & npm
|
||||||
|
RUN apk add --no-cache ca-certificates curl gzip bash nodejs npm
|
||||||
|
|
||||||
|
# Download & install Elm
|
||||||
|
RUN curl -L ${ELM_URL} \
|
||||||
|
| gunzip > /usr/local/bin/elm \
|
||||||
|
&& chmod +x /usr/local/bin/elm
|
||||||
|
|
||||||
|
# Install uglify-js globally
|
||||||
|
RUN npm install -g uglify-js
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy Elm code
|
||||||
|
COPY elm.json .
|
||||||
|
COPY elm/ elm/
|
||||||
|
|
||||||
|
# Compile module
|
||||||
|
RUN elm make --output=/app/elm.js --optimize elm/EloTracker.elm
|
||||||
|
|
||||||
|
# Optimize & Minify compiled JS
|
||||||
|
RUN uglifyjs elm.js \
|
||||||
|
--compress "pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe" \
|
||||||
|
| uglifyjs --mangle --output elo_tracker.js
|
||||||
|
|
||||||
|
FROM python:3.10-alpine AS python-builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache gcc musl-dev python3-dev
|
||||||
|
COPY requirements-elo.txt .
|
||||||
|
|
||||||
|
# Create wheels for faster installation
|
||||||
|
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements-elo.txt
|
||||||
|
|
||||||
|
FROM python:3.10-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install from pre-built wheels
|
||||||
|
COPY --from=python-builder /wheels /wheels
|
||||||
|
COPY requirements-elo.txt .
|
||||||
|
RUN pip install --no-cache-dir --no-index --find-links=/wheels \
|
||||||
|
-r requirements-elo.txt && rm -rf /wheels
|
||||||
|
|
||||||
|
# Install Elm front-end JS
|
||||||
|
COPY --from=elm-builder /app/elo_tracker.js /app/elo_tracker/static/elo_tracker.js
|
||||||
|
|
||||||
|
# Install ELO tracker code
|
||||||
|
COPY agents/ agents/
|
||||||
|
COPY elo_tracker/ elo_tracker/
|
||||||
|
COPY pyclient/ pyclient/
|
||||||
|
COPY pyserver/ pyserver/
|
||||||
|
COPY elo.py .
|
||||||
|
|
||||||
|
CMD ["python", "elo.py"]
|
||||||
|
|
@ -491,13 +491,20 @@ class EloTracker:
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Bot-Man-Toe Elo Tracker</title>
|
<title>Bot-Man-Toe Elo Tracker</title>
|
||||||
|
<script src="/static/elo_tracker.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main id="main-block">
|
||||||
<h1>Bot-Man-Toe Elo Tracker</h1>
|
<h1>Bot-Man-Toe Elo Tracker</h1>
|
||||||
<p>The JSON API is available at /leaderboard, /matches, /players, and /health.</p>
|
<p>The JSON API is available at /leaderboard, /matches, /players, and /health.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
<script>
|
||||||
|
var app = Elm.EloTracker.init({
|
||||||
|
node: document.body,
|
||||||
|
flag: { baseUrl: "http://127.0.0.1:5000" }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
""".strip(),
|
""".strip(),
|
||||||
mimetype="text/html",
|
mimetype="text/html",
|
||||||
|
|
@ -713,7 +720,7 @@ class EloTracker:
|
||||||
|
|
||||||
def start_server(
|
def start_server(
|
||||||
self,
|
self,
|
||||||
host: str = "127.0.0.1",
|
host: str = "0.0.0.0",
|
||||||
import_name : str = __name__,
|
import_name : str = __name__,
|
||||||
port: int = 5000,
|
port: int = 5000,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
blinker==1.9.0
|
||||||
|
certifi==2026.6.17
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
click==8.4.1
|
||||||
|
colorama==0.4.6
|
||||||
|
Flask==3.1.3
|
||||||
|
idna==3.18
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.3
|
||||||
|
requests==2.34.2
|
||||||
|
urllib3==2.7.0
|
||||||
|
Werkzeug==3.1.8
|
||||||
Loading…
Reference in New Issue