aboutsummaryrefslogtreecommitdiff
path: root/internal/game/look_casino.go
blob: 8d2b5c34058d12a2f5d8605253f8daa6a27d6902 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package game

import (
	"fmt"
	"strings"

	"thehouseoficarus/internal/casino"
	"thehouseoficarus/internal/net"
)

// lookCasinoTable renders a table fixture's details for a look target: the
// wager limits, the live state and seated players, and this table's rule
// variants. The invariant roulette and craps graphics and the generic rule
// dumps are deliberately omitted; blackjack, baccarat and Pai Gow keep their
// felt because it carries per-table rules and pay tables. It reports whether
// the target named this room's game.
func (g *Game) lookCasinoTable(sess *net.Session, target string, game casino.GameName) bool {
	table, ok := g.resolveRoomTable(sess, target, game)
	if !ok {
		return false
	}
	// Use the live table's normalized config: the room fixture may omit
	// defaults (seat count, bet limits) that EnsureTable fills in.
	sess.WriteLine(g.renderCasinoTableDetails(sess, table.Config, table))
	return true
}

// resolveRoomTable matches a look target against the room's table for game
// using the same names and prefix rules as the play command, plus the fixture
// id, an optional " table" suffix, and the bare "table" target when the room
// holds exactly one table.
func (g *Game) resolveRoomTable(sess *net.Session, target string, game casino.GameName) (*casino.Table, bool) {
	if g.Casino == nil || sess.Player == nil {
		return nil, false
	}
	room, err := g.World.LoadRoom(sess.Player.RoomID)
	if err != nil {
		return nil, false
	}
	target = strings.TrimSpace(strings.ToLower(target))
	target = strings.TrimSuffix(target, " table")
	address := string(game)
	if target == "table" {
		if len(room.CasinoTables) != 1 || room.CasinoTables[0].Game != game {
			return nil, false
		}
	} else {
		var choices []casinoChoice
		for _, cfg := range room.CasinoTables {
			if cfg.Game != game {
				continue
			}
			choices = append(choices, casinoChoice{address: string(cfg.Game), names: append(casinoTableAliases(cfg), cfg.ID), table: true})
		}
		choice, ok := resolveCasinoChoice(choices, target)
		if !ok {
			return nil, false
		}
		address = choice.address
	}
	table, ok := g.casinoTable(room.ID, address)
	if !ok {
		return nil, false
	}
	return table, true
}

// renderCasinoTableDetails formats the details block and, for the felt games,
// the per-table rule card. Roulette adds its live wheel; craps never prints a
// board here.
func (g *Game) renderCasinoTableDetails(sess *net.Session, cfg casino.TableConfig, table *casino.Table) string {
	seats := table.Seats()
	seated := "no one"
	if len(seats) > 0 {
		seated = strings.Join(seats, ", ")
	}
	lines := []string{
		g.colorize(sess, "casino_action", cfg.Label()),
		fmt.Sprintf("  Limits   %s", betLimitsText(cfg.MinBet, cfg.MaxBet)),
		fmt.Sprintf("  Seats    %d/%d", len(seats), cfg.MaxPlayers),
		fmt.Sprintf("  State    %s", table.State()),
		fmt.Sprintf("  Rules    %s", casinoTableSummary(cfg)),
		fmt.Sprintf("  Seated   %s", seated),
	}
	switch cfg.Game {
	case casino.GameRoulette:
		// Look, like sitting down and the table command, shows the live
		// wheel: tracker, racetrack, and betting layout.
		if rendered := strings.TrimRight(g.renderCasinoSnapshot(sess, table.Board(sess.Player.Name)), "\n"); rendered != "" {
			lines = append(lines, "", rendered)
		}
	case casino.GameBlackjack:
		lines = append(lines, "", g.renderCasinoSnapshot(sess, &casino.BlackjackRulesView{Config: cfg}))
	case casino.GameBaccarat:
		lines = append(lines, "", g.renderCasinoSnapshot(sess, &casino.BaccaratRulesView{Config: cfg}))
	case casino.GamePaiGow:
		lines = append(lines, "", renderPaiGowFelt(cfg, g.colorMode(sess), sess.Player.OptionBool("unicode")))
	}
	return strings.Join(lines, "\n")
}

// casinoTableSummary is the compact per-table rule description used by the
// look details block. The full wording lives in the rules help page.
func casinoTableSummary(cfg casino.TableConfig) string {
	switch cfg.Game {
	case casino.GameBlackjack:
		b := cfg.Blackjack.Normalize()
		dealer := "dealer stands on soft 17"
		if b.DealerHitsSoft17() {
			dealer = "dealer hits soft 17"
		}
		return fmt.Sprintf("%d decks; %s; blackjack pays %s", b.Decks, dealer, ratioText(b.BlackjackPayout))
	case casino.GameBaccarat:
		return fmt.Sprintf("%d decks", cfg.Baccarat.Normalize().Decks)
	case casino.GameRoulette:
		summary := "single-zero wheel"
		if cfg.Roulette.DoubleZero {
			summary = "double-zero wheel"
		}
		if cfg.Roulette.Racetrack {
			summary += "; racetrack call bets"
		}
		return summary
	case casino.GameCraps:
		return cfg.Craps.Normalize().Title()
	case casino.GamePaiGow:
		p := cfg.PaiGow.Normalize()
		envy := "Envy off"
		if p.Envy {
			envy = "Envy on"
		}
		return p.Label() + "; " + envy
	}
	return cfg.Label()
}