aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go360
1 files changed, 307 insertions, 53 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index c6fb63b..801c887 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -4,7 +4,9 @@ import (
"fmt"
"strconv"
"strings"
+ "unicode/utf8"
+ "thehouseoficarus/internal/cardart"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
@@ -17,6 +19,9 @@ type casinoWallet struct {
}
func (w casinoWallet) Wager(amount int) (casino.Wager, bool) {
+ if amount <= 0 {
+ return casino.Wager{}, false
+ }
chips := countChips(w.p)
if chips > amount {
chips = amount
@@ -119,23 +124,46 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
return
}
- if len(args) == 1 && args[0] == "max" {
- g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, machine.Config.MaxBet))
- return
- }
if len(args) == 1 {
+ if args[0] == "max" {
+ g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, machine.Config.MaxBet))
+ return
+ }
if amount, err := strconv.Atoi(args[0]); err == nil && amount > 0 {
g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, amount))
return
}
}
- g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ 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?")
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.")
+ return
+ }
+ }
+ 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.")
@@ -161,6 +189,19 @@ 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) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ table, playing := g.Casino.Participation(p.Name)
+ if !playing || table == nil {
+ sess.WriteLine("You're not playing a multiplayer casino game.")
+ return
+ }
+ g.emitCasinoEvents(g.Casino.Action(table.RoomID, table.Config.ID, p.Name, action))
+}
+
func (g *Game) CasinoTick() {
if g.Casino != nil {
g.emitCasinoEvents(g.Casino.Tick())
@@ -169,7 +210,7 @@ func (g *Game) CasinoTick() {
func (g *Game) emitCasinoEvents(events []casino.Event) {
for _, event := range events {
- if event.Message == "" {
+ if event.Message == "" && event.Menu == nil {
continue
}
category := event.Color
@@ -182,48 +223,51 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
continue
}
message := event.Message
- if category == "casino_menu" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
- message = g.casinoMenuBalance(sess, message)
- }
if event.Slot != nil && sess.Player != nil {
message += "\n" + renderSlotSnapshot(event.Slot, g.colorMode(sess), sess.Player.OptionBool("unicode"))
}
+ 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)
+ }
sess.WriteLine(g.colorize(sess, category, message))
}
}
- if event.PrivateMessage != "" && event.PrivateTo != "" {
- g.charsMu.Lock()
- sess := g.loggedInChars[event.PrivateTo]
- g.charsMu.Unlock()
- if sess != nil {
- privateMessage := event.PrivateMessage
- if category == "casino_menu" {
- privateMessage = g.casinoMenuBalance(sess, privateMessage)
- }
- sess.WriteLine(g.colorize(sess, category, privateMessage))
- if category == "casino_menu" {
- g.writePrompt(sess)
- }
- }
+ if event.PrivateTo == "" {
+ continue
}
- if event.PrivateTo != "" {
- g.charsMu.Lock()
- sess := g.loggedInChars[event.PrivateTo]
- g.charsMu.Unlock()
- if sess != nil && event.PrivateMessage == "" {
- privateMessage := event.Message
- if category == "casino_menu" {
- privateMessage = g.casinoMenuBalance(sess, privateMessage)
- }
- sess.WriteLine(g.colorize(sess, category, privateMessage))
- if category == "casino_menu" {
- g.writePrompt(sess)
- }
- }
+ text := event.PrivateMessage
+ if text == "" {
+ text = event.Message
+ }
+ if text == "" && event.Menu == nil {
+ continue
+ }
+ g.charsMu.Lock()
+ sess := g.loggedInChars[event.PrivateTo]
+ g.charsMu.Unlock()
+ if sess == nil {
+ continue
+ }
+ privateMessage := g.casinoPrivateMessage(sess, category, event.Menu, text)
+ 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)
+ }
+ sess.WriteLine(g.renderCasinoMessage(sess, category, event.Menu, privateMessage))
+ if category == "casino_menu" {
+ g.writePrompt(sess)
}
}
}
+// renderCasinoMessage colorizes a casino message unless it is an
+// already-composed structured menu.
+func (g *Game) renderCasinoMessage(sess *net.Session, category string, menu *casino.MenuView, message string) string {
+ if category == "casino_menu" && menu != nil && menu.Kind != casino.MenuSlotCommands {
+ return message
+ }
+ return g.colorize(sess, category, message)
+}
+
func casinoEventColor(eventType casino.EventType) string {
if eventType == casino.EventPayout {
return "casino_win"
@@ -231,14 +275,230 @@ func casinoEventColor(eventType casino.EventType) string {
return "casino_action"
}
-func (g *Game) casinoMenuBalance(sess *net.Session, message string) string {
- if sess == nil || sess.Player == nil {
+func (g *Game) casinoPrivateMessage(sess *net.Session, category string, menu *casino.MenuView, message string) string {
+ if category != "casino_menu" || menu == nil {
return message
}
chips := g.colorize(sess, "casino_chips", fmt.Sprintf("%d", countChips(sess.Player)))
credits := g.colorize(sess, "casino_credits", fmt.Sprintf("%d", sess.Player.Credits))
- line := fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
- return strings.Replace(message, "Slot machine commands:", line+"\n\nSlot machine commands:", 1)
+ balance := fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
+ switch menu.Kind {
+ case casino.MenuBlackjackBet:
+ command := func(value string) string { return g.colorize(sess, "casino_command", value) }
+ return fmt.Sprintf("%s\n\nNew hand! You can %s, %s (inc. min/max), or %s.",
+ balance, command("bet"), command("bet <amount>"), command("stop"))
+ case casino.MenuBlackjackTurn:
+ commands := make([]string, len(menu.Actions))
+ for i, action := range menu.Actions {
+ commands[i] = g.colorize(sess, "casino_command", action)
+ }
+ return menu.Title + "\nActions: " + strings.Join(commands, ", ")
+ case casino.MenuSlotCommands:
+ if message == "" {
+ return balance + "\n\n" + menu.Body
+ }
+ return message + "\n\n" + balance + "\n\n" + menu.Body
+ }
+ return message
+}
+
+func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string {
+ if snapshot == nil {
+ return ""
+ }
+ size = blackjackCardSize(unicode, size)
+ var lines []string
+ dealerTitle := "Dealer:"
+ if snapshot.DealerRevealed {
+ dealerTitle = "Dealer (" + snapshot.DealerScore + "):"
+ } 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)...)
+ 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)...)
+ }
+ return strings.Join(lines, "\n")
+}
+
+func blackjackCardLine(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 {
+ if len(cards) == 0 {
+ return nil
+ }
+ if size == 1 {
+ parts := make([]string, len(cards))
+ for i, card := range cards {
+ parts[i] = colorizeCardSymbol(mode, card)
+ }
+ return []string{strings.Join(parts, " ")}
+ }
+ artLines := make([][]string, len(cards))
+ for i, card := range cards {
+ artLines[i] = blackjackCardArt(card, size, unicode, style, art)
+ }
+ lines := make([]string, size)
+ for row := 0; row < size; row++ {
+ var lineParts []string
+ for i, card := range artLines {
+ lineParts = append(lineParts, colorizeCardLine(mode, card[row], cards[i], row, size, style))
+ }
+ lines[row] = strings.Join(lineParts, " ")
+ }
+ return lines
+}
+
+func blackjackCardSize(unicode bool, size int) int {
+ if !unicode {
+ return 5
+ }
+ if size == 1 || size == 5 || size == 7 || size == 9 {
+ return size
+ }
+ return 7
+}
+
+var blackjackSuitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣"}
+
+func blackjackSymbol(card casino.BlackjackCardView) string {
+ if card.Hidden {
+ return "?"
+ }
+ return card.Rank + blackjackSuitSymbols[card.Suit]
+}
+
+func blackjackCardArt(card casino.BlackjackCardView, size int, unicode bool, style string, art *cardart.Store) []string {
+ if card.Hidden {
+ return generatedCardArt("?", "", size, unicode, style)
+ }
+ key := card.Rank + strings.ToUpper(card.Suit[:1])
+ if configured := art.Art(key, size); len(configured) == size {
+ return configured
+ }
+ return generatedCardArt(card.Rank, card.Suit, size, unicode, style)
+}
+
+func generatedCardArt(rank, suit string, size int, unicode bool, style string) []string {
+ if size < 5 {
+ size = 5
+ }
+ border := "-"
+ left, right := "+", "+"
+ if unicode {
+ border, left, right = "─", "┌", "┐"
+ }
+ bottomLeft, bottomRight := left, right
+ if unicode {
+ bottomLeft, bottomRight = "└", "┘"
+ }
+ vertical := "|"
+ if unicode {
+ vertical = "│"
+ }
+ if unicode && style == "light" {
+ lines := []string{strings.Repeat("▀", size)}
+ lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...)
+ return append(lines, strings.Repeat("▄", size))
+ }
+ lines := []string{left + strings.Repeat(border, size-2) + right}
+ lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...)
+ lines = append(lines, bottomLeft+strings.Repeat(border, size-2)+bottomRight)
+ return lines
+}
+
+func generatedCardBody(rank, suit string, size int, unicode bool, vertical string) []string {
+ inner := size - 2
+ lines := make([]string, 0, size-2)
+ for row := 1; row < size-1; row++ {
+ content := strings.Repeat(" ", inner)
+ if row == 1 {
+ content = rank + strings.Repeat(" ", inner-len(rank))
+ }
+ if row == (size-1)/2 {
+ suitText := ""
+ if suit != "" {
+ suitText = suitLetter(suit, unicode)
+ }
+ suitWidth := utf8.RuneCountInString(suitText)
+ leftPadding := (inner - suitWidth) / 2
+ content = strings.Repeat(" ", leftPadding) + suitText + strings.Repeat(" ", inner-leftPadding-suitWidth)
+ }
+ if row == size-2 {
+ content = strings.Repeat(" ", inner-len(rank)) + rank
+ }
+ lines = append(lines, vertical+content+vertical)
+ }
+ return lines
+}
+
+func suitLetter(suit string, unicode bool) string {
+ if !unicode {
+ return strings.ToUpper(suit[:1])
+ }
+ return blackjackSuitSymbols[suit]
+}
+
+func colorizeCardLine(mode, line string, card casino.BlackjackCardView, row, size int, style string) string {
+ red := card.Suit == "hearts" || card.Suit == "diamonds"
+ rankColor := 27
+ if red {
+ rankColor = 196
+ }
+ suitColor := 27
+ if red {
+ suitColor = 196
+ }
+ rankLine := row == 1 || row == size-2
+ light := style == "light"
+ bodyBackground := light && row > 0 && row < size-1
+ var builder strings.Builder
+ for _, r := range line {
+ if strings.ContainsRune("┌─┐└┘│┬┴├┤┼╔╗╚╝═║╦╩╠╣╬▀▄+|-", r) {
+ spec := color.ColorSpec{Fg: 15}
+ if bodyBackground {
+ spec.Bg = 15
+ }
+ builder.WriteString(color.Render(mode, spec, string(r)))
+ } else if r == ' ' {
+ if bodyBackground {
+ builder.WriteString(color.Render(mode, color.ColorSpec{Fg: 15, Bg: 15}, " "))
+ } else {
+ builder.WriteRune(r)
+ }
+ } else {
+ value := suitColor
+ if rankLine {
+ value = rankColor
+ }
+ spec := color.ColorSpec{Fg: value}
+ if bodyBackground {
+ spec.Bg = 15
+ }
+ builder.WriteString(color.Render(mode, spec, string(r)))
+ }
+ }
+ return builder.String()
+}
+
+func colorizeCardSymbol(mode string, card casino.BlackjackCardView) string {
+ if card.Hidden {
+ return color.Render(mode, color.ColorSpec{Fg: 15}, "?")
+ }
+ rankColor, suitColor := 27, 27
+ if card.Suit == "hearts" || card.Suit == "diamonds" {
+ rankColor, suitColor = 196, 196
+ }
+ runes := []rune(blackjackSymbol(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:]))
}
func renderSlotSnapshot(snapshot *casino.SlotSnapshot, mode string, unicode bool) string {
@@ -295,23 +555,17 @@ func colorSlotSymbol(mode, symbol string) string {
spec.Fg = 250
switch symbol {
case "straw":
- spec = color.NoColor()
- spec.Gradient = []int{220, 226}
+ spec.Fg = 220
case "stick":
- spec = color.NoColor()
- spec.Gradient = []int{34, 46}
+ spec.Fg = 46
case "brick":
- spec = color.NoColor()
- spec.Gradient = []int{160, 196}
+ spec.Fg = 196
case "hat":
- spec = color.NoColor()
- spec.Gradient = []int{129, 201}
+ spec.Fg = 201
case "wolf":
- spec = color.NoColor()
- spec.Gradient = []int{27, 39}
+ spec.Fg = 39
case "scatter":
- spec = color.NoColor()
- spec.Gradient = []int{196, 220, 46, 51, 129, 201}
+ spec.Fg = 226
default:
spec.Dim = true
}