aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/table.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/table.go')
-rw-r--r--internal/casino/table.go147
1 files changed, 97 insertions, 50 deletions
diff --git a/internal/casino/table.go b/internal/casino/table.go
index e993cb8..be2ea18 100644
--- a/internal/casino/table.go
+++ b/internal/casino/table.go
@@ -6,6 +6,81 @@ import (
"sync"
)
+// tableGame is a multiplayer table game implementation. Table routes every
+// player interaction through this interface so new games plug in without
+// changing the dispatch layer.
+type tableGame interface {
+ join(name string) []Event
+ leave(name string) []Event
+ bet(name, spot string, amount int, wallet Wallet) []Event
+ tick() []Event
+ action(name, action string) []Event
+ rebet(name string) (spot string, amount int)
+ markDisconnected(name string) []Event
+ markConnected(name string)
+ hasParticipant(name string) bool
+}
+
+// tableBase carries the identity every table game needs to build events.
+type tableBase struct {
+ RoomID int
+ Config TableConfig
+}
+
+func (b tableBase) event(message string, public bool) Event {
+ return Event{RoomID: b.RoomID, TableID: b.Config.ID, Type: EventResult, Message: message, Public: public, Color: "casino_action"}
+}
+
+func (b tableBase) private(name, message string) Event {
+ e := b.event(message, false)
+ e.PrivateTo = name
+ return e
+}
+
+func (b tableBase) playerAction(name, publicMessage, privateMessage string) Event {
+ e := b.event(publicMessage, true)
+ e.PrivateTo = name
+ e.PrivateMessage = privateMessage
+ return e
+}
+
+// Table is the manager-facing handle for a multiplayer table. It only holds
+// routing state; all game logic lives in the tableGame implementation.
+type Table struct {
+ RoomID int
+ Config TableConfig
+ game tableGame
+}
+
+func newTable(roomID int, cfg TableConfig, rng *rand.Rand) *Table {
+ cfg = cfg.Normalize()
+ t := &Table{RoomID: roomID, Config: cfg}
+ switch cfg.Game {
+ case GameBlackjack:
+ t.game = newBlackjackTable(roomID, cfg, rng)
+ case GameBaccarat:
+ t.game = newBaccaratTable(roomID, cfg, rng)
+ default:
+ t.game = newGenericTable(roomID, cfg, rng)
+ }
+ return t
+}
+
+func (t *Table) join(name string) []Event { return t.game.join(name) }
+func (t *Table) leave(name string) []Event { return t.game.leave(name) }
+
+func (t *Table) bet(name, spot string, amount int, wallet Wallet) []Event {
+ return t.game.bet(name, spot, amount, wallet)
+}
+
+func (t *Table) tick() []Event { return t.game.tick() }
+func (t *Table) action(name, action string) []Event { return t.game.action(name, action) }
+func (t *Table) rebet(name string) (string, int) { return t.game.rebet(name) }
+
+func (t *Table) markDisconnected(name string) []Event { return t.game.markDisconnected(name) }
+func (t *Table) markConnected(name string) { t.game.markConnected(name) }
+func (t *Table) hasParticipant(name string) bool { return t.game.hasParticipant(name) }
+
type tablePhase string
const (
@@ -13,7 +88,9 @@ const (
phaseBetting tablePhase = "betting"
)
-type Table struct {
+// genericTable is the fallback table game: every bettor's wager resolves
+// immediately against a simple spin once all connected players have bet.
+type genericTable struct {
mu sync.Mutex
RoomID int
Config TableConfig
@@ -23,24 +100,19 @@ type Table struct {
phase tablePhase
timer countdown
rng *rand.Rand
- blackjack *blackjackTable
}
-func newTable(roomID int, cfg TableConfig, rng *rand.Rand) *Table {
- t := &Table{
+func newGenericTable(roomID int, cfg TableConfig, rng *rand.Rand) *genericTable {
+ return &genericTable{
RoomID: roomID,
Config: cfg,
phase: phaseWaiting,
participants: make(map[string]*Participant),
rng: rng,
}
- if cfg.Game == GameBlackjack {
- t.blackjack = newBlackjackTable(roomID, cfg, rng)
- }
- return t
}
-func (t *Table) seats() []*Participant {
+func (t *genericTable) seats() []*Participant {
seats := make([]*Participant, 0, len(t.order))
for _, name := range t.order {
if p := t.participants[name]; p != nil {
@@ -50,10 +122,7 @@ func (t *Table) seats() []*Participant {
return seats
}
-func (t *Table) join(name string) []Event {
- if t.blackjack != nil {
- return t.blackjack.join(name)
- }
+func (t *genericTable) join(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
if p := t.participants[name]; p != nil {
@@ -68,10 +137,7 @@ func (t *Table) join(name string) []Event {
return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventJoined, Player: name, Message: fmt.Sprintf("%s joins the %s table.", name, t.Config.ID), Public: true}}
}
-func (t *Table) leave(name string) []Event {
- if t.blackjack != nil {
- return t.blackjack.leave(name)
- }
+func (t *genericTable) leave(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
p := t.participants[name]
@@ -95,16 +161,16 @@ func (t *Table) leave(name string) []Event {
return events
}
-func (t *Table) bet(name string, amount int, wallet Wallet) []Event {
- if t.blackjack != nil {
- return t.blackjack.bet(name, amount, wallet)
- }
+func (t *genericTable) bet(name, spot string, amount int, wallet Wallet) []Event {
t.mu.Lock()
defer t.mu.Unlock()
p := t.participants[name]
if p == nil {
return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "You aren't playing at that table."}}
}
+ if spot != "" {
+ return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "That table doesn't take a bet target."}}
+ }
if t.phase != phaseWaiting && t.phase != phaseBetting {
return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "That table isn't accepting bets."}}
}
@@ -135,10 +201,7 @@ func (t *Table) bet(name string, amount int, wallet Wallet) []Event {
return events
}
-func (t *Table) tick() []Event {
- if t.blackjack != nil {
- return t.blackjack.tick()
- }
+func (t *genericTable) tick() []Event {
t.mu.Lock()
defer t.mu.Unlock()
if t.phase != phaseBetting {
@@ -163,7 +226,7 @@ func (t *Table) tick() []Event {
return events
}
-func (t *Table) resolveRound() []Event {
+func (t *genericTable) resolveRound() []Event {
var events []Event
for _, name := range t.order {
p := t.participants[name]
@@ -184,16 +247,13 @@ func (t *Table) resolveRound() []Event {
return events
}
-func (t *Table) resetRound() {
+func (t *genericTable) resetRound() {
t.phase = phaseWaiting
t.timer.stop()
clearBets(t.seats())
}
-func (t *Table) markDisconnected(name string) []Event {
- if t.blackjack != nil {
- return t.blackjack.markDisconnected(name)
- }
+func (t *genericTable) markDisconnected(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
if p := t.participants[name]; p != nil {
@@ -202,11 +262,7 @@ func (t *Table) markDisconnected(name string) []Event {
return nil
}
-func (t *Table) markConnected(name string) {
- if t.blackjack != nil {
- t.blackjack.markConnected(name)
- return
- }
+func (t *genericTable) markConnected(name string) {
t.mu.Lock()
defer t.mu.Unlock()
if p := t.participants[name]; p != nil {
@@ -214,25 +270,16 @@ func (t *Table) markConnected(name string) {
}
}
-func (t *Table) hasParticipant(name string) bool {
- if t.blackjack != nil {
- return t.blackjack.hasParticipant(name)
- }
+func (t *genericTable) hasParticipant(name string) bool {
t.mu.Lock()
defer t.mu.Unlock()
return t.participants[name] != nil
}
-func (t *Table) action(name, action string) []Event {
- if t.blackjack == nil {
- return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, PrivateTo: name, Message: "That game has no player actions."}}
- }
- return t.blackjack.action(name, action)
+func (t *genericTable) action(name, action string) []Event {
+ return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, PrivateTo: name, Message: "That game has no player actions."}}
}
-func (t *Table) rebetAmount(name string) int {
- if t.blackjack != nil {
- return t.blackjack.rebetAmount(name)
- }
- return t.Config.MinBet
+func (t *genericTable) rebet(name string) (string, int) {
+ return "", t.Config.MinBet
}