Initial commit

This commit is contained in:
Trevor Slocum 2020-05-29 15:31:56 -07:00
commit 08e4d196bf
12 changed files with 1361 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea/
dist/
vendor/
*.sh
jack

26
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,26 @@
image: golang:latest
stages:
- validate
- build
fmt:
stage: validate
script:
- gofmt -l -s -e .
- exit $(gofmt -l -s -e . | wc -l)
vet:
stage: validate
script:
- go vet -composites=false ./...
test:
stage: validate
script:
- go test -race -v ./...
build:
stage: build
script:
- go build

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# jack
[![CI status](https://gitlab.com/tslocum/jack/badges/master/pipeline.svg)](https://gitlab.com/tslocum/jack/commits/master)
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
Cribbage server
## Demo
[Cribbage.World](https://cribbage.world) is powered by jack.
## Support
Please share issues/suggestions [here](https://gitlab.com/tslocum/jack/issues).

60
card.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"fmt"
"strings"
"gitlab.com/tslocum/joker"
)
type PlayerCard struct {
joker.Card
Player int
}
func (c PlayerCard) String() string {
return fmt.Sprintf("{%d}%s", c.Player, c.Card)
}
type PlayerCards []PlayerCard
func (c PlayerCards) String() string {
var s strings.Builder
for i := range c {
if i > 0 {
s.WriteRune(',')
}
s.WriteString(c[i].String())
}
return s.String()
}
func (c PlayerCards) Len() int {
return len(c)
}
func (c PlayerCards) Less(i, j int) bool {
return c[i].Value() < c[j].Value()
}
func (c PlayerCards) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c PlayerCards) Cards() joker.Cards {
var cards = make(joker.Cards, len(c))
for i, card := range c {
cards[i] = card.Card
}
return cards
}
func (c PlayerCards) PlayerCards(player int) int {
var i int
for _, card := range c {
if card.Player == player {
i++
}
}
return i
}

190
client.go Normal file
View File

@ -0,0 +1,190 @@
package main
import (
"log"
"net"
"strconv"
"strings"
"github.com/gorilla/websocket"
"gitlab.com/tslocum/joker"
)
const ClientTerminating = -1
const ClientTelnet = 1
const ClientWebsocket = 2
type Client struct {
ConnType int
ConnTelnet net.Conn
ConnWebsocket *websocket.Conn
game *Game
gameplayer int
gameready bool
gamewaiting string
gamego bool
readbuffer chan string
writebuffer chan string
}
func (c *Client) initialize() {
c.readbuffer = make(chan string, 10)
c.writebuffer = make(chan string, 10)
}
func (c *Client) processRead() {
var command_pieces []string
var handled bool
for command := range c.readbuffer {
if command == "" || c.game == nil {
break
} else if c.ConnType == ClientTerminating {
continue
}
handled = false
command = strings.TrimLeft(command, " \r\n"+string(0))
command = strings.TrimRight(command, " \r\n"+string(0))
command_pieces = strings.Fields(strings.ToLower(command))
if len(command_pieces) > 0 {
handled = true
args := ""
if len(command_pieces) > 1 {
for i := 1; i < len(command_pieces); i++ {
command_piece := command_pieces[i]
command_piece = strings.TrimLeft(command_piece, " \r\n"+string(0))
command_piece = strings.TrimRight(command_piece, " \r\n"+string(0))
if command_piece != "" {
if args != "" {
args += " "
}
args += command_piece
}
}
}
card, sentCardIdentifier := joker.Parse(command_pieces[0])
if _, err := strconv.Atoi(command_pieces[0]); err == nil {
c.game.CommandQueue <- GameCommand{Player: c.gameplayer, Command: CommandRaw, Value: command_pieces[0] + " " + args}
} else if sentCardIdentifier && c.game.getHand(c.gameplayer).Contains(card) {
c.game.CommandQueue <- GameCommand{Player: c.gameplayer, Command: CommandRaw, Value: command_pieces[0] + " " + args}
} else if command_pieces[0] == "continue" {
c.game.CommandQueue <- GameCommand{Player: c.gameplayer, Command: CommandContinue, Value: args}
} else if command_pieces[0] == "cut" || command_pieces[0] == "c" || command_pieces[0] == "cu" {
c.game.CommandQueue <- GameCommand{Player: c.gameplayer, Command: CommandCut, Value: args}
} else if command_pieces[0] == "throw" || command_pieces[0] == "t" || command_pieces[0] == "th" {
c.game.CommandQueue <- GameCommand{Player: c.gameplayer, Command: CommandThrow, Value: args}
} else if command_pieces[0] == "print" && c.game != nil {
c.game.printAll()
} else {
handled = false
}
}
if !handled {
c.write(c.game.printGame(c.gameplayer))
}
}
}
func (c *Client) read(message string) {
if c.ConnType == ClientTerminating {
return
}
c.readbuffer <- message
}
func (c *Client) handleRead() {
if c.ConnType == ClientWebsocket {
for {
_, message, err := c.ConnWebsocket.ReadMessage()
if err != nil {
log.Println("WebSocket read error:", err)
break
}
log.Println("WebSocket read:", string(message))
c.readbuffer <- string(message)
}
} else {
buf := make([]byte, 4096)
var readtext string
var nextline int
for {
n, err := c.ConnTelnet.Read(buf)
if err != nil || n == 0 {
c.ConnTelnet.Close()
break
}
readtext += strings.TrimLeft(string(buf), "\r\n"+string(0))
for {
readtext = strings.TrimLeft(readtext, "\r\n"+string(0))
nextline = strings.Index(readtext, "\n")
if nextline <= 0 {
if readtext != "" {
c.read(readtext)
readtext = ""
}
break
}
c.read(readtext[:nextline+1])
readtext = readtext[nextline+1:]
}
buf = make([]byte, 4096)
}
log.Printf("Telnet connection closed")
}
c.terminate()
if c.game != nil && c.game.Phase != PhaseEnd {
c.game.end(c.game.getOpponent(c.gameplayer), "Player disconnected")
}
}
func (c *Client) terminate() {
c.write("")
c.ConnType = ClientTerminating
c.readbuffer <- ""
}
func (c *Client) write(message string) {
if c.ConnType == ClientTerminating {
return
}
c.writebuffer <- message
}
func (c *Client) handleWrite() {
for message := range c.writebuffer {
if message == "" {
break
} else if c.ConnType == ClientTerminating {
continue
}
if c.ConnType == ClientWebsocket {
err := c.ConnWebsocket.WriteMessage(websocket.TextMessage, []byte(message))
if err != nil {
log.Printf("WebSocket connection closed: %v", err)
break
}
} else {
_, err := c.ConnTelnet.Write([]byte(message + "\n\n"))
if err != nil {
c.ConnTelnet.Close()
log.Printf("Telnet connection closed: %v", err)
break
}
}
}
}

867
game.go Normal file
View File

@ -0,0 +1,867 @@
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"gitlab.com/tslocum/joker"
. "gitlab.com/tslocum/joker"
cribbage "gitlab.com/tslocum/joker-cribbage"
)
const (
PhaseEnd = -1
PhaseSetup = 0
PhasePick = 1
PhasePeg = 2
PhaseScore = 3
)
const (
PegPhaseNormal = 0
PegPhaseSolo = 1
PegPhaseFinal = 2
)
const WinningScore = 121
type Game struct {
CommandQueue chan GameCommand `json:"-"`
Deck *Deck `json:"-"`
Phase int `json:"phase"`
Dealer int `json:"dealer"`
Turn int `json:"turn"`
Starter Card `json:"starter"`
Player1 *Client `json:"-"`
Player2 *Client `json:"-"`
Hand1 Cards `json:"hand1"`
Hand2 Cards `json:"hand2"`
Crib PlayerCards `json:"crib"`
Score1 int `json:"score1"`
Score2 int `json:"score2"`
ThrowPile PlayerCards `json:"throwpile"`
DiscardPile PlayerCards `json:"-"`
PegPhase int `json:"-"`
sync.RWMutex `json:"-"`
}
func NewGame(player1 *Client, player2 *Client) *Game {
game := new(Game)
game.Initialize()
game.addPlayer(1, player1)
game.addPlayer(2, player2)
game.Deal()
return game
}
func (g *Game) Initialize() {
g.CommandQueue = make(chan GameCommand, 10)
g.Deck = joker.NewDeck(joker.StandardCards, 0)
g.Dealer = rand.Intn(2) + 1
if g.Dealer == 1 {
g.Turn = 2
} else {
g.Turn = 1
}
g.Reset()
}
func (g *Game) Reset() {
g.Deck.Shuffle()
g.Hand1 = Cards{}
g.Hand2 = Cards{}
g.Crib = PlayerCards{}
g.Starter = Card{}
g.ThrowPile = PlayerCards{}
g.DiscardPile = PlayerCards{}
// TODO SHUFFLE_MIN + rand.Intn(SHUFFLE_MAX-SHUFFLE_MIN)
g.Deck.Shuffle()
g.PegPhase = PegPhaseNormal
g.Phase = PhaseSetup
}
func (g *Game) addPlayer(player int, client *Client) {
client.game = g
client.gameplayer = player
client.gameready = true
if player == 1 {
g.Player1 = client
} else {
g.Player2 = client
}
}
func (g *Game) Deal() {
if g.Phase != PhaseSetup {
return
}
var dealtcards joker.Cards
var ok bool
g.Phase = PhasePick
g.ResetGo()
g.ResetReady()
for i := 0; i < 12; i++ {
dealtcards, ok = g.Deck.Draw(1)
if !ok {
log.Fatalf("failed to deal: not enough cards")
}
if i%2 == 0 {
g.Hand1 = append(g.Hand1, dealtcards[0])
} else {
g.Hand2 = append(g.Hand2, dealtcards[0])
}
}
sort.Sort(g.Hand1)
sort.Sort(g.Hand2)
}
func (g *Game) Cut(cut int) bool {
if g.Phase != PhasePick || g.Crib.Len() < 4 || g.Starter.Value() > 0 {
return false
}
g.logDebug("Cutting @", cut)
g.Starter = g.Deck.Cards[cut]
g.Phase = PhasePeg
for msgplayer := 1; msgplayer <= 2; msgplayer++ {
if g.getClient(msgplayer).ConnType == ClientTelnet {
g.getClient(msgplayer).write("Starter: " + g.Starter.String())
} else {
g.update(msgplayer)
}
}
if g.Starter.Identifier() == "j" {
g.award(g.Dealer, 2, cribbage.Peg, "Nibs")
}
return true
}
func (g *Game) exportJSON(player int) (string, error) {
gamestate := make(map[string]string)
gamereflected := reflect.ValueOf(g).Elem()
for i := 0; i < gamereflected.NumField(); i++ {
fieldname := gamereflected.Type().Field(i).Name
fieldtag := gamereflected.Type().Field(i).Tag.Get("json")
if fieldtag == "" || fieldtag == "-" {
continue
} else if g.Phase != PhaseScore && (fieldname == "Hand1" && player != 1) || (fieldname == "Hand2" && player != 2) {
hand := gamereflected.Field(i).Interface().(joker.Cards)
handprinted := ""
for j := 0; j < hand.Len(); j++ {
if j > 0 {
handprinted += ","
}
handprinted += "\"??\""
}
gamestate[fieldtag] = fmt.Sprintf("[%s]", handprinted)
} else if g.Phase != PhaseScore && fieldname == "Crib" {
hand := gamereflected.Field(i).Interface().(PlayerCards)
handprinted := ""
for j := 0; j < hand.Len(); j++ {
if j > 0 {
handprinted += ","
}
handprinted += "\"??\""
}
gamestate[fieldtag] = fmt.Sprintf("[%s]", handprinted)
} else {
jsonvalue, err := json.Marshal(gamereflected.Field(i).Interface())
if err != nil {
return "", err
}
gamestate[fieldtag] = string(jsonvalue)
}
}
if player > 0 {
gamestate["player"] = strconv.Itoa(player)
jsonvalue, err := json.Marshal(g.getClient(player).gamewaiting)
if err != nil {
return "", err
}
gamestate["waiting"] = string(jsonvalue)
}
throwPileSum := cribbage.Sum(g.ThrowPile.Cards())
gamestate["throwpilesum"] = strconv.Itoa(throwPileSum)
throwpilescore, _ := cribbage.Score(cribbage.Peg, g.ThrowPile.Cards(), g.Starter)
if g.Phase == PhasePeg && g.PegPhase == PegPhaseFinal && g.PeggingFinished() && throwPileSum != 31 {
throwpilescore++
}
gamestate["throwpilescore"] = strconv.Itoa(throwpilescore)
gamestatelines := []string{}
for statefield, statevalue := range gamestate {
gamestatelines = append(gamestatelines, fmt.Sprintf("\"%s\": %s", statefield, statevalue))
}
return fmt.Sprintf("{%s}", strings.Join(gamestatelines, ",")), nil
}
func (g *Game) Peg(player int) int {
if g.Phase != PhasePeg || g.Turn != player {
log.Println("Error, not players turn or other invalid state")
return -1
}
pegscore, _ := cribbage.Score(cribbage.Peg, g.ThrowPile.Cards(), g.Starter)
if pegscore > 0 {
g.award(g.Turn, pegscore, cribbage.Peg, "")
}
return pegscore
}
func (g *Game) award(player int, score int, scoretype cribbage.ScoringType, reason string) int {
if g.Phase != PhaseEnd {
points := "point"
if score != 1 {
points = "points"
}
st := "scored"
if scoretype == cribbage.Peg {
st = "pegged"
}
if reason != "" {
reason = " - " + reason
}
for msgplayer := 1; msgplayer <= 2; msgplayer++ {
g.getClient(msgplayer).write(fmt.Sprintf("%s %s %d %s%s", g.getName(msgplayer, player), st, score, points, reason))
}
if player == 1 {
g.Score1 += score
} else {
g.Score2 += score
}
if g.getScore(player) >= WinningScore {
g.end(player, "Won!")
}
}
return g.getScore(player)
}
func (g *Game) scoreHands() (int, int, int) {
if g.Phase != PhasePeg || g.Hand1.Len() > 0 || g.Hand2.Len() > 0 {
log.Println("Error, not pegging phase or player still has cards")
return -1, -1, -1
}
// TODO: Store player in card data or separate slice
g.Phase = PhaseScore
for _, card := range g.ThrowPile {
if card.Player == 1 {
g.Hand1 = append(g.Hand1, card.Card)
} else {
g.Hand2 = append(g.Hand2, card.Card)
}
}
for _, card := range g.DiscardPile {
if card.Player == 1 {
g.Hand1 = append(g.Hand1, card.Card)
} else {
g.Hand2 = append(g.Hand2, card.Card)
}
}
sort.Sort(g.Hand1)
sort.Sort(g.Hand2)
opponentHandScore, _ := cribbage.Score(cribbage.ShowHand, *g.getHand(g.getOpponent(g.Dealer)), g.Starter)
dealerHandScore, _ := cribbage.Score(cribbage.ShowHand, *g.getHand(g.Dealer), g.Starter)
dealerCribScore, _ := cribbage.Score(cribbage.Peg, g.Crib.Cards(), g.Starter)
// Awarding order is important
opponentscore := g.award(g.getOpponent(g.Dealer), opponentHandScore, cribbage.ShowHand, "Hand")
dealerscore := g.award(g.Dealer, dealerHandScore, cribbage.ShowHand, "Hand")
dealercribscore := g.award(g.Dealer, dealerCribScore, cribbage.ShowHand, "Crib")
var yourscore string
var theirscore string
dealerscoreprinted := fmt.Sprintf("%d [%d-%d]", dealerHandScore+dealerCribScore, dealerHandScore, dealerCribScore)
opponentscoreprinted := fmt.Sprintf("%d", opponentHandScore)
for msgplayer := 1; msgplayer <= 2; msgplayer++ {
if g.Dealer == msgplayer {
yourscore = dealerscoreprinted
theirscore = opponentscoreprinted
} else {
yourscore = opponentscoreprinted
theirscore = dealerscoreprinted
}
g.getClient(msgplayer).gameready = false
g.getClient(msgplayer).gamewaiting = "You scored " + yourscore + " - Your opponent scored " + theirscore
}
return opponentscore, dealerscore, dealercribscore
}
func (g *Game) end(player int, reason string) {
g.CommandQueue <- GameCommand{Player: player, Command: CommandEnd, Value: reason}
}
func (g *Game) Throw(player int, cardidentifier string) bool {
if g.Phase == PhaseScore {
return false
} else if g.Phase == PhasePick {
// TODO Store players of thrown cards or check number of cards still left in thrower hand
if g.Crib.PlayerCards(player) == 2 {
log.Println("Error, already thrown")
return false
}
} else if g.Phase == PhasePeg && (g.Turn != player || g.PegPhase == PegPhaseFinal) {
log.Println("Error, not players turn")
return false
}
card, ok := Parse(cleanIdentifier(cardidentifier))
if !ok {
log.Println("Attempted to throw invalid card:", player, cardidentifier)
return false
}
playerHand := g.getHand(player)
opponentHand := g.getHand(g.getOpponent(player))
if !playerHand.Contains(card) {
log.Println("Attempted to throw card not in hand:", player, cardidentifier, playerHand)
return false
}
if g.Phase == PhasePeg {
if cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(card) > 31 {
g.getClient(player).write("Error: Illegal throw, would exceed 31")
return false
}
}
*playerHand = (*playerHand).Remove(card)
if g.Phase == PhasePick {
g.Crib = append(g.Crib, PlayerCard{card, player})
sort.Sort(g.Crib)
/*if len(g.Crib.Cards) == 4 && player == g.Dealer {
g.updateOpponent(player)
}*/
// TODO: Temporary before cut UI
if len(g.Crib) == 4 {
g.Cut(rand.Intn(32) + 4)
}
} else if g.Phase == PhasePeg {
g.ThrowPile = append(g.ThrowPile, PlayerCard{card, player})
g.Peg(player)
if cribbage.Sum(g.ThrowPile.Cards()) == 31 {
log.Println("=31 move to solo")
g.PegPhase = PegPhaseSolo
g.pegTurn(player)
} else if cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(opponentHand.Low()) > 31 {
if opponentHand.Len() > 0 && !g.getClient(g.getOpponent(player)).gamego {
log.Println("Setting Go - Player", g.getOpponent(player))
g.getClient(g.getOpponent(player)).gamego = true
g.getClient(g.getOpponent(player)).gamewaiting = "Go"
g.getClient(g.getOpponent(player)).gameready = false
g.NextTurn()
} else if g.PeggingFinished() {
log.Println(">31 pegging finished move to solo")
g.PegPhase = PegPhaseSolo
g.pegTurn(player)
}
} else {
g.NextTurn()
}
}
return true
}
func (g *Game) PeggingFinished() bool {
return cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(g.getHand(1).Low()) > 31 && cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(g.getHand(2).Low()) > 31
}
func (g *Game) ResetGo() {
if g.Phase > 0 {
g.logDebug("Reset go")
g.Player1.gamego = false
g.Player2.gamego = false
}
}
func (g *Game) ResetReady() {
if g.Phase > 0 {
g.Player1.gameready = true
g.Player1.gamewaiting = ""
g.Player2.gameready = true
g.Player2.gamewaiting = ""
}
}
func (g *Game) Ready(player int) {
g.getClient(player).gameready = true
g.getClient(player).gamewaiting = "Waiting on opponent..."
}
func (g *Game) AllReady() bool {
return g.Player1.gameready && g.Player2.gameready
}
func (g *Game) NextTurn() {
g.logDebug("Turn...")
if g.Turn == 1 {
g.Turn = 2
} else {
g.Turn = 1
}
}
func (g *Game) NextHand() {
g.Turn = g.Dealer
g.Dealer = g.getOpponent(g.Dealer)
g.Reset()
g.Deal()
}
func (g *Game) getClient(player int) *Client {
if player == 1 {
return g.Player1
} else {
return g.Player2
}
}
func (g *Game) getOpponent(player int) int {
if player == 1 {
return 2
} else {
return 1
}
}
func (g *Game) getName(player int, clientplayer int) string {
if player == clientplayer {
return "You"
} else {
return "Your opponent"
}
}
func (g *Game) getScore(player int) int {
if player == 1 {
return g.Score1
} else {
return g.Score2
}
}
func (g *Game) getHand(player int) *Cards {
if player == 1 {
return &g.Hand1
} else {
return &g.Hand2
}
}
func (g *Game) update(player int) {
g.sendGameState(player)
}
func (g *Game) updateOpponent(player int) {
if player == 1 {
g.update(2)
} else {
g.update(1)
}
}
func (g *Game) updateAll() {
g.update(1)
g.update(2)
}
func (g *Game) printScoring() {
for player := 1; player <= 2; player++ {
gameprinted := []string{}
opponentscore, _ := cribbage.Score(cribbage.ShowHand, *g.getHand(g.getOpponent(g.Dealer)), g.Starter)
dealerscore, _ := cribbage.Score(cribbage.ShowHand, *g.getHand(g.Dealer), g.Starter)
dealercribscore, _ := cribbage.Score(cribbage.ShowCrib, g.Crib.Cards(), g.Starter)
dealerscoreprinted := fmt.Sprintf("%d [%d-%d]", dealerscore+dealercribscore, dealerscore, dealercribscore)
opponentscoreprinted := fmt.Sprintf("%d", opponentscore)
var yourscore string
var theirscore string
if g.Dealer == player {
yourscore = dealerscoreprinted
theirscore = opponentscoreprinted
} else {
yourscore = opponentscoreprinted
theirscore = dealerscoreprinted
}
gameprinted = append(gameprinted, "Your hand(s) scored "+yourscore+" - Your opponent's hand(s) scored "+theirscore)
gameprinted = append(gameprinted, "Starter "+g.Starter.String())
gameprinted = append(gameprinted, fmt.Sprintf("Opponent (%2d) %s", opponentscore, g.getHand(g.getOpponent(g.Dealer))))
gameprinted = append(gameprinted, fmt.Sprintf("Dealer (%2d) %s", dealerscore, g.getHand(g.Dealer)))
gameprinted = append(gameprinted, fmt.Sprintf("Dealer c (%2d) %s", dealercribscore, g.Crib))
g.getClient(player).write(strings.Join(gameprinted, "\n"))
}
}
func (g *Game) printGame(player int) string {
lightprint := false
gameprinted := []string{}
if g.Phase == PhasePeg {
gameprinted = append(gameprinted, "Pegging: "+g.ThrowPile.String()+" ("+strconv.Itoa(cribbage.Sum(g.ThrowPile.Cards()))+")")
if g.Turn == player {
gameprinted = append(gameprinted, fmt.Sprintf("Please throw a card. (Enter 1-%d)", g.getHand(player).Len()))
} else if g.getHand(player).Len() > 0 && cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(g.getHand(player).Low()) > 31 {
lightprint = true
gameprinted = append(gameprinted, "Go - Waiting on opponent...")
} else {
lightprint = true
gameprinted = append(gameprinted, "Waiting on opponent...")
}
}
gameprintedstr := ""
if g.Phase == PhasePick {
cardsthrown := 0
for _, card := range g.Crib {
if card.Player == player {
cardsthrown++
}
}
if cardsthrown < 2 {
if cardsthrown == 0 {
gameprintedstr += "Please throw two cards for "
} else {
gameprintedstr += "Please throw one additional card for "
}
if g.Dealer == player {
gameprintedstr += "your"
} else {
gameprintedstr += "your opponent's"
}
gameprintedstr += fmt.Sprintf(" crib. (Enter 1-%d, throw multiple cards by separating card#'s with a space)", g.getHand(player).Len())
} else if len(g.Crib) < 4 {
lightprint = true
gameprintedstr += "Waiting on opponent..."
} else if g.Starter.Value() == 0 {
lightprint = true
if g.Dealer != player {
gameprintedstr += "Please cut the deck. (Enter 4-36 or c/cut for a random cut)"
} else {
gameprintedstr += "Waiting on opponent..."
}
}
gameprinted = append(gameprinted, gameprintedstr)
gameprintedstr = ""
}
if !lightprint {
if len(gameprinted) > 0 {
gameprinted = append(gameprinted, "")
}
gameprinted = append(gameprinted, fmt.Sprintf("Score: (%s) %d / %d (%s)", g.getName(1, player), g.getScore(1), g.getScore(2), g.getName(2, player)))
gameprinted = append(gameprinted, "Your cards: "+g.getHand(player).String())
}
return strings.Join(gameprinted, "\n")
}
func (g *Game) printAll() {
log.Println("Printing game state...")
var phaseprinted string
switch g.Phase {
case PhaseSetup:
phaseprinted = "Setup"
break
case PhasePick:
phaseprinted = "Pick"
break
case PhasePeg:
phaseprinted = "Peg"
switch g.Phase {
case PegPhaseNormal:
phaseprinted += " Normal"
break
case PegPhaseSolo:
phaseprinted += " Solo"
break
case PegPhaseFinal:
phaseprinted += " Final"
break
}
break
case PhaseScore:
phaseprinted = "Score"
break
case PhaseEnd:
phaseprinted = "End"
}
log.Printf("[%s] Dealer: %d - Turn: %d", phaseprinted, g.Dealer, g.Turn)
if g.Phase > 0 {
log.Printf("Player 1 ready: %t - Player 2 ready: %t", g.Player1.gameready, g.Player2.gameready)
}
var starter Card
if g.Starter.Value() > 0 {
starter = g.Starter
}
hand1Score, hand1ScoreResults := cribbage.Score(cribbage.ShowHand, g.Hand1, starter)
log.Println("Hand 1 [", hand1Score, "]", g.Hand1.String(), hand1ScoreResults)
cribScore, cribScoreResults := cribbage.Score(cribbage.ShowCrib, g.Crib.Cards(), starter)
log.Println("Crib [", cribScore, "]", g.Crib.Cards().String(), cribScoreResults)
hand2Score, hand2ScoreResults := cribbage.Score(cribbage.ShowHand, g.Hand2, starter)
log.Println("Hand 2 [", hand2Score, "]", g.Hand2.String(), hand2ScoreResults)
if g.Starter.Value() > 0 {
log.Println("Starter:", g.Starter.String())
}
if g.ThrowPile.Len() > 0 {
pegScore, pegResults := cribbage.Score(cribbage.Peg, g.ThrowPile.Cards(), starter)
log.Println("Pegging: [", pegScore, "]", g.ThrowPile.Cards().String(), pegResults)
}
if g.DiscardPile.Len() > 0 {
log.Println("Discard:", g.DiscardPile.String())
}
}
func (g *Game) sendGameState(player int) {
client := g.getClient(player)
if client.ConnType == ClientWebsocket {
gamestate, err := g.exportJSON(player)
if err != nil {
log.Fatal("failed to marshal game state:", err)
}
client.write(string(gamestate))
} else {
client.write(g.printGame(player))
}
}
func (g *Game) pegTurn(player int) {
if g.Phase != PhasePeg {
return
}
g.logDebug("PEG CONTINUE")
if cribbage.Sum(g.ThrowPile.Cards())+cribbage.Value(g.getHand(player).Low()) > 31 && g.PegPhase == PegPhaseNormal {
g.logDebug("PEG NEXT TURN")
if g.PeggingFinished() {
g.ResetGo()
g.PegPhase = PegPhaseSolo
g.getClient(g.getOpponent(player)).gameready = false
g.getClient(g.getOpponent(player)).gamewaiting = "Go - Pegging finished"
}
g.NextTurn()
} else if g.PeggingFinished() {
if g.PegPhase == PegPhaseSolo {
g.logDebug("ENTERING FINAL PEG")
g.PegPhase = PegPhaseFinal
finalpeg, _ := cribbage.Score(cribbage.Peg, g.ThrowPile.Cards(), g.Starter)
if cribbage.Sum(g.ThrowPile.Cards()) != 31 {
finalpeg++
g.award(g.Turn, 1, cribbage.Peg, "Last card")
}
g.getClient(g.Turn).gameready = false
g.getClient(g.Turn).gamewaiting = "You pegged " + strconv.Itoa(finalpeg) + " point"
if finalpeg != 1 {
g.getClient(g.Turn).gamewaiting += "s"
}
g.getClient(g.getOpponent(g.Turn)).gameready = false
g.getClient(g.getOpponent(g.Turn)).gamewaiting = "Your opponent pegged " + strconv.Itoa(finalpeg) + " point"
if finalpeg != 1 {
g.getClient(g.getOpponent(g.Turn)).gamewaiting += "s"
}
g.NextTurn()
} else if g.PegPhase == PegPhaseFinal {
g.logDebug("FINAL PEG")
g.PegPhase = PegPhaseNormal
if g.Hand1.Len() == 0 && g.Hand2.Len() == 0 {
if cribbage.Sum(g.ThrowPile.Cards()) != 31 {
g.award(g.Turn, 1, cribbage.Peg, "Last card")
}
g.scoreHands()
g.printScoring()
g.NextTurn()
} else {
g.DiscardPile = append(g.DiscardPile, g.ThrowPile...)
g.ThrowPile = PlayerCards{}
}
}
}
}
func (g *Game) processGameCommand(command GameCommand) {
g.Lock()
defer g.Unlock()
if command.Command == CommandEnd {
g.Phase = PhaseEnd
g.updateAll()
return
}
command_args := strings.Fields(command.Value)
if command.Command == CommandRaw {
if g.Phase == PhasePick && g.Crib.Len() == 4 && g.Starter.Value() == 0 {
command.Command = CommandCut
} else {
command.Command = CommandThrow
}
}
if g.Phase == PhasePick && command.Command == CommandCut {
if len(command_args) > 0 {
cut, err := strconv.Atoi(command_args[0])
if err == nil && cut >= 4 && cut <= 36 {
g.Cut(cut - 1)
}
}
g.Cut(rand.Intn(32) + 4)
} else if command.Command == CommandContinue {
if g.getClient(command.Player).gameready {
return
}
g.Ready(command.Player)
if g.AllReady() {
g.Player1.gamewaiting = ""
g.Player2.gamewaiting = ""
g.logDebug("ALL READY")
if g.Phase == PhasePeg {
g.pegTurn(command.Player)
} else if g.Phase == PhaseScore {
g.NextHand()
g.update(g.getOpponent(command.Player))
}
}
} else if command.Command == CommandThrow {
if g.Phase == PhasePick || g.Phase == PhasePeg {
cards := []string{}
lastindex := 0
for i, cardarg := range command_args {
cardindex, err := strconv.Atoi(cardarg)
if err == nil {
if cardindex == lastindex {
break
}
if cardindex >= 1 && g.getHand(command.Player).Len() >= cardindex {
cards = append(cards, (*g.getHand(command.Player))[(cardindex-1)].Identifier())
lastindex = cardindex
}
} else {
cards = append(cards, cardarg)
}
if i > 1 {
break
}
}
for _, card := range cards {
g.Throw(command.Player, card)
}
}
}
g.updateAll()
}
func (g *Game) playGame() {
log.Println("Starting new game")
g.updateAll()
for command := range g.CommandQueue {
g.logDebug("Received GameCommand", command.ToStr())
g.processGameCommand(command)
if g.Phase == PhaseEnd {
break
}
}
}
func (g *Game) logDebug(args ...interface{}) {
log.Println("phase:", g.Phase,
"turn:", g.Turn,
"dealer:", g.Dealer, args)
}
func cleanIdentifier(cardidentifier string) string {
return strings.ToLower(strings.TrimLeft(strings.TrimRight(cardidentifier, " \r\n"+string(0)), " \r\n"+string(0)))
}

35
gamecommand.go Normal file
View File

@ -0,0 +1,35 @@
package main
import "fmt"
const CommandEnd = -1
const CommandRaw = 0
const CommandContinue = 1
const CommandThrow = 2
const CommandCut = 3
type GameCommand struct {
Player int
Command int
Value string
}
func (gc GameCommand) ToStr() string {
var commandprinted string
switch gc.Command {
case CommandEnd:
commandprinted = "End"
break
case CommandRaw:
commandprinted = "Raw"
case CommandContinue:
commandprinted = "Continue"
break
case CommandCut:
commandprinted = "Cut"
break
case CommandThrow:
commandprinted = "Throw"
}
return fmt.Sprintf("Player %d - %s - %s", gc.Player, commandprinted, gc.Value)
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module gitlab.com/tslocum/jack
go 1.14
require (
github.com/gorilla/websocket v1.4.2
gitlab.com/tslocum/joker v0.1.3-0.20200529165915-df7bd71a3bcd
gitlab.com/tslocum/joker-cribbage v0.1.2-0.20200206160759-4543b916d838
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
gitlab.com/tslocum/joker v0.1.2 h1:7ujvgkGNUJbrvpXvNHSvKWFDYIKTCWbvFcWL1IbRVWA=
gitlab.com/tslocum/joker v0.1.2/go.mod h1:bxTQ0FFmBP465r9z76zcm97S4Ld9eCLa3q20TyVM82A=
gitlab.com/tslocum/joker v0.1.3-0.20200529165915-df7bd71a3bcd h1:nDmVh1g7yCbffyAozEPQsCRHc1MQ30P+u5FQQlZwa8A=
gitlab.com/tslocum/joker v0.1.3-0.20200529165915-df7bd71a3bcd/go.mod h1:bxTQ0FFmBP465r9z76zcm97S4Ld9eCLa3q20TyVM82A=
gitlab.com/tslocum/joker-cribbage v0.1.2-0.20200206160759-4543b916d838 h1:Z3Sv+ArlCL2NezHmv6U6HA2GOY40Ed8+eCu4WrIZvdk=
gitlab.com/tslocum/joker-cribbage v0.1.2-0.20200206160759-4543b916d838/go.mod h1:/fD4xxguXPg+N9s5+Q6t6tib1xfvh7zZ6AQ6TZXViDA=

29
main.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"log"
"math/rand"
"net/http"
"time"
)
func main() {
rand.Seed(time.Now().UTC().UnixNano())
go func() {
log.Fatal(http.ListenAndServe(":8880", nil))
}()
log.Println("CribServer initialized")
cs := CribServer{}
cs.clientqueuealert = make(chan bool)
go cs.matchPlayers()
go cs.listenWebSocket()
cs.listenTelnet()
// TODO
select {}
}

98
server.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"log"
"net"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type CribServer struct {
clients []*Client
clientqueue []*Client
clientqueuealert chan bool
sync.RWMutex
}
func (cs *CribServer) handleReadWebSocket(w http.ResponseWriter, r *http.Request) {
log.Println("New WebSocket")
c, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("WebSocket upgrade error:", err)
return
}
defer c.Close()
client := &Client{ConnType: ClientWebsocket, ConnWebsocket: c}
client.initialize()
cs.addClient(client)
}
func (cs *CribServer) listenWebSocket() {
http.HandleFunc("/crib", cs.handleReadWebSocket)
http.ListenAndServe(":8884", nil)
}
func (cs *CribServer) addClient(client *Client) {
cs.Lock()
cs.clients = append(cs.clients, client)
cs.Unlock()
go client.processRead()
go client.handleWrite()
cs.queueClient(client)
client.handleRead()
}
func (cs *CribServer) listenTelnet() {
ln, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println(err)
return
}
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
continue
}
client := &Client{ConnType: ClientTelnet, ConnTelnet: conn}
client.initialize()
go cs.addClient(client)
}
}
func (cs *CribServer) queueClient(client *Client) {
cs.Lock()
cs.clientqueue = append(cs.clientqueue, client)
cs.Unlock()
cs.clientqueuealert <- true
}
func (cs *CribServer) matchPlayers() {
for range cs.clientqueuealert {
cs.Lock()
if len(cs.clientqueue) > 1 {
go cs.startGame(cs.clientqueue[0], cs.clientqueue[1])
cs.clientqueue = cs.clientqueue[2:]
}
cs.Unlock()
}
}
func (cs *CribServer) startGame(player1, player2 *Client) {
game := NewGame(player1, player2)
game.playGame()
}