6 Commits

Author SHA1 Message Date
Bel LaPointe
60adca804b draft expressing games via config 2021-03-13 23:46:48 -06:00
Bel LaPointe
b72c48f705 Remove participating, add pot to Player 2021-03-13 23:44:35 -06:00
Bel LaPointe
bce32ed6a3 split game.master 2021-03-13 16:02:30 -06:00
Bel LaPointe
4bf83b3e40 reorg 2021-03-13 15:59:07 -06:00
Bel LaPointe
cb42bdc8d0 to packages 2021-03-13 15:19:55 -06:00
Bel LaPointe
ebbf21d23d moving game logic server side 2021-03-13 14:52:23 -06:00
29 changed files with 306 additions and 210 deletions

33
games/poker.yaml Normal file
View File

@@ -0,0 +1,33 @@
# after each phase, given game, evaluate if end
end:
- op: playerCount
operand: 1
phases:
- op: charge
operand: 5
- op: deal
operand: 5
- op: bet
- op: trade
operand: 5
- op: bet
- op: end
# always return an number type (or zero if not qualified) w/ high card
# ie
# two pair and a card
# king, king, 4, 4, 3 => 1000 * (13) + 10 * (4) + 3 = 1343
# TODO: inverted ranking, I think negative is enough
hands:
- royalFlush
- straightFlush
- fourOfAKind
- fullHouse
- flush
- straight
- threeOfAKind
- twoPair
- pair
- highCard
deck:
exclude:
- joker

25
main.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/game"
"local/sandbox/cards/src/server"
"local/sandbox/cards/src/storage"
"log"
"net/http"
)
func main() {
config := config.NewConfig()
storage := storage.NewStorage(config)
gm := game.NewMaster(config, storage)
server := server.NewServer(config, gm)
if err := server.Routes(); err != nil {
panic(err)
}
log.Println(config)
if err := http.ListenAndServe(fmt.Sprintf(":%d", config.Server.Port), server); err != nil {
panic(err)
}
}

View File

@@ -1,14 +1,18 @@
package main package config
import ( import (
"io/ioutil"
"local/args" "local/args"
"local/storage" "local/storage"
"os"
"path"
"strings" "strings"
"testing"
) )
type Config struct { type Config struct {
Game struct { Game struct {
db storage.DB DB storage.DB
} }
Server struct { Server struct {
Port int Port int
@@ -42,7 +46,7 @@ func NewConfig() Config {
} }
config := Config{} config := Config{}
config.Game.db = db config.Game.DB = db
config.Server.Port = as.GetInt("p") config.Server.Port = as.GetInt("p")
config.Server.File.Root = as.GetString("root") config.Server.File.Root = as.GetString("root")
config.Server.File.Prefix = "/" + strings.TrimPrefix(as.GetString("root-http-prefix"), "/") config.Server.File.Prefix = "/" + strings.TrimPrefix(as.GetString("root-http-prefix"), "/")
@@ -50,3 +54,21 @@ func NewConfig() Config {
return config return config
} }
func NewTestConfig(t *testing.T) Config {
d := t.TempDir()
config := Config{}
config.Game.DB = storage.NewMap()
config.Server.Port = 9999
config.Server.File.Root = d
config.Server.File.Prefix = "/file"
config.Server.API.Prefix = "/api"
err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm)
if err != nil {
t.Fatal(err)
}
return config
}

35
src/consts/errors.go Normal file
View File

@@ -0,0 +1,35 @@
package consts
import (
"net/http"
)
var (
ErrGameExists = NewCodeError("game exists", http.StatusConflict)
)
type CodeError struct {
Err string
Status int
}
func NewCodeError(err string, status int) CodeError {
return CodeError{
Err: err,
Status: status,
}
}
func (ce CodeError) Error() string {
if ce.Err == "" {
return "unspecified error occurred"
}
return ce.Err
}
func (ce CodeError) Code() int {
if ce.Status == 0 {
return http.StatusInternalServerError
}
return ce.Status
}

6
src/entity/card.go Normal file
View File

@@ -0,0 +1,6 @@
package entity
type Card struct {
Suit int
Value int
}

3
src/entity/currency.go Normal file
View File

@@ -0,0 +1,3 @@
package entity
type Currency int

14
src/entity/game.go Normal file
View File

@@ -0,0 +1,14 @@
package entity
import "fmt"
type Game struct {
Pot Currency
Turn int
Players Players
Log string
}
func (game Game) Equals(gameB Game) bool {
return fmt.Sprint(game) != fmt.Sprint(gameB)
}

15
src/entity/player.go Normal file
View File

@@ -0,0 +1,15 @@
package entity
type Player struct {
ID string
Name string
Card Card
Balance Currency
Pot Currency
Active bool
Checked bool
}
func (p Player) Empty() bool {
return p == (Player{})
}

16
src/entity/player_test.go Normal file
View File

@@ -0,0 +1,16 @@
package entity
import (
"testing"
)
func TestPlayerEmpty(t *testing.T) {
var p Player
if !p.Empty() {
t.Fatal(p)
}
p.ID = "id"
if p.Empty() {
t.Fatal(p)
}
}

7
src/entity/players.go Normal file
View File

@@ -0,0 +1,7 @@
package entity
type Players []Player
func (players *Players) Add(player Player) {
*players = append(*players, player)
}

43
src/game/game.go Normal file
View File

@@ -0,0 +1,43 @@
package game
import (
"local/sandbox/cards/src/consts"
"local/sandbox/cards/src/entity"
)
func (gm *Master) ListGames() ([]string, error) {
return gm.storage.ListGames()
}
func (gm *Master) GetGame(id string) (entity.Game, error) {
gm.locks.RLock(id)
defer gm.locks.RUnlock(id)
game, err := gm.storage.GetGame(id)
if game.Players == nil {
game.Players = make(entity.Players, 0)
}
return game, err
}
func (gm *Master) CreateGame(id string) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err == nil {
return consts.ErrGameExists
}
return gm.storage.CreateGame(id)
}
func (gm *Master) ReplaceGame(id string, game entity.Game) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err != nil {
return err
}
return gm.storage.ReplaceGame(id, game)
}

27
src/game/master.go Normal file
View File

@@ -0,0 +1,27 @@
package game
import (
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/storage"
"testing"
)
type Master struct {
config config.Config
storage Storage
locks *storage.RWLockMap
}
func NewMaster(config config.Config, s Storage) *Master {
return &Master{
config: config,
storage: s,
locks: storage.NewRWLockMap(),
}
}
func NewTestMaster(t *testing.T) *Master {
config := config.NewTestConfig(t)
storage := storage.NewStorage(config)
return NewMaster(config, storage)
}

4
src/game/rule.go Normal file
View File

@@ -0,0 +1,4 @@
package game
type Rule interface {
}

12
src/game/storage.go Normal file
View File

@@ -0,0 +1,12 @@
package game
import (
"local/sandbox/cards/src/entity"
)
type Storage interface {
CreateGame(string) error
GetGame(string) (entity.Game, error)
ListGames() ([]string, error)
ReplaceGame(string, entity.Game) error
}

View File

@@ -1,15 +1,12 @@
package main package game
import "testing" import (
"local/sandbox/cards/src/entity"
"testing"
)
func mockGameMaster(t *testing.T) *GameMaster { func TestMasterGetCreateGetList(t *testing.T) {
config := mockConfig(t) gm := NewTestMaster(t)
storage := NewStorage(config)
return NewGameMaster(config, storage)
}
func TestGameMasterGetCreateGetList(t *testing.T) {
gm := mockGameMaster(t)
id := "game" id := "game"
if games, err := gm.ListGames(); err != nil { if games, err := gm.ListGames(); err != nil {
@@ -33,8 +30,8 @@ func TestGameMasterGetCreateGetList(t *testing.T) {
} }
} }
func TestGameMasterUpdate(t *testing.T) { func TestMasterUpdate(t *testing.T) {
gm := mockGameMaster(t) gm := NewTestMaster(t)
id := "game" id := "game"
err := gm.CreateGame(id) err := gm.CreateGame(id)
@@ -47,7 +44,9 @@ func TestGameMasterUpdate(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
game.Players[2].ID = "hi" game.Players.Add(entity.Player{
ID: "hi",
})
game.Pot = 123 game.Pot = 123
err = gm.ReplaceGame(id, game) err = gm.ReplaceGame(id, game)
if err != nil { if err != nil {
@@ -58,7 +57,7 @@ func TestGameMasterUpdate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if game2 != game { if game2.Equals(game) {
t.Fatalf("replace+get don't match:\nwant\t%+v\ngot\t%+v", game, game2) t.Fatalf("replace+get don't match:\nwant\t%+v\ngot\t%+v", game, game2)
} }
} }

View File

@@ -1,27 +0,0 @@
package main
import (
"io/ioutil"
"local/storage"
"os"
"path"
"testing"
)
func mockConfig(t *testing.T) Config {
d := t.TempDir()
config := Config{}
config.Game.db = storage.NewMap()
config.Server.Port = 9999
config.Server.File.Root = d
config.Server.File.Prefix = "/file"
config.Server.API.Prefix = "/api"
err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm)
if err != nil {
t.Fatal(err)
}
return config
}

View File

@@ -1,7 +0,0 @@
package main
import "errors"
var (
errGameExists = errors.New("cannot create game: already exists")
)

View File

@@ -1,41 +0,0 @@
package main
type Players [16]Player
type Game struct {
Pot Currency
Turn int
Players Players
Log string
}
type Player struct {
ID string
Name string
Card Card
Balance Currency
Active bool
Participating bool
Checked bool
}
type Currency int
type Card struct {
Suit int
Value int
}
func (game Game) GetPlayers() []Player {
players := []Player{}
for _, player := range game.Players {
if !player.Empty() {
players = append(players, player)
}
}
return players
}
func (p Player) Empty() bool {
return p == (Player{})
}

View File

@@ -1,29 +0,0 @@
package main
import (
"testing"
)
func TestPlayerEmpty(t *testing.T) {
var p Player
if !p.Empty() {
t.Fatal(p)
}
p.ID = "id"
if p.Empty() {
t.Fatal(p)
}
}
func TestGameGetPlayers(t *testing.T) {
var g Game
if players := g.GetPlayers(); len(players) != 0 {
t.Fatal(players)
}
g.Players[5].ID = "id"
if players := g.GetPlayers(); len(players) != 1 {
t.Fatal(players)
} else if players[0].ID != "id" {
t.Fatal(players[0])
}
}

View File

@@ -1,48 +0,0 @@
package main
type GameMaster struct {
config Config
storage *Storage
locks *RWLockMap
}
func NewGameMaster(config Config, storage *Storage) *GameMaster {
return &GameMaster{
config: config,
storage: storage,
locks: NewRWLockMap(),
}
}
func (gm *GameMaster) ListGames() ([]string, error) {
return gm.storage.ListGames()
}
func (gm *GameMaster) GetGame(id string) (Game, error) {
gm.locks.RLock(id)
defer gm.locks.RUnlock(id)
return gm.storage.GetGame(id)
}
func (gm *GameMaster) CreateGame(id string) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err == nil {
return errGameExists
}
return gm.storage.CreateGame(id)
}
func (gm *GameMaster) ReplaceGame(id string, game Game) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err != nil {
return err
}
return gm.storage.ReplaceGame(id, game)
}

View File

@@ -1,21 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
config := NewConfig()
storage := NewStorage(config)
gm := NewGameMaster(config, storage)
server := NewServer(config, gm)
if err := server.Routes(); err != nil {
panic(err)
}
log.Println(config)
if err := http.ListenAndServe(fmt.Sprintf(":%d", config.Server.Port), server); err != nil {
panic(err)
}
}

View File

@@ -1,9 +1,12 @@
package main package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"local/router" "local/router"
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/entity"
"local/sandbox/cards/src/game"
"local/storage" "local/storage"
"log" "log"
"net/http" "net/http"
@@ -11,12 +14,12 @@ import (
) )
type Server struct { type Server struct {
config Config config config.Config
gm *GameMaster gm *game.Master
*router.Router *router.Router
} }
func NewServer(config Config, gm *GameMaster) *Server { func NewServer(config config.Config, gm *game.Master) *Server {
return &Server{ return &Server{
config: config, config: config,
gm: gm, gm: gm,
@@ -102,7 +105,7 @@ func (server *Server) GameReplace(w http.ResponseWriter, r *http.Request) {
badRequest(w, err.Error()) badRequest(w, err.Error())
return return
} }
var game Game var game entity.Game
err = json.NewDecoder(r.Body).Decode(&game) err = json.NewDecoder(r.Body).Decode(&game)
if err != nil { if err != nil {
badRequest(w, err.Error()) badRequest(w, err.Error())
@@ -128,7 +131,7 @@ func (server *Server) GameInsert(w http.ResponseWriter, r *http.Request) {
internalError(w, err.Error()) internalError(w, err.Error())
return return
} }
json.NewEncoder(w).Encode(Game{}) json.NewEncoder(w).Encode(entity.Game{})
} }
func notImpl(w http.ResponseWriter) { func notImpl(w http.ResponseWriter) {

View File

@@ -1,6 +1,8 @@
package main package server
import ( import (
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/game"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"path" "path"
@@ -9,8 +11,9 @@ import (
) )
func TestServerRouter(t *testing.T) { func TestServerRouter(t *testing.T) {
gm := mockGameMaster(t) config := config.NewTestConfig(t)
server := NewServer(gm.config, gm) gm := game.NewTestMaster(t)
server := NewServer(config, gm)
if err := server.Routes(); err != nil { if err := server.Routes(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -55,7 +58,7 @@ func TestServerRouter(t *testing.T) {
} }
func TestServerByMethod(t *testing.T) { func TestServerByMethod(t *testing.T) {
server := NewServer(Config{}, nil) server := NewServer(config.Config{}, nil)
gotGet := false gotGet := false
get := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotGet = true }) get := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotGet = true })

View File

@@ -1,4 +1,4 @@
package main package storage
import "sync" import "sync"

View File

@@ -1,4 +1,4 @@
package main package storage
import "testing" import "testing"

View File

@@ -1,7 +1,9 @@
package main package storage
import ( import (
"encoding/json" "encoding/json"
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/entity"
"local/storage" "local/storage"
) )
@@ -10,18 +12,18 @@ const (
) )
type Storage struct { type Storage struct {
config Config config config.Config
db storage.DB db storage.DB
} }
func NewStorage(config Config) *Storage { func NewStorage(config config.Config) *Storage {
return &Storage{ return &Storage{
config: config, config: config,
db: config.Game.db, db: config.Game.DB,
} }
} }
func (storage Storage) ReplaceGame(id string, game Game) error { func (storage Storage) ReplaceGame(id string, game entity.Game) error {
b, err := json.Marshal(game) b, err := json.Marshal(game)
if err != nil { if err != nil {
return err return err
@@ -29,8 +31,8 @@ func (storage Storage) ReplaceGame(id string, game Game) error {
return storage.db.Set(id, b, nsGames) return storage.db.Set(id, b, nsGames)
} }
func (storage Storage) GetGame(id string) (Game, error) { func (storage Storage) GetGame(id string) (entity.Game, error) {
var game Game var game entity.Game
b, err := storage.db.Get(id, nsGames) b, err := storage.db.Get(id, nsGames)
if err != nil { if err != nil {
@@ -42,7 +44,7 @@ func (storage Storage) GetGame(id string) (Game, error) {
} }
func (storage Storage) CreateGame(id string) error { func (storage Storage) CreateGame(id string) error {
b, err := json.Marshal(Game{}) b, err := json.Marshal(entity.Game{})
if err != nil { if err != nil {
return err return err
} }