initial commit
This commit is contained in:
commit
ac9aa86d33
14 changed files with 1021 additions and 0 deletions
30
internal/game/bot.go
Normal file
30
internal/game/bot.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package game
|
||||
|
||||
import "math/rand"
|
||||
|
||||
type Bot struct {
|
||||
Player
|
||||
}
|
||||
|
||||
func NewBot(id, name string) *Bot {
|
||||
return &Bot{
|
||||
Player: *NewPlayer(id, name),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) ChooseRank(hand []Card) Rank {
|
||||
// Bot simply picks a random card rank from its hand
|
||||
if len(hand) == 0 {
|
||||
return RankAce
|
||||
}
|
||||
idx := rand.Intn(len(hand))
|
||||
return hand[idx].Rank
|
||||
}
|
||||
|
||||
func (b *Bot) ChooseCardToAsk(availableRanks []Rank) Rank {
|
||||
if len(availableRanks) > 0 {
|
||||
idx := rand.Intn(len(availableRanks))
|
||||
return availableRanks[idx]
|
||||
}
|
||||
return RankAce
|
||||
}
|
||||
111
internal/game/cards.go
Normal file
111
internal/game/cards.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package game
|
||||
|
||||
import "math/rand"
|
||||
|
||||
type Rank int
|
||||
|
||||
const (
|
||||
RankAce Rank = iota
|
||||
Rank2
|
||||
Rank3
|
||||
Rank4
|
||||
Rank5
|
||||
Rank6
|
||||
Rank7
|
||||
Rank8
|
||||
Rank9
|
||||
Rank10
|
||||
RankJack
|
||||
RankQueen
|
||||
RankKing
|
||||
)
|
||||
|
||||
func (r Rank) String() string {
|
||||
switch r {
|
||||
case RankAce:
|
||||
return "A"
|
||||
case Rank2:
|
||||
return "2"
|
||||
case Rank3:
|
||||
return "3"
|
||||
case Rank4:
|
||||
return "4"
|
||||
case Rank5:
|
||||
return "5"
|
||||
case Rank6:
|
||||
return "6"
|
||||
case Rank7:
|
||||
return "7"
|
||||
case Rank8:
|
||||
return "8"
|
||||
case Rank9:
|
||||
return "9"
|
||||
case Rank10:
|
||||
return "10"
|
||||
case RankJack:
|
||||
return "J"
|
||||
case RankQueen:
|
||||
return "Q"
|
||||
case RankKing:
|
||||
return "K"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
type Suit int
|
||||
|
||||
const (
|
||||
SuitClubs Suit = iota
|
||||
SuitDiamonds
|
||||
SuitHearts
|
||||
SuitSpades
|
||||
)
|
||||
|
||||
func (s Suit) String() string {
|
||||
switch s {
|
||||
case SuitClubs:
|
||||
return "♣"
|
||||
case SuitDiamonds:
|
||||
return "♦"
|
||||
case SuitHearts:
|
||||
return "♥"
|
||||
case SuitSpades:
|
||||
return "♠"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
Rank Rank
|
||||
Suit Suit
|
||||
}
|
||||
|
||||
func NewDeck() []Card {
|
||||
var deck []Card
|
||||
for suit := SuitClubs; suit <= SuitSpades; suit++ {
|
||||
for rank := RankAce; rank <= RankKing; rank++ {
|
||||
deck = append(deck, Card{Rank: rank, Suit: suit})
|
||||
}
|
||||
}
|
||||
return deck
|
||||
}
|
||||
|
||||
func Shuffle(deck []Card) []Card {
|
||||
result := make([]Card, len(deck))
|
||||
copy(result, deck)
|
||||
for i := len(result) - 1; i > 0; i-- {
|
||||
j := rand.Intn(i + 1)
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var faceCards = map[Rank]bool{
|
||||
RankJack: true,
|
||||
RankQueen: true,
|
||||
RankKing: true,
|
||||
}
|
||||
|
||||
func IsFaceCard(r Rank) bool {
|
||||
return faceCards[r]
|
||||
}
|
||||
81
internal/game/game.go
Normal file
81
internal/game/game.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package game
|
||||
|
||||
type GameState int
|
||||
|
||||
const (
|
||||
StateWaiting GameState = iota
|
||||
StatePlaying
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
ID string
|
||||
Player1 *Player
|
||||
Player2 *Player
|
||||
Current int // 1 or 2, whose turn it is
|
||||
State GameState
|
||||
Deck []Card
|
||||
BookPile [][]Card
|
||||
}
|
||||
|
||||
func NewGame(p1, p2 *Player) *Game {
|
||||
deck := Shuffle(NewDeck())
|
||||
return &Game{
|
||||
ID: "",
|
||||
Player1: p1,
|
||||
Player2: p2,
|
||||
Current: 1,
|
||||
State: StatePlaying,
|
||||
Deck: deck,
|
||||
BookPile: [][]Card{},
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) DrawCard() Card {
|
||||
if len(g.Deck) == 0 {
|
||||
// Reshuffle book pile
|
||||
var all []Card
|
||||
for _, book := range g.BookPile {
|
||||
all = append(all, book...)
|
||||
}
|
||||
if len(all) > 0 {
|
||||
g.Deck = Shuffle(all)
|
||||
g.BookPile = [][]Card{}
|
||||
}
|
||||
}
|
||||
card := g.Deck[0]
|
||||
g.Deck = g.Deck[1:]
|
||||
return card
|
||||
}
|
||||
|
||||
func (g *Game) Ask(target *Player, rank Rank) bool {
|
||||
if !target.HasCard(rank) {
|
||||
// Player draws from deck
|
||||
card := g.DrawCard()
|
||||
target.Draw(card)
|
||||
return false // turn passes
|
||||
}
|
||||
// Target gives the card
|
||||
removed := target.RemoveCard(rank)
|
||||
if len(removed) > 0 {
|
||||
// Add to asker's hand
|
||||
g.Player1.Hand = append(g.Player1.Hand, removed...)
|
||||
// Check for book
|
||||
if g.Player1.HasBook(rank) {
|
||||
// Find and remove the 4 cards from hand
|
||||
var book []Card
|
||||
var remaining []Card
|
||||
count := 0
|
||||
for _, c := range g.Player1.Hand {
|
||||
if c.Rank == rank && count < 4 {
|
||||
book = append(book, c)
|
||||
count++
|
||||
} else {
|
||||
remaining = append(remaining, c)
|
||||
}
|
||||
}
|
||||
g.Player1.Hand = remaining
|
||||
g.BookPile = append(g.BookPile, book)
|
||||
}
|
||||
}
|
||||
return true // same player continues
|
||||
}
|
||||
56
internal/game/player.go
Normal file
56
internal/game/player.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package game
|
||||
|
||||
type Player struct {
|
||||
ID string
|
||||
Name string
|
||||
Hand []Card
|
||||
Booked [][]Card // sets of 4 cards
|
||||
Connected bool
|
||||
}
|
||||
|
||||
func NewPlayer(id, name string) *Player {
|
||||
return &Player{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Hand: []Card{},
|
||||
Booked: [][]Card{},
|
||||
Connected: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) Draw(card Card) {
|
||||
p.Hand = append(p.Hand, card)
|
||||
}
|
||||
|
||||
func (p *Player) RemoveCard(rank Rank) []Card {
|
||||
var removed []Card
|
||||
var remaining []Card
|
||||
for _, c := range p.Hand {
|
||||
if c.Rank == rank && len(removed) < 4 {
|
||||
removed = append(removed, c)
|
||||
} else {
|
||||
remaining = append(remaining, c)
|
||||
}
|
||||
}
|
||||
p.Hand = remaining
|
||||
return removed
|
||||
}
|
||||
|
||||
func (p *Player) HasCard(rank Rank) bool {
|
||||
for _, c := range p.Hand {
|
||||
if c.Rank == rank {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Player) HasBook(rank Rank) bool {
|
||||
count := 0
|
||||
for _, c := range p.Hand {
|
||||
if c.Rank == rank {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count == 4
|
||||
}
|
||||
80
internal/store/store.go
Normal file
80
internal/store/store.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"github.com/danyel/go-fish/internal/game"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type PlayerStore struct {
|
||||
mu sync.Mutex
|
||||
players map[string]*game.Player
|
||||
games map[string]*game.Game // keyed by player ID
|
||||
}
|
||||
|
||||
func NewPlayerStore() *PlayerStore {
|
||||
return &PlayerStore{
|
||||
players: make(map[string]*game.Player),
|
||||
games: make(map[string]*game.Game),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PlayerStore) AddPlayer(id, name string) *game.Player {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p := game.NewPlayer(id, name)
|
||||
s.players[id] = p
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *PlayerStore) GetPlayer(id string) (*game.Player, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p, ok := s.players[id]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (s *PlayerStore) GetAvailablePlayers() []*game.Player {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var available []*game.Player
|
||||
for _, p := range s.players {
|
||||
if p.Connected && len(p.Hand) < 7 {
|
||||
available = append(available, p)
|
||||
}
|
||||
}
|
||||
return available
|
||||
}
|
||||
|
||||
func (s *PlayerStore) RemovePlayer(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.players, id)
|
||||
delete(s.games, id)
|
||||
}
|
||||
|
||||
func (s *PlayerStore) MarkPlaying(id string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p, ok := s.players[id]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
p.Connected = false // player is now in a game
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PlayerStore) GetGame(id string) (*game.Game, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
g, ok := s.games[id]
|
||||
return g, ok
|
||||
}
|
||||
|
||||
func (s *PlayerStore) CreateGame(playerID string, g *game.Game) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// Store game under the player's ID so ask handler can find it
|
||||
s.games[playerID] = g
|
||||
// Also store under game ID
|
||||
s.games[g.ID] = g
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue