package game import ( "fmt" "strconv" "strings" "unicode/utf8" "thehouseoficarus/internal/cardart" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) type casinoWallet struct { g *Game p *player.Player } 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 } credits := amount - chips if w.p.Credits < credits { return casino.Wager{}, false } if chips > 0 { w.p.RemoveItem("chips", chips) } w.p.Credits -= credits w.g.AccountStore.SaveCharacter(w.p) return casino.Wager{Total: amount, Chips: chips, Credits: credits}, true } func (w casinoWallet) PayCredits(amount int) { w.p.Credits += amount w.g.AccountStore.SaveCharacter(w.p) } // casinoAffordable is the most a player can wager: chips are spent first, // then credits. func casinoAffordable(p *player.Player) int { return p.Credits + countChips(p) } func (g *Game) casinoTable(roomID int, gameName string) (*casino.Table, bool) { room, err := g.World.LoadRoom(roomID) if err != nil { return nil, false } for _, cfg := range room.CasinoTables { if strings.EqualFold(string(cfg.Game), gameName) { table := g.Casino.EnsureTable(roomID, cfg) if table == nil { return nil, false } return table, true } } return nil, false } func (g *Game) casinoMachine(roomID int, gameName string) (*casino.Machine, bool) { room, err := g.World.LoadRoom(roomID) if err != nil { return nil, false } for _, cfg := range room.CasinoMachines { if strings.EqualFold(string(cfg.Game), gameName) || strings.EqualFold(cfg.Variant, gameName) { return g.Casino.EnsureMachine(roomID, cfg), true } } // Migrate older room definitions that declared slots under casino_tables; // slots are still always instantiated as one-player machines. if strings.EqualFold(gameName, "slots") { for _, cfg := range room.CasinoTables { if strings.EqualFold(string(cfg.Game), "slots") { return g.Casino.EnsureMachine(roomID, casino.MachineConfig{ ID: cfg.ID, Game: casino.GameSlots, MinBet: cfg.MinBet, MaxBet: cfg.MaxBet, }), true } } } return nil, false } func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) { p := sess.Player if p == nil || len(args) == 0 { sess.WriteLine("Play what?") return } if _, playing := g.Casino.Participation(p.Name); playing { sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.") return } g.startCasinoGame(sess, p, args[0]) } func (g *Game) startCasinoGame(sess *net.Session, p *player.Player, gameName string) { if p == nil || sess == nil { return } if _, playing := g.Casino.Participation(p.Name); playing { sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.") return } if machine, ok := g.casinoMachine(p.RoomID, gameName); ok { g.emitCasinoEvents(g.Casino.JoinMachine(machine.RoomID, machine.Config, p.Name)) return } table, ok := g.casinoTable(p.RoomID, gameName) if !ok { sess.WriteLine(fmt.Sprintf("There is no %s table here.", gameName)) return } g.emitCasinoEvents(g.Casino.Join(table.RoomID, table.Config, p.Name)) } func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) { p := sess.Player if p == nil { sess.WriteLine("Bet how much?") return } if machine, ok := g.Casino.MachineFor(p.Name); ok { if len(args) == 0 { g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p})) return } if len(args) == 1 { if args[0] == "max" { amount := min(machine.Config.MaxBet, casinoAffordable(p)) if amount < 1 { sess.WriteLine("You don't have any chips or credits to bet.") return } g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, amount, casinoWallet{g: g, p: p})) 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, casinoWallet{g: g, p: p})) return } } sess.WriteLine("Use bet, bet , or bet max.") return } table, playing := g.Casino.Participation(p.Name) if !playing || table == nil { sess.WriteLine("You're not playing a game.") return } // Table bet syntax: bet | bet [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 [on ], bet min, or bet max.") return } amount, hasAmount = table.Config.MinBet, true continue } if strings.EqualFold(arg, "max") { if hasAmount { sess.WriteLine("Use bet, bet [on ], bet min, or bet max.") return } amount = min(table.Config.MaxBet, casinoAffordable(p)) if amount < 1 { sess.WriteLine("You don't have any chips or credits to bet.") return } hasAmount = true continue } if value, err := strconv.Atoi(arg); err == nil && value > 0 { if hasAmount { sess.WriteLine("Use bet, bet [on ], bet min, or bet max.") return } amount, hasAmount = value, true continue } if spot != "" { sess.WriteLine("Use bet, bet [on ], bet min, or bet max.") return } spot = strings.ToLower(arg) } 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, spot, amount, casinoWallet{g: g, p: p})) } func (g *Game) executeAutoBet(sess *net.Session, args []string, rawInput string) { p := sess.Player if p == nil { return } if machine, ok := g.Casino.MachineFor(p.Name); ok { g.emitCasinoEvents(g.Casino.EnableMachineAutoBet(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p})) return } sess.WriteLine("Autobet is only available at games without player choices.") } func (g *Game) executeTableAction(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()) } } func (g *Game) emitCasinoEvents(events []casino.Event) { for _, event := range events { if event.Message == "" && event.Menu == nil && event.Blackjack == nil && event.Baccarat == nil && event.Slot == nil { continue } category := event.Color if category == "" { category = casinoEventColor(event.Type) } if event.Public && g.Hub != nil { for _, sess := range g.Hub.PlayersInRoom(event.RoomID) { if event.PrivateTo != "" && sess.Player != nil && sess.Player.Name == event.PrivateTo { continue } message := event.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) } 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)) } } if event.PrivateTo == "" { continue } 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) } 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) } } } // 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 { return message } return g.colorize(sess, category, message) } func casinoEventColor(eventType casino.EventType) string { if eventType == casino.EventPayout { return "casino_win" } return "casino_action" } 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)) balance := fmt.Sprintf("You have %s chips and %s credits.", chips, credits) switch menu.Kind { case casino.MenuBlackjackBet, casino.MenuBaccaratBet: return g.renderCasinoCommands(sess, balance, message, menu.Commands) case casino.MenuBlackjackTurn: return g.renderCasinoCommands(sess, "", menu.Title, menu.Commands) case casino.MenuSlotCommands: return g.renderCasinoCommands(sess, balance, message, menu.Commands) } return message } // renderCasinoCommands lays every casino menu out in the same style: an // optional balance line, a heading, and an aligned command list with each // command in the "command" color and its description in plain text. func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string, commands []casino.MenuCommand) string { var sb strings.Builder if balance != "" { sb.WriteString(balance) sb.WriteString("\n\n") } if heading != "" { sb.WriteString(heading) sb.WriteString("\n\n") } width := 0 for _, c := range commands { if n := utf8.RuneCountInString(c.Command); n > width { width = n } } for _, c := range commands { padded := c.Command + strings.Repeat(" ", width-utf8.RuneCountInString(c.Command)) cmd := g.colorize(sess, "command", padded) if c.Desc != "" { fmt.Fprintf(&sb, " %s %s\n", cmd, c.Desc) } else { fmt.Fprintf(&sb, " %s\n", cmd) } } return strings.TrimRight(sb.String(), "\n") } func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string { if snapshot == nil { return "" } size = cardRenderSize(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, cardLine(mode, dealerTitle)) lines = append(lines, cardsLine(mode, snapshot.Dealer, size, unicode, style, art)...) for _, player := range snapshot.Players { 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 cardLine(mode, text string) string { spec := color.NoColor() spec.Fg = 39 return color.Render(mode, spec, text) } func cardsLine(mode string, cards []casino.CardView, 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] = cardArtLines(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 cardRenderSize(unicode bool, size int) int { if !unicode { return 5 } if size == 1 || size == 5 || size == 7 || size == 9 { return size } return 7 } var suitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣"} func cardSymbol(card casino.CardView) string { if card.Hidden { return "?" } return card.Rank + suitSymbols[card.Suit] } func cardArtLines(card casino.CardView, 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 suitSymbols[suit] } 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 { 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.CardView) 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(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:])) } func renderSlotSnapshot(snapshot *casino.SlotSnapshot, mode string, unicode bool) string { if snapshot == nil { return "" } left, junction, right, horizontal, vertical := '+', '+', '+', '-', '|' bottomLeft, bottomJunction, bottomRight := '+', '+', '+' if unicode { left, junction, right, horizontal, vertical = '\u250c', '\u252c', '\u2510', '\u2500', '\u2502' bottomLeft, bottomJunction, bottomRight = '\u2514', '\u2534', '\u2518' } separator := string(left) for reel := 0; reel < 5; reel++ { if reel > 0 { separator += string(junction) } separator += strings.Repeat(string(horizontal), 9) } separator += string(right) lines := []string{casinoFrame(mode, separator)} frameVertical := casinoFrame(mode, string(vertical)) for row := 0; row < 3; row++ { line := frameVertical for reel := 0; reel < 5; reel++ { value := "..." if reel < snapshot.StoppedReels { value = snapshot.Grid[row][reel] } line += " " + colorSlotSymbol(mode, value) + strings.Repeat(" ", 7-len(value)) + " " + frameVertical } lines = append(lines, line) } bottom := string(bottomLeft) for reel := 0; reel < 5; reel++ { if reel > 0 { bottom += string(bottomJunction) } bottom += strings.Repeat(string(horizontal), 9) } bottom += string(bottomRight) lines = append(lines, casinoFrame(mode, bottom)) return strings.Join(lines, "\n") } func casinoFrame(mode, text string) string { spec := color.NoColor() spec.Fg = 244 return color.Render(mode, spec, text) } func colorSlotSymbol(mode, symbol string) string { spec := color.NoColor() spec.Fg = 250 switch symbol { case "straw": spec.Fg = 220 case "stick": spec.Fg = 46 case "brick": spec.Fg = 196 case "hat": spec.Fg = 201 case "wolf": spec.Fg = 39 case "scatter": spec.Fg = 226 default: spec.Dim = true } return color.Render(mode, spec, symbol) }