diff options
Diffstat (limited to 'internal/game/casino.go')
| -rw-r--r-- | internal/game/casino.go | 360 |
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 } |
