Compare commits
16 Commits
clientSide
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c2510d837 | ||
|
|
a26c1a0043 | ||
|
|
9efb30ae3e | ||
|
|
117250f5c1 | ||
|
|
cc4a19a47c | ||
|
|
0c406e3163 | ||
|
|
ec81bb24ad | ||
|
|
3e71ab6217 | ||
|
|
3a8985486a | ||
|
|
d79721b760 | ||
|
|
60adca804b | ||
|
|
b72c48f705 | ||
|
|
bce32ed6a3 | ||
|
|
4bf83b3e40 | ||
|
|
cb42bdc8d0 | ||
|
|
ebbf21d23d |
32
games/poker.yaml
Normal file
32
games/poker.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# after each player op, 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
|
||||||
|
# TODO: inverted ranking, I think negative is enough
|
||||||
|
hands:
|
||||||
|
- royalFlush
|
||||||
|
- straightFlush
|
||||||
|
- fourOfAKind
|
||||||
|
- fullHouse
|
||||||
|
- flush
|
||||||
|
- straight
|
||||||
|
- threeOfAKind
|
||||||
|
- twoPair
|
||||||
|
- pair
|
||||||
|
- highCard
|
||||||
|
deck:
|
||||||
|
aceLow: false
|
||||||
|
withoutValue:
|
||||||
|
- joker
|
||||||
|
withoutSuit:
|
||||||
|
- heart
|
||||||
25
main.go
Normal file
25
main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
35
src/consts/errors.go
Normal 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
6
src/entity/card.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type Card struct {
|
||||||
|
Suit int
|
||||||
|
Value int
|
||||||
|
}
|
||||||
7
src/entity/curernt.go
Normal file
7
src/entity/curernt.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type Current struct {
|
||||||
|
Bet Currency
|
||||||
|
Turn int
|
||||||
|
Phase int
|
||||||
|
}
|
||||||
3
src/entity/currency.go
Normal file
3
src/entity/currency.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type Currency int
|
||||||
34
src/entity/deck.go
Normal file
34
src/entity/deck.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
type Deck struct {
|
||||||
|
Deck []Card
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deck) Shuffle() {
|
||||||
|
shuffle(d.Deck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deck) Draw() Card {
|
||||||
|
if len(d.Deck) == 0 {
|
||||||
|
panic("cannot draw from an empty deck")
|
||||||
|
}
|
||||||
|
card := d.Deck[len(d.Deck)-1]
|
||||||
|
d.Deck = d.Deck[:len(d.Deck)-1]
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
func shuffle(cards []Card) {
|
||||||
|
if len(cards) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switches := rand.Intn(100) + 100
|
||||||
|
for i := 0; i < switches; i++ {
|
||||||
|
a := rand.Intn(len(cards))
|
||||||
|
b := rand.Intn(len(cards))
|
||||||
|
j := cards[a]
|
||||||
|
cards[a] = cards[b]
|
||||||
|
cards[b] = j
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/entity/game.go
Normal file
93
src/entity/game.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
Players Players
|
||||||
|
Log string
|
||||||
|
Deck Deck
|
||||||
|
Current Current
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game Game) Equals(gameB Game) bool {
|
||||||
|
return fmt.Sprint(game) != fmt.Sprint(gameB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game Game) Pot() Currency {
|
||||||
|
var pot Currency
|
||||||
|
for _, player := range game.Players {
|
||||||
|
pot += player.Pot
|
||||||
|
pot += player.Bet
|
||||||
|
}
|
||||||
|
return pot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game Game) ActivePlayers() []*Player {
|
||||||
|
players := make([]*Player, 0)
|
||||||
|
for i := range game.Players {
|
||||||
|
if game.Players[i].Active {
|
||||||
|
players = append(players, &game.Players[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return players
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) NextTurn() {
|
||||||
|
if len(game.ActivePlayers()) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foo := func() {
|
||||||
|
game.Current.Turn = (game.Current.Turn + 1) % len(game.Players)
|
||||||
|
}
|
||||||
|
foo()
|
||||||
|
for !game.Players[game.Current.Turn].Active {
|
||||||
|
foo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) NextPhase() {
|
||||||
|
for i := range game.Players {
|
||||||
|
game.Players[i].Checked = false
|
||||||
|
game.Players[i].Pot += game.Players[i].Bet
|
||||||
|
game.Players[i].Bet = 0
|
||||||
|
}
|
||||||
|
game.Current.Bet = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) ChargeActivePlayers(v Currency) {
|
||||||
|
for _, player := range game.ActivePlayers() {
|
||||||
|
if player.Balance < v {
|
||||||
|
player.Active = false
|
||||||
|
} else {
|
||||||
|
player.Balance -= v
|
||||||
|
player.Pot += v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) Deal(n int) {
|
||||||
|
for i := range game.Players {
|
||||||
|
if game.Players[i].Active {
|
||||||
|
for j := 0; j < n; j++ {
|
||||||
|
card := game.Deck.Draw()
|
||||||
|
game.Players[i].Hand.Push(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game Game) IsPotRight() bool {
|
||||||
|
potsRight := true
|
||||||
|
for _, player := range game.ActivePlayers() {
|
||||||
|
potsRight = potsRight && (player.Balance == 0 || player.Bet == game.Current.Bet)
|
||||||
|
}
|
||||||
|
return potsRight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game Game) IsAllActivePlayersChecked() bool {
|
||||||
|
allChecks := true
|
||||||
|
for _, player := range game.ActivePlayers() {
|
||||||
|
allChecks = allChecks && player.Checked
|
||||||
|
}
|
||||||
|
return allChecks
|
||||||
|
}
|
||||||
58
src/entity/hand.go
Normal file
58
src/entity/hand.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hand struct {
|
||||||
|
Public []Card
|
||||||
|
Private []Card
|
||||||
|
ReversePublic []Card
|
||||||
|
ReversePrivate []Card
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hand *Hand) Push(card Card) {
|
||||||
|
if hand.Public == nil {
|
||||||
|
hand.Public = make([]Card, 0, 1)
|
||||||
|
}
|
||||||
|
hand.Public = append(hand.Public, card)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hand Hand) Len() int {
|
||||||
|
return len(hand.Public) + len(hand.Private) + len(hand.ReversePublic) + len(hand.ReversePrivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hand Hand) AllCards() []Card {
|
||||||
|
allcards := make([]Card, 0, hand.Len())
|
||||||
|
for _, cards := range [][]Card{hand.Public, hand.Private, hand.ReversePublic, hand.ReversePrivate} {
|
||||||
|
allcards = append(allcards, cards...)
|
||||||
|
}
|
||||||
|
return allcards
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hand Hand) Flush() bool {
|
||||||
|
suit := -1
|
||||||
|
for _, card := range hand.AllCards() {
|
||||||
|
if suit == -1 {
|
||||||
|
suit = card.Suit
|
||||||
|
} else if suit != card.Suit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return suit != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hand Hand) Straight() bool {
|
||||||
|
cards := hand.AllCards()
|
||||||
|
values := make([]int, 0, len(cards))
|
||||||
|
for _, card := range cards {
|
||||||
|
values = append(values, card.Value)
|
||||||
|
}
|
||||||
|
sort.Ints(values)
|
||||||
|
for i := 1; i < len(values); i++ {
|
||||||
|
if values[i-1]+1 != values[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(values) > 1
|
||||||
|
}
|
||||||
107
src/entity/hand_test.go
Normal file
107
src/entity/hand_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandAllCards(t *testing.T) {
|
||||||
|
t.Run("all pub", func(t *testing.T) {
|
||||||
|
hand := Hand{
|
||||||
|
Public: []Card{
|
||||||
|
Card{Value: 1},
|
||||||
|
Card{Value: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
all := hand.AllCards()
|
||||||
|
if len(all) != 2 {
|
||||||
|
t.Fatal(all)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all pri", func(t *testing.T) {
|
||||||
|
hand := Hand{
|
||||||
|
Private: []Card{
|
||||||
|
Card{Value: 1},
|
||||||
|
Card{Value: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
all := hand.AllCards()
|
||||||
|
if len(all) != 2 {
|
||||||
|
t.Fatal(all)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("both", func(t *testing.T) {
|
||||||
|
hand := Hand{
|
||||||
|
Public: []Card{
|
||||||
|
Card{Value: 3},
|
||||||
|
Card{Value: 4},
|
||||||
|
},
|
||||||
|
Private: []Card{
|
||||||
|
Card{Value: 1},
|
||||||
|
Card{Value: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
all := hand.AllCards()
|
||||||
|
if len(all) != 4 {
|
||||||
|
t.Fatal(all)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandStraight(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
public []Card
|
||||||
|
private []Card
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"one public card": {
|
||||||
|
public: []Card{Card{Value: 3}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"one private card": {
|
||||||
|
private: []Card{Card{Value: 3}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"two public card nonseq": {
|
||||||
|
public: []Card{Card{Value: 3}, Card{Value: 1}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"two private card nonseq": {
|
||||||
|
private: []Card{Card{Value: 3}, Card{Value: 1}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"one public card one private card nonseq": {
|
||||||
|
public: []Card{Card{Value: 3}},
|
||||||
|
private: []Card{Card{Value: 1}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"two public card seq": {
|
||||||
|
public: []Card{Card{Value: 3}, Card{Value: 2}},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
"two private card seq": {
|
||||||
|
private: []Card{Card{Value: 3}, Card{Value: 2}},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
"one public card one private card seq": {
|
||||||
|
public: []Card{Card{Value: 2}},
|
||||||
|
private: []Card{Card{Value: 1}},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
hand := Hand{Public: c.public, Private: c.private}
|
||||||
|
got := hand.Straight()
|
||||||
|
if got != c.want {
|
||||||
|
t.Fatal(c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/entity/player.go
Normal file
12
src/entity/player.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Hand Hand
|
||||||
|
Balance Currency
|
||||||
|
Pot Currency
|
||||||
|
Bet Currency
|
||||||
|
Active bool
|
||||||
|
Checked bool
|
||||||
|
}
|
||||||
7
src/entity/players.go
Normal file
7
src/entity/players.go
Normal 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
43
src/game/game.go
Normal 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
27
src/game/master.go
Normal 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
4
src/game/rule.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type Rule interface {
|
||||||
|
}
|
||||||
5
src/game/rule/deck.go
Normal file
5
src/game/rule/deck.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "local/sandbox/cards/src/game/rule/operation"
|
||||||
|
|
||||||
|
type Deck operation.Cards
|
||||||
5
src/game/rule/end.go
Normal file
5
src/game/rule/end.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "local/sandbox/cards/src/game/rule/operation"
|
||||||
|
|
||||||
|
type End operation.Bool
|
||||||
5
src/game/rule/hand.go
Normal file
5
src/game/rule/hand.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "local/sandbox/cards/src/game/rule/operation"
|
||||||
|
|
||||||
|
type Hand operation.Int
|
||||||
36
src/game/rule/operation/bool.go
Normal file
36
src/game/rule/operation/bool.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bool func(*entity.Game, interface{}) bool
|
||||||
|
|
||||||
|
var boolStringified = map[string]Bool{
|
||||||
|
"charge": charge,
|
||||||
|
"deal": deal,
|
||||||
|
"bet": bet,
|
||||||
|
"trade": trade,
|
||||||
|
"end": end,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foo *Bool) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return foo.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foo *Bool) FromString(s string) error {
|
||||||
|
for k, v := range boolStringified {
|
||||||
|
if k == s {
|
||||||
|
*foo = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("unknown bool method " + s)
|
||||||
|
}
|
||||||
61
src/game/rule/operation/cards.go
Normal file
61
src/game/rule/operation/cards.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cards struct {
|
||||||
|
AceLow bool
|
||||||
|
Jokers int
|
||||||
|
WithoutValue []Value
|
||||||
|
WithoutSuit []Suit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cards Cards) NewDeck() entity.Deck {
|
||||||
|
deck := new52DeckAceHigh()
|
||||||
|
if cards.AceLow {
|
||||||
|
deck = new52DeckAceLow()
|
||||||
|
}
|
||||||
|
for i := 0; i < cards.Jokers; i++ {
|
||||||
|
deck.Deck = append(deck.Deck, entity.Card{Value: int(Joker)})
|
||||||
|
}
|
||||||
|
for i := len(deck.Deck) - 1; i >= 0; i-- {
|
||||||
|
card := deck.Deck[i]
|
||||||
|
drop := false
|
||||||
|
for _, value := range cards.WithoutValue {
|
||||||
|
drop = drop || card.Value == int(value)
|
||||||
|
}
|
||||||
|
for _, suit := range cards.WithoutSuit {
|
||||||
|
drop = drop || card.Suit == int(suit)
|
||||||
|
}
|
||||||
|
if drop {
|
||||||
|
deck.Deck = append(deck.Deck[:i], deck.Deck[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deck.Shuffle()
|
||||||
|
return deck
|
||||||
|
}
|
||||||
|
|
||||||
|
func new52DeckAceHigh() entity.Deck {
|
||||||
|
cards := make([]entity.Card, 0, 52)
|
||||||
|
for _, suit := range suitStrings {
|
||||||
|
for _, value := range valueStrings {
|
||||||
|
if value != AceLow && value != Joker {
|
||||||
|
cards = append(cards, entity.Card{Suit: int(suit), Value: int(value)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deck := entity.Deck{Deck: cards}
|
||||||
|
deck.Shuffle()
|
||||||
|
return deck
|
||||||
|
}
|
||||||
|
|
||||||
|
func new52DeckAceLow() entity.Deck {
|
||||||
|
deck := new52DeckAceHigh()
|
||||||
|
for i, card := range deck.Deck {
|
||||||
|
if card.Value == int(AceHigh) {
|
||||||
|
deck.Deck[i].Value = int(AceLow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deck
|
||||||
|
}
|
||||||
67
src/game/rule/operation/cards_test.go
Normal file
67
src/game/rule/operation/cards_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDeck52(t *testing.T) {
|
||||||
|
deck := new52DeckAceLow()
|
||||||
|
if len(deck.Deck) != 52 {
|
||||||
|
t.Fatal(len(deck.Deck))
|
||||||
|
}
|
||||||
|
for _, card := range deck.Deck {
|
||||||
|
if card.Value == int(AceHigh) {
|
||||||
|
t.Fatal(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCardsNewDeck(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
cards Cards
|
||||||
|
check func(entity.Deck) bool
|
||||||
|
}{
|
||||||
|
"default": {
|
||||||
|
cards: Cards{},
|
||||||
|
check: func(deck entity.Deck) bool {
|
||||||
|
return len(deck.Deck) == 52
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"two jokers": {
|
||||||
|
cards: Cards{
|
||||||
|
Jokers: 2,
|
||||||
|
},
|
||||||
|
check: func(deck entity.Deck) bool {
|
||||||
|
return len(deck.Deck) == 54
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no fours": {
|
||||||
|
cards: Cards{
|
||||||
|
WithoutValue: []Value{Four},
|
||||||
|
},
|
||||||
|
check: func(deck entity.Deck) bool {
|
||||||
|
return len(deck.Deck) == 48
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no hearts, no clubs, one joker": {
|
||||||
|
cards: Cards{
|
||||||
|
Jokers: 1,
|
||||||
|
WithoutSuit: []Suit{Heart, Club},
|
||||||
|
},
|
||||||
|
check: func(deck entity.Deck) bool {
|
||||||
|
return len(deck.Deck) == 27
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
deck := c.cards.NewDeck()
|
||||||
|
if !c.check(deck) {
|
||||||
|
t.Fatal(deck)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/game/rule/operation/compare.go
Normal file
20
src/game/rule/operation/compare.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compareNumber(a int, b interface{}) int {
|
||||||
|
v := convertNumber(b)
|
||||||
|
if a == v {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if a > v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareString(a string, b interface{}) int {
|
||||||
|
return strings.Compare(a, convertString(b))
|
||||||
|
}
|
||||||
37
src/game/rule/operation/compare_test.go
Normal file
37
src/game/rule/operation/compare_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCompareNumber(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
a int
|
||||||
|
b interface{}
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
"int int equal": {
|
||||||
|
a: 1,
|
||||||
|
b: 1,
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"int int gt": {
|
||||||
|
a: 2,
|
||||||
|
b: 1,
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
"int int lt": {
|
||||||
|
a: 0,
|
||||||
|
b: 1,
|
||||||
|
want: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := compareNumber(c.a, c.b)
|
||||||
|
if got != c.want {
|
||||||
|
t.Fatal(got, c.a, c.b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/game/rule/operation/convert.go
Normal file
22
src/game/rule/operation/convert.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertNumber(v interface{}) int {
|
||||||
|
s := fmt.Sprint(v)
|
||||||
|
v2, _ := strconv.ParseFloat(s, 64)
|
||||||
|
return int(v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertString(v interface{}) string {
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
return v.(string)
|
||||||
|
case []byte:
|
||||||
|
return string(v.([]byte))
|
||||||
|
}
|
||||||
|
return fmt.Sprint(v)
|
||||||
|
}
|
||||||
164
src/game/rule/operation/hand.go
Normal file
164
src/game/rule/operation/hand.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func royalFlush(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
if !hand.Flush() || !hand.Straight() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, card := range hand.AllCards() {
|
||||||
|
if card.Value < 11 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func straightFlush(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
if !hand.Flush() || !hand.Straight() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
biggest := 0
|
||||||
|
for _, card := range hand.AllCards() {
|
||||||
|
if card.Value > biggest {
|
||||||
|
biggest = card.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return int64(biggest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fourOfAKind(game *entity.Game, _ interface{}) int64 {
|
||||||
|
return nOfAKind(game, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fullHouse(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
counts := counts(hand)
|
||||||
|
trio := entity.Card{}
|
||||||
|
duo := entity.Card{}
|
||||||
|
for k, v := range counts {
|
||||||
|
if v == 3 && k.Value > trio.Value {
|
||||||
|
trio = k
|
||||||
|
}
|
||||||
|
if v == 2 && k.Value > duo.Value {
|
||||||
|
duo = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if trio.Value == 0 || duo.Value == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int64(trio.Value*100 + duo.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func flush(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
if !hand.Flush() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return highCard(game, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func straight(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
if !hand.Straight() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
big := 0
|
||||||
|
for _, card := range hand.AllCards() {
|
||||||
|
if card.Value > big {
|
||||||
|
big = card.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return int64(big)
|
||||||
|
}
|
||||||
|
|
||||||
|
func threeOfAKind(game *entity.Game, _ interface{}) int64 {
|
||||||
|
return nOfAKind(game, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func twoPair(game *entity.Game, _ interface{}) int64 {
|
||||||
|
high := nOfAKind(game, 2)
|
||||||
|
if high == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
high = high / 100
|
||||||
|
hand := getHand(game)
|
||||||
|
counts := counts(hand)
|
||||||
|
low := 0
|
||||||
|
highCard := 0
|
||||||
|
for k, v := range counts {
|
||||||
|
if ((int64(k.Value) != high && v >= 2) || v >= 4) && k.Value > low {
|
||||||
|
low = k.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if low == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for k, v := range counts {
|
||||||
|
if ((int64(k.Value) != high && k.Value != low) || v > 4 || v == 3) && k.Value > highCard {
|
||||||
|
highCard = k.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return high*100*100 + int64(low*100+highCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pair(game *entity.Game, _ interface{}) int64 {
|
||||||
|
return nOfAKind(game, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nOfAKind(game *entity.Game, n int) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
counts := counts(hand)
|
||||||
|
best := entity.Card{}
|
||||||
|
high := entity.Card{}
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
for k, v := range counts {
|
||||||
|
if v >= n && k.Value > best.Value {
|
||||||
|
best = k
|
||||||
|
}
|
||||||
|
if (v != n || k.Value != best.Value) && k.Value > high.Value {
|
||||||
|
high = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best.Value == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int64(best.Value*100 + high.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func highCard(game *entity.Game, _ interface{}) int64 {
|
||||||
|
hand := getHand(game)
|
||||||
|
values := make([]int, 0)
|
||||||
|
for _, card := range hand.AllCards() {
|
||||||
|
values = append(values, card.Value)
|
||||||
|
}
|
||||||
|
sort.Ints(values)
|
||||||
|
value := int64(0)
|
||||||
|
for i := len(values) - 1; i >= 0; i-- {
|
||||||
|
value = value*100 + int64(values[i])
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHand(game *entity.Game) entity.Hand {
|
||||||
|
player := game.Players[game.Current.Turn]
|
||||||
|
return player.Hand
|
||||||
|
}
|
||||||
|
|
||||||
|
func counts(hand entity.Hand) map[entity.Card]int {
|
||||||
|
cards := hand.AllCards()
|
||||||
|
m := make(map[entity.Card]int, len(cards))
|
||||||
|
for _, card := range cards {
|
||||||
|
if _, ok := m[card]; !ok {
|
||||||
|
m[card] = 0
|
||||||
|
}
|
||||||
|
m[card] += 1
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
505
src/game/rule/operation/hand_test.go
Normal file
505
src/game/rule/operation/hand_test.go
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHandCase struct {
|
||||||
|
public []entity.Card
|
||||||
|
private []entity.Card
|
||||||
|
want int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStraight(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"straight big 3": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 12},
|
||||||
|
entity.Card{Value: 13},
|
||||||
|
},
|
||||||
|
want: 13,
|
||||||
|
},
|
||||||
|
"straight sm 3": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
entity.Card{Value: 2},
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
"straight sm 2": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
entity.Card{Value: 2},
|
||||||
|
},
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
"not a straight": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, straight, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlush(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"not a flush": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 1, Suit: 0},
|
||||||
|
entity.Card{Value: 2, Suit: 1},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"big, big, small, small flush": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 12},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 2},
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
},
|
||||||
|
want: 12100201,
|
||||||
|
},
|
||||||
|
"small, small flush": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 2},
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
},
|
||||||
|
want: 201,
|
||||||
|
},
|
||||||
|
"big, small flush": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 1},
|
||||||
|
},
|
||||||
|
want: 1101,
|
||||||
|
},
|
||||||
|
"big, big flush": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 12},
|
||||||
|
},
|
||||||
|
want: 1211,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, flush, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHighCard(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"seven card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 13},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
},
|
||||||
|
want: 13111111110403,
|
||||||
|
},
|
||||||
|
"four card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 13},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
},
|
||||||
|
want: 13110403,
|
||||||
|
},
|
||||||
|
"three card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 13},
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
},
|
||||||
|
want: 130403,
|
||||||
|
},
|
||||||
|
"two card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
},
|
||||||
|
want: 403,
|
||||||
|
},
|
||||||
|
"one card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 3},
|
||||||
|
},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, highCard, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTwoPair(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"no pair": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one pair": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"no second pair": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 12},
|
||||||
|
entity.Card{Value: 12},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two pair diff": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
},
|
||||||
|
want: 111000,
|
||||||
|
},
|
||||||
|
"two pair same": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
},
|
||||||
|
want: 111100,
|
||||||
|
},
|
||||||
|
"two pair high": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 13},
|
||||||
|
},
|
||||||
|
want: 111013,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, twoPair, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullHouse(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"10 > 11": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 11},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
entity.Card{Value: 10},
|
||||||
|
},
|
||||||
|
want: 1011,
|
||||||
|
},
|
||||||
|
"five of a kind": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two pair": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 6},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, fullHouse, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNOfAKind(t *testing.T) {
|
||||||
|
cases4 := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"three cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"four cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 500,
|
||||||
|
},
|
||||||
|
"five cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases4 {
|
||||||
|
testHand(t, name, fourOfAKind, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases2 := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 500,
|
||||||
|
},
|
||||||
|
"three cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
"four cards, two diff pairs": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
entity.Card{Value: 4},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 504,
|
||||||
|
},
|
||||||
|
"four cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
"five cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases2 {
|
||||||
|
testHand(t, name, pair, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases3 := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one card": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"three cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 500,
|
||||||
|
},
|
||||||
|
"four cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
"five cards": {
|
||||||
|
public: []entity.Card{
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
entity.Card{Value: 5},
|
||||||
|
},
|
||||||
|
want: 505,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases3 {
|
||||||
|
testHand(t, name, threeOfAKind, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStraightFlush(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one nonroyal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 5}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one royal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 12}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two nonroyal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 5}, entity.Card{Value: 6}},
|
||||||
|
want: 6,
|
||||||
|
},
|
||||||
|
"twe royal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 12}, entity.Card{Value: 13}},
|
||||||
|
want: 13,
|
||||||
|
},
|
||||||
|
"twe mix cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 11}, entity.Card{Value: 10}},
|
||||||
|
want: 11,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, straightFlush, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoyalFlush(t *testing.T) {
|
||||||
|
cases := map[string]testHandCase{
|
||||||
|
"no cards": {
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one nonroyal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 5}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"one royal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 12}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"two nonroyal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 5}, entity.Card{Value: 6}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
"twe royal cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 12}, entity.Card{Value: 13}},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
"twe mix cards": {
|
||||||
|
public: []entity.Card{entity.Card{Value: 11}, entity.Card{Value: 10}},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
testHand(t, name, royalFlush, d.public, d.private, d.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHand(t *testing.T, name string, foo Int, public []entity.Card, private []entity.Card, want int) {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
hand := entity.Hand{
|
||||||
|
Public: public,
|
||||||
|
Private: private,
|
||||||
|
}
|
||||||
|
game := &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Hand: hand},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got := foo(game, nil)
|
||||||
|
if got != int64(want) {
|
||||||
|
t.Fatal(want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
41
src/game/rule/operation/int.go
Normal file
41
src/game/rule/operation/int.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Int func(*entity.Game, interface{}) int64
|
||||||
|
|
||||||
|
var intStringified = map[string]Int{
|
||||||
|
"royalFlush": royalFlush,
|
||||||
|
"straightFlush": straightFlush,
|
||||||
|
"fourOfAKind": fourOfAKind,
|
||||||
|
"fullHouse": fullHouse,
|
||||||
|
"flush": flush,
|
||||||
|
"straight": straight,
|
||||||
|
"threeOfAKind": threeOfAKind,
|
||||||
|
"twoPair": twoPair,
|
||||||
|
"pair": pair,
|
||||||
|
"highCard": highCard,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foo *Int) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return foo.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foo *Int) FromString(s string) error {
|
||||||
|
for k, v := range intStringified {
|
||||||
|
if k == s {
|
||||||
|
*foo = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("unknown int method " + s)
|
||||||
|
}
|
||||||
13
src/game/rule/operation/operation_test.go
Normal file
13
src/game/rule/operation/operation_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOperationInterface(t *testing.T) {
|
||||||
|
foo := func(*entity.Game, interface{}) int64 { return 0 }
|
||||||
|
var _ Int = foo
|
||||||
|
bar := func(*entity.Game, interface{}) bool { return false }
|
||||||
|
var _ Bool = bar
|
||||||
|
}
|
||||||
98
src/game/rule/operation/phase.go
Normal file
98
src/game/rule/operation/phase.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func charge(game *entity.Game, charge interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v := entity.Currency(convertNumber(charge))
|
||||||
|
game.ChargeActivePlayers(v)
|
||||||
|
game.NextPhase()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deal(game *entity.Game, deal interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := convertNumber(deal)
|
||||||
|
game.Deal(n)
|
||||||
|
game.NextPhase()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func bet(game *entity.Game, _ interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(game.ActivePlayers()) == 0 {
|
||||||
|
game.NextPhase()
|
||||||
|
game.NextTurn()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
player := &game.Players[game.Current.Turn]
|
||||||
|
raised := player.Active && player.Bet > game.Current.Bet
|
||||||
|
if raised {
|
||||||
|
game.Current.Bet = player.Bet
|
||||||
|
for i := range game.Players {
|
||||||
|
game.Players[i].Checked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.Checked = player.Active && (player.Checked || player.Balance == 0 || player.Bet == game.Current.Bet)
|
||||||
|
|
||||||
|
if game.IsAllActivePlayersChecked() && game.IsPotRight() {
|
||||||
|
game.NextPhase()
|
||||||
|
game.NextTurn()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if player.Checked {
|
||||||
|
game.NextTurn()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func trade(game *entity.Game, max interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n := convertNumber(max)
|
||||||
|
|
||||||
|
for _, player := range game.ActivePlayers() {
|
||||||
|
if player.Checked {
|
||||||
|
for len(player.Hand.Public) < n {
|
||||||
|
card := game.Deck.Draw()
|
||||||
|
player.Hand.Push(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete := game.IsAllActivePlayersChecked()
|
||||||
|
if complete {
|
||||||
|
game.NextPhase()
|
||||||
|
game.NextTurn()
|
||||||
|
}
|
||||||
|
return complete
|
||||||
|
}
|
||||||
|
|
||||||
|
func end(game *entity.Game, _ interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
game.NextPhase()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func playerCount(game *entity.Game, v interface{}) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := convertNumber(v)
|
||||||
|
return len(game.ActivePlayers()) == n
|
||||||
|
}
|
||||||
626
src/game/rule/operation/phase_test.go
Normal file
626
src/game/rule/operation/phase_test.go
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoolInterface(t *testing.T) {
|
||||||
|
var _ Bool = charge
|
||||||
|
var _ Bool = deal
|
||||||
|
var _ Bool = bet
|
||||||
|
var _ Bool = trade
|
||||||
|
var _ Bool = end
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCharge(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
game *entity.Game
|
||||||
|
charge interface{}
|
||||||
|
check func(bool, *entity.Game) bool
|
||||||
|
}{
|
||||||
|
"game is nil": {
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is nil": {
|
||||||
|
game: &entity.Game{},
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"cannot afford, active": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Balance: 4, Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
charge: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Balance == 4 && player.Pot == 0 && game.Pot() == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"cannot afford, not active": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Balance: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
charge: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Balance == 4 && player.Pot == 0 && game.Pot() == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"can afford, active": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Balance: 6, Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
charge: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Balance == 1 && player.Pot == 5 && game.Pot() == 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"can afford, not active": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Balance: 6},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
charge: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Balance == 6 && player.Pot == 0 && game.Pot() == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := charge(c.game, c.charge)
|
||||||
|
if approved := c.check(got, c.game); !approved {
|
||||||
|
t.Fatalf("not approved: got=%v, game=%+v", got, c.game)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeal(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
game *entity.Game
|
||||||
|
deal interface{}
|
||||||
|
check func(bool, *entity.Game) bool
|
||||||
|
}{
|
||||||
|
"game is nil": {
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is nil": {
|
||||||
|
game: &entity.Game{},
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no active, no hand": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deal: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Hand.Len() == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active, no hand": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deal: 5,
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
player := game.Players[0]
|
||||||
|
return a == true && player.Hand.Len() == 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if c.game != nil {
|
||||||
|
c.game.Deck = newDeck()
|
||||||
|
}
|
||||||
|
got := deal(c.game, c.deal)
|
||||||
|
if approved := c.check(got, c.game); !approved {
|
||||||
|
t.Fatalf("not approved: got=%v, game=%+v", got, c.game)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeck() entity.Deck {
|
||||||
|
deck := make([]entity.Card, 0, 26)
|
||||||
|
for i := 0; i < 52; i++ {
|
||||||
|
card := entity.Card{Suit: i % 4, Value: i % 13}
|
||||||
|
if i%2 == 1 {
|
||||||
|
deck = append(deck, card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entity.Deck{
|
||||||
|
Deck: deck,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBet(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
game *entity.Game
|
||||||
|
check func(bool, *entity.Game) bool
|
||||||
|
}{
|
||||||
|
"game is nil": {
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is nil": {
|
||||||
|
game: &entity.Game{},
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no active": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active, checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active, checked, pots wrong, has balance": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 2, Balance: 3},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, _ *entity.Game) bool {
|
||||||
|
return a == false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active, checked, pots wrong, no balance": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 2, Balance: 0},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"first turn in phase: checks": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 0, Balance: 5},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 0, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == false && game.Players[0].Checked && game.Current.Turn == 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"only turn in phase: checks": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 0, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked && game.Current.Turn == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"only turn in phase: raises": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 5, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked && game.Current.Turn == 0 && game.Players[0].Bet == 0 && game.Players[0].Pot == 5 && game.Current.Bet == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"first turn in phase: raises": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 5, Balance: 5},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 0, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == false && game.Players[0].Checked && game.Current.Turn == 1 && game.Players[0].Bet == 5 && game.Players[0].Pot == 0 && game.Current.Bet == 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"last: check": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 5, Balance: 5},
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 5, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 5,
|
||||||
|
Turn: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == true && !game.Players[0].Checked && game.Current.Turn == 0 && game.Players[0].Bet == 0 && game.Players[0].Pot == 5 && game.Current.Bet == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mid: raise": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 5, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 7, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 0, Balance: 15},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 5,
|
||||||
|
Turn: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == false && !game.Players[0].Checked && game.Current.Turn == 2 && game.Players[0].Bet == 5 && game.Players[0].Pot == 0 && game.Current.Bet == 7 && game.Players[1].Checked && !game.Players[2].Checked && game.Players[1].Bet == 7 && game.Players[1].Pot == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mid: check": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 0, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 0, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 0, Balance: 15},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == false && game.Players[0].Checked && game.Current.Turn == 2 && game.Players[0].Bet == 0 && game.Players[0].Pot == 0 && game.Current.Bet == 0 && game.Players[1].Checked && !game.Players[2].Checked && game.Players[1].Bet == 0 && game.Players[1].Pot == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"last: raise": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 0, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: true, Bet: 0, Balance: 15},
|
||||||
|
entity.Player{Active: true, Checked: false, Bet: 10, Balance: 5},
|
||||||
|
},
|
||||||
|
Current: entity.Current{
|
||||||
|
Bet: 0,
|
||||||
|
Turn: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(a bool, game *entity.Game) bool {
|
||||||
|
return a == false && !game.Players[0].Checked && game.Current.Turn == 0 && game.Players[0].Bet == 0 && game.Players[0].Pot == 0 && game.Current.Bet == 10 && !game.Players[1].Checked && game.Players[2].Checked && game.Players[1].Bet == 0 && game.Players[1].Pot == 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if c.game != nil {
|
||||||
|
c.game.Deck = newDeck()
|
||||||
|
}
|
||||||
|
got := bet(c.game, nil)
|
||||||
|
if approved := c.check(got, c.game); !approved {
|
||||||
|
t.Fatalf("not approved: got=%v, game=%+v", got, c.game)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrade(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
game *entity.Game
|
||||||
|
check func(*testing.T, bool, *entity.Game)
|
||||||
|
}{
|
||||||
|
"game is nil": {
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is nil": {
|
||||||
|
game: &entity.Game{},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if !a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1 player no trades checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: true,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if !a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
if !(game.Players[0].Hand.Public[0].Suit == 4 && game.Players[0].Hand.Public[1].Suit == 4 && game.Players[0].Hand.Public[2].Suit == 4) {
|
||||||
|
t.Fatal(game.Players[0].Hand.Public)
|
||||||
|
}
|
||||||
|
if game.Players[0].Checked {
|
||||||
|
t.Fatal(game.Players[0].Checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1 player trades checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: true,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if !a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
if !(game.Players[0].Hand.Public[0].Suit == 4) {
|
||||||
|
t.Fatal(game.Players[0].Hand.Public[0])
|
||||||
|
}
|
||||||
|
if len(game.Players[0].Hand.Public) != 3 {
|
||||||
|
t.Fatal(len(game.Players[0].Hand.Public))
|
||||||
|
}
|
||||||
|
if game.Players[0].Checked {
|
||||||
|
t.Fatal(game.Players[0].Checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1 player no trades not checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: false,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
if !(game.Players[0].Hand.Public[0].Suit == 4 && game.Players[0].Hand.Public[1].Suit == 4 && game.Players[0].Hand.Public[2].Suit == 4) {
|
||||||
|
t.Fatal(game.Players[0].Hand.Public)
|
||||||
|
}
|
||||||
|
if game.Players[0].Checked {
|
||||||
|
t.Fatal(game.Players[0].Checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1 player trades not checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: false,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
if game.Players[0].Hand.Public[0].Suit != 4 {
|
||||||
|
t.Fatal(game.Players[0].Hand.Public[0])
|
||||||
|
}
|
||||||
|
if len(game.Players[0].Hand.Public) != 1 {
|
||||||
|
t.Fatal(len(game.Players[0].Hand.Public))
|
||||||
|
}
|
||||||
|
if game.Players[0].Checked {
|
||||||
|
t.Fatal(game.Players[0].Checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1 player trades not checked, 1 player trades checked": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: false,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entity.Player{
|
||||||
|
Active: true,
|
||||||
|
Checked: true,
|
||||||
|
Hand: entity.Hand{
|
||||||
|
Public: []entity.Card{
|
||||||
|
entity.Card{Suit: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
if game.Players[0].Hand.Public[0].Suit != 4 {
|
||||||
|
t.Fatal(game.Players[0].Hand.Public[0])
|
||||||
|
}
|
||||||
|
if len(game.Players[0].Hand.Public) != 1 {
|
||||||
|
t.Fatal(len(game.Players[0].Hand.Public))
|
||||||
|
}
|
||||||
|
if game.Players[0].Checked {
|
||||||
|
t.Fatal(game.Players[0].Checked)
|
||||||
|
}
|
||||||
|
if game.Players[1].Hand.Public[0].Suit != 3 {
|
||||||
|
t.Fatal(game.Players[1].Hand.Public[0])
|
||||||
|
}
|
||||||
|
if len(game.Players[1].Hand.Public) != 3 {
|
||||||
|
t.Fatal(len(game.Players[1].Hand.Public))
|
||||||
|
}
|
||||||
|
if !game.Players[1].Checked {
|
||||||
|
t.Fatal(game.Players[1].Checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if c.game != nil {
|
||||||
|
c.game.Deck = newDeck()
|
||||||
|
}
|
||||||
|
got := trade(c.game, 3)
|
||||||
|
c.check(t, got, c.game)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerCount(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
game *entity.Game
|
||||||
|
check func(*testing.T, bool, *entity.Game)
|
||||||
|
}{
|
||||||
|
"game is nil": {
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is nil": {
|
||||||
|
game: &entity.Game{},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is short": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is high": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true},
|
||||||
|
entity.Player{Active: true},
|
||||||
|
entity.Player{Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"players is right": {
|
||||||
|
game: &entity.Game{
|
||||||
|
Players: []entity.Player{
|
||||||
|
entity.Player{Active: true},
|
||||||
|
entity.Player{Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a bool, game *entity.Game) {
|
||||||
|
if !a {
|
||||||
|
t.Fatal(a)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := playerCount(c.game, 2)
|
||||||
|
c.check(t, got, c.game)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/game/rule/operation/suit.go
Normal file
52
src/game/rule/operation/suit.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Suit int
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ Suit = iota
|
||||||
|
Heart Suit = iota
|
||||||
|
Spade Suit = iota
|
||||||
|
Diamond Suit = iota
|
||||||
|
Club Suit = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var suitStrings = map[string]Suit{
|
||||||
|
"heart": Heart,
|
||||||
|
"spade": Spade,
|
||||||
|
"diamond": Diamond,
|
||||||
|
"club": Club,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suit Suit) Is(card entity.Card) bool {
|
||||||
|
return card.Suit == int(suit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suit Suit) MarshalJSON() ([]byte, error) {
|
||||||
|
for k, v := range suitStrings {
|
||||||
|
if suit == v {
|
||||||
|
return json.Marshal(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.Marshal("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suit *Suit) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
err := json.Unmarshal(b, &s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range suitStrings {
|
||||||
|
if k == s {
|
||||||
|
*suit = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*suit = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
src/game/rule/operation/suit_test.go
Normal file
24
src/game/rule/operation/suit_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSuit(t *testing.T) {
|
||||||
|
var s Suit
|
||||||
|
if err := json.Unmarshal([]byte(`"heart"`), &s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if s != Heart {
|
||||||
|
t.Fatal(Heart, s)
|
||||||
|
} else if b, err := json.Marshal(s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if string(b) != `"heart"` {
|
||||||
|
t.Fatal(`"heart"`, string(b))
|
||||||
|
} else if card := (entity.Card{Suit: int(Heart)}); !s.Is(card) {
|
||||||
|
t.Fatal(true, s.Is(card))
|
||||||
|
} else if card := (entity.Card{Suit: int(Diamond)}); s.Is(card) {
|
||||||
|
t.Fatal(false, s.Is(card))
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/game/rule/operation/value.go
Normal file
73
src/game/rule/operation/value.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Value int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Joker Value = iota - 1
|
||||||
|
AceLow Value = iota
|
||||||
|
Two Value = iota
|
||||||
|
Three Value = iota
|
||||||
|
Four Value = iota
|
||||||
|
Five Value = iota
|
||||||
|
Six Value = iota
|
||||||
|
Seven Value = iota
|
||||||
|
Eight Value = iota
|
||||||
|
Nine Value = iota
|
||||||
|
Ten Value = iota
|
||||||
|
Jack Value = iota
|
||||||
|
Queen Value = iota
|
||||||
|
King Value = iota
|
||||||
|
AceHigh Value = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var valueStrings = map[string]Value{
|
||||||
|
"joker": Joker,
|
||||||
|
"aceLow": AceLow,
|
||||||
|
"two": Two,
|
||||||
|
"three": Three,
|
||||||
|
"four": Four,
|
||||||
|
"five": Five,
|
||||||
|
"six": Six,
|
||||||
|
"seven": Seven,
|
||||||
|
"eight": Eight,
|
||||||
|
"nine": Nine,
|
||||||
|
"ten": Ten,
|
||||||
|
"jack": Jack,
|
||||||
|
"queen": Queen,
|
||||||
|
"king": King,
|
||||||
|
"aceHigh": AceHigh,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value Value) Is(card entity.Card) bool {
|
||||||
|
return card.Value == int(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value Value) MarshalJSON() ([]byte, error) {
|
||||||
|
for k, v := range valueStrings {
|
||||||
|
if value == v {
|
||||||
|
return json.Marshal(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.Marshal("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *Value) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
err := json.Unmarshal(b, &s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range valueStrings {
|
||||||
|
if k == s {
|
||||||
|
*value = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*value = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
src/game/rule/operation/value_test.go
Normal file
24
src/game/rule/operation/value_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"local/sandbox/cards/src/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValue(t *testing.T) {
|
||||||
|
var v Value
|
||||||
|
if err := json.Unmarshal([]byte(`"jack"`), &v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if v != Jack {
|
||||||
|
t.Fatal(Jack, v)
|
||||||
|
} else if b, err := json.Marshal(v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if string(b) != `"jack"` {
|
||||||
|
t.Fatal(`"jack"`, string(b))
|
||||||
|
} else if card := (entity.Card{Value: int(Jack)}); !v.Is(card) {
|
||||||
|
t.Fatal(true, v.Is(card))
|
||||||
|
} else if card := (entity.Card{Value: int(Four)}); v.Is(card) {
|
||||||
|
t.Fatal(false, v.Is(card))
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/game/rule/phase.go
Normal file
5
src/game/rule/phase.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "local/sandbox/cards/src/game/rule/operation"
|
||||||
|
|
||||||
|
type Phase operation.Bool
|
||||||
40
src/game/rule/rule.go
Normal file
40
src/game/rule/rule.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "local/sandbox/cards/src/entity"
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Ends []End
|
||||||
|
Phases []Phase
|
||||||
|
Hands []Hand
|
||||||
|
Deck Deck
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule Rule) ShouldEnd(game *entity.Game) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if game.Current.Phase >= len(rule.Phases) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, end := range rule.Ends {
|
||||||
|
if _, ok := end(game); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule Rule) AdvancedPhase(game *entity.Game) bool {
|
||||||
|
if game == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rule.ShouldEnd(game) {
|
||||||
|
game.Current.Phase = len(rule.Phases)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ok := rule.Phases[game.Current.Phase]; ok {
|
||||||
|
game.Current.Phase += 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
12
src/game/storage.go
Normal file
12
src/game/storage.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,8 +44,10 @@ func TestGameMasterUpdate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
game.Players[2].ID = "hi"
|
game.Players.Add(entity.Player{
|
||||||
game.Pot = 123
|
ID: "hi",
|
||||||
|
Pot: 123,
|
||||||
|
})
|
||||||
err = gm.ReplaceGame(id, game)
|
err = gm.ReplaceGame(id, game)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
errGameExists = errors.New("cannot create game: already exists")
|
|
||||||
)
|
|
||||||
@@ -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{})
|
|
||||||
}
|
|
||||||
@@ -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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package storage
|
||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package storage
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user