aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/casino.go143
-rw-r--r--internal/game/casino_test.go64
-rw-r--r--internal/game/cmd_registry.go12
-rw-r--r--internal/game/render_help.go2
4 files changed, 163 insertions, 58 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 801c887..9666c41 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -137,44 +137,61 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
sess.WriteLine("Use bet, bet <amount>, or bet max.")
return
}
- if len(args) == 0 {
- if table, playing := g.Casino.Participation(p.Name); playing && table != nil && table.Config.Game == casino.GameBlackjack {
- amount := g.Casino.RebetAmount(table.RoomID, table.Config.ID, p.Name)
- g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, amount, casinoWallet{g: g, p: p}))
- return
- }
- sess.WriteLine("Bet how much?")
+ table, playing := g.Casino.Participation(p.Name)
+ if !playing || table == nil {
+ sess.WriteLine("You're not playing a game.")
return
}
- if table, playing := g.Casino.Participation(p.Name); playing && table != nil && table.Config.Game == casino.GameBlackjack {
- amount := 0
- switch args[0] {
- case "min":
- amount = table.Config.MinBet
- case "max":
- amount = table.Config.MaxBet
- default:
- var err error
- amount, err = strconv.Atoi(args[0])
- if err != nil {
- sess.WriteLine("Use bet, bet <amount>, bet min, or bet max.")
+ // Table bet syntax: bet | bet <amount> [on] [spot] | bet min|max [on] [spot].
+ // Tokens that parse as an amount (or min/max) set the wager; any other
+ // token is the bet target and the table game validates it.
+ amount, spot, hasAmount := 0, "", false
+ for _, arg := range args {
+ if strings.EqualFold(arg, "on") {
+ continue
+ }
+ if strings.EqualFold(arg, "min") {
+ if hasAmount {
+ sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
return
}
+ amount, hasAmount = table.Config.MinBet, true
+ continue
}
- g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, amount, casinoWallet{g: g, p: p}))
- return
- }
- amount, err := strconv.Atoi(args[0])
- if err != nil || amount <= 0 {
- sess.WriteLine("Your bet must be a positive whole number.")
- return
+ if strings.EqualFold(arg, "max") {
+ if hasAmount {
+ sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
+ return
+ }
+ amount, hasAmount = table.Config.MaxBet, true
+ continue
+ }
+ if value, err := strconv.Atoi(arg); err == nil && value > 0 {
+ if hasAmount {
+ sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
+ return
+ }
+ amount, hasAmount = value, true
+ continue
+ }
+ if spot != "" {
+ sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
+ return
+ }
+ spot = strings.ToLower(arg)
}
- table, playing := g.Casino.Participation(p.Name)
- if !playing {
- sess.WriteLine("You're not playing a game.")
- return
+ if !hasAmount {
+ var lastSpot string
+ lastSpot, amount = g.Casino.Rebet(table.RoomID, table.Config.ID, p.Name)
+ if spot == "" {
+ spot = lastSpot
+ }
+ if amount <= 0 {
+ sess.WriteLine("Bet how much?")
+ return
+ }
}
- g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, amount, casinoWallet{g: g, p: p}))
+ g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, spot, amount, casinoWallet{g: g, p: p}))
}
func (g *Game) executeAutoBet(sess *net.Session, args []string, rawInput string) {
@@ -189,7 +206,7 @@ func (g *Game) executeAutoBet(sess *net.Session, args []string, rawInput string)
sess.WriteLine("Autobet is only available at games without player choices.")
}
-func (g *Game) executeBlackjackAction(sess *net.Session, action string) {
+func (g *Game) executeTableAction(sess *net.Session, action string) {
p := sess.Player
if p == nil {
return
@@ -229,6 +246,9 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
if event.Blackjack != nil && sess.Player != nil {
message += "\n" + renderBlackjackSnapshot(event.Blackjack, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
}
+ if event.Baccarat != nil && sess.Player != nil {
+ message += "\n" + renderBaccaratSnapshot(event.Baccarat, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
+ }
sess.WriteLine(g.colorize(sess, category, message))
}
}
@@ -252,6 +272,9 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
if event.Blackjack != nil && sess.Player != nil {
privateMessage += "\n" + renderBlackjackSnapshot(event.Blackjack, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
}
+ if event.Baccarat != nil && sess.Player != nil {
+ privateMessage += "\n" + renderBaccaratSnapshot(event.Baccarat, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
+ }
sess.WriteLine(g.renderCasinoMessage(sess, category, event.Menu, privateMessage))
if category == "casino_menu" {
g.writePrompt(sess)
@@ -293,6 +316,10 @@ func (g *Game) casinoPrivateMessage(sess *net.Session, category string, menu *ca
commands[i] = g.colorize(sess, "casino_command", action)
}
return menu.Title + "\nActions: " + strings.Join(commands, ", ")
+ case casino.MenuBaccaratBet:
+ command := func(value string) string { return g.colorize(sess, "casino_command", value) }
+ return fmt.Sprintf("%s\n\nNew hand! You can %s, %s on player|banker|tie (inc. min/max), or %s.",
+ balance, command("bet"), command("bet <amount>"), command("stop"))
case casino.MenuSlotCommands:
if message == "" {
return balance + "\n\n" + menu.Body
@@ -306,7 +333,7 @@ func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, un
if snapshot == nil {
return ""
}
- size = blackjackCardSize(unicode, size)
+ size = cardRenderSize(unicode, size)
var lines []string
dealerTitle := "Dealer:"
if snapshot.DealerRevealed {
@@ -314,22 +341,40 @@ func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, un
} else if snapshot.DealerShownScore != "" {
dealerTitle = "Dealer (" + snapshot.DealerShownScore + "):"
}
- lines = append(lines, blackjackCardLine(mode, dealerTitle))
- lines = append(lines, blackjackCardsLine(mode, snapshot.Dealer, size, unicode, style, art)...)
+ lines = append(lines, cardLine(mode, dealerTitle))
+ lines = append(lines, cardsLine(mode, snapshot.Dealer, size, unicode, style, art)...)
for _, player := range snapshot.Players {
- lines = append(lines, blackjackCardLine(mode, fmt.Sprintf("%s (%s):", player.Name, player.Score)))
- lines = append(lines, blackjackCardsLine(mode, player.Cards, size, unicode, style, art)...)
+ lines = append(lines, cardLine(mode, fmt.Sprintf("%s (%s):", player.Name, player.Score)))
+ lines = append(lines, cardsLine(mode, player.Cards, size, unicode, style, art)...)
+ }
+ return strings.Join(lines, "\n")
+}
+
+// renderBaccaratSnapshot draws the player and banker hands followed by the
+// list of wagers riding on the round.
+func renderBaccaratSnapshot(snapshot *casino.BaccaratSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string {
+ if snapshot == nil {
+ return ""
+ }
+ size = cardRenderSize(unicode, size)
+ var lines []string
+ lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):"))
+ lines = append(lines, cardsLine(mode, snapshot.Player, size, unicode, style, art)...)
+ lines = append(lines, cardLine(mode, "Banker ("+snapshot.BankerScore+"):"))
+ lines = append(lines, cardsLine(mode, snapshot.Banker, size, unicode, style, art)...)
+ for _, bet := range snapshot.Bets {
+ lines = append(lines, cardLine(mode, fmt.Sprintf("%s bets %d on %s.", bet.Name, bet.Amount, bet.Spot)))
}
return strings.Join(lines, "\n")
}
-func blackjackCardLine(mode, text string) string {
+func cardLine(mode, text string) string {
spec := color.NoColor()
spec.Fg = 39
return color.Render(mode, spec, text)
}
-func blackjackCardsLine(mode string, cards []casino.BlackjackCardView, size int, unicode bool, style string, art *cardart.Store) []string {
+func cardsLine(mode string, cards []casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string {
if len(cards) == 0 {
return nil
}
@@ -342,7 +387,7 @@ func blackjackCardsLine(mode string, cards []casino.BlackjackCardView, size int,
}
artLines := make([][]string, len(cards))
for i, card := range cards {
- artLines[i] = blackjackCardArt(card, size, unicode, style, art)
+ artLines[i] = cardArtLines(card, size, unicode, style, art)
}
lines := make([]string, size)
for row := 0; row < size; row++ {
@@ -355,7 +400,7 @@ func blackjackCardsLine(mode string, cards []casino.BlackjackCardView, size int,
return lines
}
-func blackjackCardSize(unicode bool, size int) int {
+func cardRenderSize(unicode bool, size int) int {
if !unicode {
return 5
}
@@ -365,16 +410,16 @@ func blackjackCardSize(unicode bool, size int) int {
return 7
}
-var blackjackSuitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣"}
+var suitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣"}
-func blackjackSymbol(card casino.BlackjackCardView) string {
+func cardSymbol(card casino.CardView) string {
if card.Hidden {
return "?"
}
- return card.Rank + blackjackSuitSymbols[card.Suit]
+ return card.Rank + suitSymbols[card.Suit]
}
-func blackjackCardArt(card casino.BlackjackCardView, size int, unicode bool, style string, art *cardart.Store) []string {
+func cardArtLines(card casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string {
if card.Hidden {
return generatedCardArt("?", "", size, unicode, style)
}
@@ -442,10 +487,10 @@ func suitLetter(suit string, unicode bool) string {
if !unicode {
return strings.ToUpper(suit[:1])
}
- return blackjackSuitSymbols[suit]
+ return suitSymbols[suit]
}
-func colorizeCardLine(mode, line string, card casino.BlackjackCardView, row, size int, style string) string {
+func colorizeCardLine(mode, line string, card casino.CardView, row, size int, style string) string {
red := card.Suit == "hearts" || card.Suit == "diamonds"
rankColor := 27
if red {
@@ -487,7 +532,7 @@ func colorizeCardLine(mode, line string, card casino.BlackjackCardView, row, siz
return builder.String()
}
-func colorizeCardSymbol(mode string, card casino.BlackjackCardView) string {
+func colorizeCardSymbol(mode string, card casino.CardView) string {
if card.Hidden {
return color.Render(mode, color.ColorSpec{Fg: 15}, "?")
}
@@ -495,7 +540,7 @@ func colorizeCardSymbol(mode string, card casino.BlackjackCardView) string {
if card.Suit == "hearts" || card.Suit == "diamonds" {
rankColor, suitColor = 196, 196
}
- runes := []rune(blackjackSymbol(card))
+ runes := []rune(cardSymbol(card))
rankLength := len([]rune(card.Rank))
return color.Render(mode, color.ColorSpec{Fg: rankColor}, string(runes[:rankLength])) +
color.Render(mode, color.ColorSpec{Fg: suitColor}, string(runes[rankLength:]))
diff --git a/internal/game/casino_test.go b/internal/game/casino_test.go
index b74baab..d7b5ae9 100644
--- a/internal/game/casino_test.go
+++ b/internal/game/casino_test.go
@@ -45,6 +45,66 @@ func TestExecuteBetRejectsInvalidMachineArgs(t *testing.T) {
}
}
+func TestExecuteBetParsesBaccaratSpots(t *testing.T) {
+ g := &Game{Casino: casino.NewManager(1)}
+ cfg := casino.TableConfig{ID: "baccarat", Game: casino.GameBaccarat}
+ g.Casino.Join(1, cfg, "alice")
+ conn := &casinoTestConn{}
+ sess := &net.Session{Player: player.New("alice"), Conn: conn}
+ g.loggedInChars = map[string]*net.Session{"alice": sess}
+ // Valid spot syntaxes reach the table and fail only at the (empty) wallet.
+ for _, args := range [][]string{{"tie", "100"}, {"100", "tie"}, {"100", "on", "banker"}, {"banker", "100"}} {
+ conn.data = nil
+ g.executeBet(sess, args, "bet "+strings.Join(args, " "))
+ if !strings.Contains(string(conn.data), "You don't have enough chips and credits") {
+ t.Fatalf("bet %v: expected wallet failure, got %q", args, string(conn.data))
+ }
+ }
+ // Invalid spots are rejected by the table.
+ conn.data = nil
+ g.executeBet(sess, []string{"red", "100"}, "bet red 100")
+ if !strings.Contains(string(conn.data), "must be on player, banker, or tie") {
+ t.Fatalf("bet red 100: expected spot rejection, got %q", string(conn.data))
+ }
+ // Two amounts or two spots are usage errors.
+ for _, args := range [][]string{{"10", "20"}, {"player", "banker"}} {
+ conn.data = nil
+ g.executeBet(sess, args, "bet "+strings.Join(args, " "))
+ if !strings.Contains(string(conn.data), "Use bet,") {
+ t.Fatalf("bet %v: expected usage error, got %q", args, string(conn.data))
+ }
+ }
+ // A bare first bet asks for a target (via the rebet path).
+ conn.data = nil
+ g.executeBet(sess, nil, "bet")
+ if !strings.Contains(string(conn.data), "Bet on what?") {
+ t.Fatalf("bare bet: expected target guidance, got %q", string(conn.data))
+ }
+}
+
+func TestRenderBaccaratSnapshot(t *testing.T) {
+ snapshot := &casino.BaccaratSnapshot{
+ Player: []casino.CardView{{Rank: "5", Suit: "clubs"}, {Rank: "4", Suit: "hearts"}},
+ Banker: []casino.CardView{{Rank: "2", Suit: "spades"}, {Rank: "3", Suit: "diamonds"}},
+ PlayerScore: "9",
+ BankerScore: "5",
+ Bets: []casino.BaccaratBetView{{Name: "alice", Spot: "player", Amount: 100}},
+ }
+ plain := renderBaccaratSnapshot(snapshot, "none", false, 5, "dark", nil)
+ if !strings.Contains(plain, "Player (9):") || !strings.Contains(plain, "Banker (5):") || !strings.Contains(plain, "alice bets 100 on player.") {
+ t.Fatalf("unexpected baccarat render: %q", plain)
+ }
+ output := renderBaccaratSnapshot(snapshot, "xterm256", true, 7, "dark", nil)
+ if !strings.Contains(output, "\033[38;5;") {
+ t.Fatalf("expected colored baccarat art: %q", output)
+ }
+ for _, line := range strings.Split(output, "\n") {
+ if color.VisibleLen(line) > 80 {
+ t.Fatalf("baccarat line exceeds console width: %d", color.VisibleLen(line))
+ }
+ }
+}
+
func TestRenderSlotSnapshotColorsWordSymbols(t *testing.T) {
snapshot := &casino.SlotSnapshot{StoppedReels: 1}
snapshot.Grid[0][0] = "scatter"
@@ -67,8 +127,8 @@ func TestRenderSlotSnapshotColorsWordSymbols(t *testing.T) {
func TestRenderBlackjackSnapshotUsesCompactASCIIArt(t *testing.T) {
snapshot := &casino.BlackjackSnapshot{
- Dealer: []casino.BlackjackCardView{{Rank: "?", Hidden: true}, {Rank: "K", Suit: "hearts"}},
- Players: []casino.BlackjackPlayerView{{Name: "alice", Score: "20", Cards: []casino.BlackjackCardView{{Rank: "5", Suit: "clubs"}, {Rank: "Q", Suit: "spades"}}}},
+ Dealer: []casino.CardView{{Rank: "?", Hidden: true}, {Rank: "K", Suit: "hearts"}},
+ Players: []casino.BlackjackPlayerView{{Name: "alice", Score: "20", Cards: []casino.CardView{{Rank: "5", Suit: "clubs"}, {Rank: "Q", Suit: "spades"}}}},
}
plain := renderBlackjackSnapshot(snapshot, "none", false, 5, "dark", nil)
if !strings.Contains(plain, "+---+") || !strings.Contains(plain, "Dealer:") || !strings.Contains(plain, "alice (20):") {
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index db6cd96..0fee89e 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -28,12 +28,12 @@ var commandRegistry = map[string]commandDef{
"spin": {(*Game).executeBet, ClassActive},
"autobet": {(*Game).executeAutoBet, ClassActive},
"autospin": {(*Game).executeAutoBet, ClassActive},
- "hit": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "hit") }, ClassActive},
- "stand": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "stand") }, ClassActive},
- "double": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "double") }, ClassActive},
- "split": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "split") }, ClassActive},
- "insurance": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "insurance") }, ClassActive},
- "surrender": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeBlackjackAction(s, "surrender") }, ClassActive},
+ "hit": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "hit") }, ClassActive},
+ "stand": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "stand") }, ClassActive},
+ "double": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "double") }, ClassActive},
+ "split": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "split") }, ClassActive},
+ "insurance": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "insurance") }, ClassActive},
+ "surrender": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "surrender") }, ClassActive},
"style": {(*Game).executeStyle, ClassInstant},
"look": {(*Game).executeLook, ClassInstant},
"l": {(*Game).executeLook, ClassInstant},
diff --git a/internal/game/render_help.go b/internal/game/render_help.go
index afd5b86..4a283d1 100644
--- a/internal/game/render_help.go
+++ b/internal/game/render_help.go
@@ -54,7 +54,7 @@ var commandList = []cmdEntry{
{"look / l", "Instant", "Look around or examine things"},
{"map", "Instant", "Display an ASCII map of the area"},
{"play", "Active", "Join a casino game in the current room"},
- {"bet", "Active", "Place a casino wager"},
+ {"bet", "Active", "Place a casino wager (e.g. bet 100 on banker)"},
{"autobet / autospin", "Active", "Automatically repeat a casino wager"},
{"spin", "Active", "Spin the current casino wager"},
{"hit / stand / double / split", "Active", "Play a blackjack hand"},