package plugin import ( "fmt" "strings" "github.com/chehsunliu/poker" ) // cardGlyphMap maps poker card strings (e.g. "As") to Unicode glyphs. var cardGlyphMap map[string]string func init() { cardGlyphMap = make(map[string]string, 52) // Unicode playing card block. Q and K skip a codepoint (Knight sits between J and Q). // Spades: U+1F0A1.. // Hearts: U+1F0B1.. // Diamonds: U+1F0C1.. // Clubs: U+1F0D1.. type suitInfo struct { letter string base rune } suits := []suitInfo{ {"s", 0x1F0A0}, // Spades {"h", 0x1F0B0}, // Hearts {"d", 0x1F0C0}, // Diamonds {"c", 0x1F0D0}, // Clubs } // Rank offsets within each suit block. // A=1, 2=2, ..., 10=10, J=11, Q=13(skip Knight at 12), K=14 rankOffsets := map[string]int{ "A": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "T": 10, "J": 11, "Q": 13, "K": 14, } for _, s := range suits { for rank, offset := range rankOffsets { cardStr := rank + s.letter glyph := string(rune(s.base + rune(offset))) cardGlyphMap[cardStr] = glyph } } } // holdemSuitSymbols maps poker library suit letters to display symbols. var holdemSuitSymbols = map[byte]string{ 's': "โ™ ", 'h': "โ™ฅ", 'd': "โ™ฆ", 'c': "โ™ฃ", } // rankDisplay converts library rank chars to display. "T" -> "10", rest unchanged. var rankDisplay = map[byte]string{ '2': "2", '3': "3", '4': "4", '5': "5", '6': "6", '7': "7", '8': "8", '9': "9", 'T': "10", 'J': "J", 'Q': "Q", 'K': "K", 'A': "A", } // renderCard renders a card as "๐Ÿ‚ก (Aโ™ )". func renderCard(c poker.Card) string { s := c.String() // e.g. "As", "Td" glyph := cardGlyphMap[s] if glyph == "" { glyph = "๐Ÿ‚ " } rank := rankDisplay[s[0]] suit := holdemSuitSymbols[s[1]] return fmt.Sprintf("%s (%s%s)", glyph, rank, suit) } // renderCards renders multiple cards separated by double space. func renderCards(cards []poker.Card) string { parts := make([]string, len(cards)) for i, c := range cards { parts[i] = renderCard(c) } return strings.Join(parts, " ") } // renderHoleCards renders a 2-card hole hand. func renderHoleCards(hole [2]poker.Card) string { return renderCard(hole[0]) + " " + renderCard(hole[1]) } // renderTableView builds the DM table view for a specific player. func renderTableView(g *HoldemGame, viewerIdx int) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("๐ŸŽฐ **Texas Hold'em** | %s\n", g.Street.String())) // Pot info. if len(g.SidePots) > 0 { total := int64(0) for _, sp := range g.SidePots { total += sp.Amount } sb.WriteString(fmt.Sprintf("Pot: โ‚ฌ%d", total)) for i, sp := range g.SidePots { sb.WriteString(fmt.Sprintf(" | Side %d: โ‚ฌ%d", i+1, sp.Amount)) } sb.WriteString("\n") } else { // Include outstanding bets in displayed pot. totalPot := g.Pot for _, p := range g.Players { totalPot += p.Bet } sb.WriteString(fmt.Sprintf("Pot: โ‚ฌ%d\n", totalPot)) } // Board. if len(g.Community) > 0 { sb.WriteString(fmt.Sprintf("Board: %s\n", renderCards(g.Community))) } else { sb.WriteString("Board: โ€”\n") } sb.WriteString("\n**Seats:**\n") for i, p := range g.Players { if p.State == PlayerSatOut { sb.WriteString(fmt.Sprintf(" %s โ€” *(sitting out)*\n", p.DisplayName)) continue } marker := "" if i == g.DealerIdx { marker = " ๐Ÿ”˜" } actionMarker := "" if i == g.ActionIdx && g.HandInProgress && g.Street != StreetShowdown { actionMarker = " โ† action" } switch p.State { case PlayerFolded: sb.WriteString(fmt.Sprintf(" %s%s โ€” โ‚ฌ%d [folded]%s\n", p.DisplayName, marker, p.Stack, actionMarker)) case PlayerAllIn: sb.WriteString(fmt.Sprintf(" %s%s โ€” โ‚ฌ%d [ALL IN]%s\n", p.DisplayName, marker, p.Stack, actionMarker)) default: betStr := "" if p.Bet > 0 { betStr = fmt.Sprintf(" | bet: โ‚ฌ%d", p.Bet) } sb.WriteString(fmt.Sprintf(" %s%s โ€” โ‚ฌ%d%s%s\n", p.DisplayName, marker, p.Stack, betStr, actionMarker)) } } // If viewer is the action player, show action prompt. if viewerIdx == g.ActionIdx && g.HandInProgress && g.Street != StreetShowdown { p := g.Players[viewerIdx] if p.State == PlayerActive { sb.WriteString("\n") toCall := g.CurrentBet - p.Bet if toCall > p.Stack { toCall = p.Stack } if toCall > 0 { minRaiseTo := g.CurrentBet + g.MinRaise sb.WriteString(fmt.Sprintf("To call: โ‚ฌ%d | Min raise to: โ‚ฌ%d\n", toCall, minRaiseTo)) sb.WriteString("`!holdem call` `!holdem raise ` `!holdem allin` `!holdem fold`\n") } else { minRaiseTo := g.CurrentBet + g.MinRaise if g.CurrentBet == 0 { minRaiseTo = g.BigBlind } sb.WriteString(fmt.Sprintf("Check available | Min bet: โ‚ฌ%d\n", minRaiseTo)) sb.WriteString("`!holdem check` `!holdem raise ` `!holdem allin` `!holdem fold`\n") } } } return sb.String() } // renderPrivateHand builds the private hand DM for a player. func renderPrivateHand(g *HoldemGame, playerIdx int) string { p := g.Players[playerIdx] var sb strings.Builder sb.WriteString(fmt.Sprintf("๐Ÿƒ Your hand: %s\n", renderHoleCards(p.Hole))) if len(g.Community) > 0 { sb.WriteString(fmt.Sprintf("Board: %s\n", renderCards(g.Community))) } totalPot := g.Pot for _, pp := range g.Players { totalPot += pp.Bet } toCall := g.CurrentBet - p.Bet if toCall < 0 { toCall = 0 } if toCall > p.Stack { toCall = p.Stack } sb.WriteString(fmt.Sprintf("Stack: โ‚ฌ%d | Pot: โ‚ฌ%d | To call: โ‚ฌ%d\n", p.Stack, totalPot, toCall)) return sb.String() } // renderActionAnnouncement formats an action for room/DM announcement. func renderActionAnnouncement(name, action string, amount int64) string { switch action { case "fold": return fmt.Sprintf("**%s** folds.", name) case "check": return fmt.Sprintf("**%s** checks.", name) case "call": return fmt.Sprintf("**%s** calls โ‚ฌ%d.", name, amount) case "raise": return fmt.Sprintf("**%s** raises to โ‚ฌ%d.", name, amount) case "allin": return fmt.Sprintf("**%s** is ALL IN for โ‚ฌ%d! ๐Ÿšจ", name, amount) default: return fmt.Sprintf("**%s** acts.", name) } } // renderWinnerAnnouncement formats a winner message. func renderWinnerAnnouncement(name string, amount int64, handName string, showCards bool, hole [2]poker.Card) string { if showCards { return fmt.Sprintf("๐Ÿ† **%s** wins โ‚ฌ%d with %s! %s", name, amount, handName, renderHoleCards(hole)) } return fmt.Sprintf("๐Ÿ† **%s** wins โ‚ฌ%d!", name, amount) } // renderShowdownLine formats one player's showdown result. func renderShowdownLine(name string, hole [2]poker.Card, handName string, won int64) string { if won > 0 { return fmt.Sprintf(" %s: %s โ€” %s (won โ‚ฌ%d)", name, renderHoleCards(hole), handName, won) } return fmt.Sprintf(" %s: %s โ€” %s", name, renderHoleCards(hole), handName) } // renderUncalledBetReturn formats the uncalled bet message. func renderUncalledBetReturn(name string, amount int64) string { return fmt.Sprintf("โ†ฉ๏ธ Uncalled bet of โ‚ฌ%d returned to **%s**.", amount, name) } // renderStartAnnouncement formats the hand start message for the room. func renderStartAnnouncement(g *HoldemGame) string { var sb strings.Builder sb.WriteString("๐ŸŽฐ **Texas Hold'em** โ€” Hand starting!\n\n") sb.WriteString("**Players:**\n") for i, p := range g.Players { if p.State == PlayerSatOut { continue } pos := g.positionLabel(i) sb.WriteString(fmt.Sprintf(" %s (%s) โ€” โ‚ฌ%d\n", p.DisplayName, pos, p.Stack)) } sb.WriteString(fmt.Sprintf("\nBlinds: โ‚ฌ%d / โ‚ฌ%d\n", g.SmallBlind, g.BigBlind)) return sb.String() } // renderEndAnnouncement formats the hand end message for the room. func renderEndAnnouncement(results []showdownResult, g *HoldemGame) string { var sb strings.Builder sb.WriteString("๐ŸŽฐ **Texas Hold'em** โ€” Hand complete!\n\n") if len(g.Community) > 0 { sb.WriteString(fmt.Sprintf("Board: %s\n\n", renderCards(g.Community))) } if len(results) > 0 { sb.WriteString("**Results:**\n") for _, r := range results { sb.WriteString(r.line + "\n") } } sb.WriteString("\n**Stacks:**\n") for _, p := range g.Players { if p.State == PlayerSatOut { continue } delta := p.Stack - p.OpeningStack sign := "" if delta > 0 { sign = "+" } sb.WriteString(fmt.Sprintf(" %s โ€” โ‚ฌ%d (%s%d)\n", p.DisplayName, p.Stack, sign, delta)) } return sb.String() } // renderHelpMessage returns the help text for !holdem help. func renderHelpMessage() string { return "๐ŸŽฐ **Texas Hold'em Commands**\n\n" + "**Quick Start:**\n" + "`!holdem play [amount]` โ€” Join, add bot, and deal in one command\n\n" + "**Lobby:**\n" + "`!holdem join [amount]` โ€” Sit down (optional buy-in amount)\n" + "`!holdem leave` โ€” Leave the table\n" + "`!holdem start` โ€” Start dealing (โ‰ฅ2 players)\n" + "`!holdem addbot` โ€” Add an AI opponent\n\n" + "**In-Game (your turn):**\n" + "`!holdem fold` โ€” Fold your hand\n" + "`!holdem check` โ€” Check (no bet to call)\n" + "`!holdem call` โ€” Call the current bet\n" + "`!holdem raise ` โ€” Raise *to* โ‚ฌamount total (not raise *by*)\n" + "`!holdem allin` โ€” Go all-in\n\n" + "**Other:**\n" + "`!holdem status` โ€” Get current table state (DM)\n" + "`!holdem help` โ€” Show this message\n\n" + "**DM-Only:**\n" + "`!holdem tips on/off` โ€” Toggle coaching tips\n" } // showdownResult holds one player's showdown display line. type showdownResult struct { line string }