aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino_ux.go
blob: fdd453354e8d49d4803ccf0f9017cce6826e5d3e (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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package game

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"

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

type casinoChoice struct {
	address string
	names   []string
	table   bool
}

// Exact addresses beat aliases; prefixes must identify exactly one game.
func resolveCasinoChoice(choices []casinoChoice, input string) (casinoChoice, bool) {
	input = strings.ToLower(input)
	for _, c := range choices {
		if input != "" && strings.EqualFold(c.address, input) {
			return c, true
		}
	}
	if input != "" {
		var exact []casinoChoice
		for _, c := range choices {
			for _, name := range c.names {
				if strings.EqualFold(name, input) {
					exact = append(exact, c)
					break
				}
			}
		}
		if len(exact) == 1 {
			return exact[0], true
		}
		if len(exact) > 1 {
			return casinoChoice{}, false
		}
	}
	matches := []casinoChoice{}
	for _, c := range choices {
		matched := input == ""
		for _, name := range c.names {
			if strings.EqualFold(name, input) || (input != "" && strings.HasPrefix(strings.ToLower(name), input)) {
				matched = true
			}
		}
		if matched {
			matches = append(matches, c)
		}
	}
	if len(matches) == 1 {
		return matches[0], true
	}
	return casinoChoice{}, false
}

func (g *Game) casinoChoices(roomID int) []casinoChoice {
	room, err := g.World.LoadRoom(roomID)
	if err != nil {
		return nil
	}
	var choices []casinoChoice
	for _, c := range room.CasinoTables {
		names := []string{string(c.Game)}
		switch c.Game {
		case casino.GameBlackjack:
			names = append(names, "bj")
		case casino.GameBaccarat:
			names = append(names, "bac")
		}
		choices = append(choices, casinoChoice{address: string(c.Game), names: names, table: true})
	}
	for _, c := range room.CasinoMachines {
		choices = append(choices, casinoChoice{address: c.ID, names: []string{c.ID, string(c.Game), c.Variant}})
	}
	return choices
}

func (g *Game) showCasinoChoices(sess *net.Session, choices []casinoChoice) {
	if len(choices) == 0 {
		sess.WriteLine("There are no casino games here.")
		return
	}
	commands := []casino.MenuCommand{}
	for _, c := range choices {
		commands = append(commands, casino.MenuCommand{Command: "play " + c.address, Desc: "Join this game"})
	}
	sess.WriteLine(g.renderCasinoCommands(sess, "", "Choose a game (use an unambiguous name or abbreviation):", commands))
}

func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) {
	if sess.Player == nil {
		return
	}
	if len(args) > 0 && args[0] != "rules" && args[0] != "help" {
		sess.WriteLine("Use table, table rules, or table help.")
		return
	}
	t, playing := g.Casino.Participation(sess.Player.Name)
	if !playing || t == nil {
		g.showCasinoChoices(sess, g.casinoChoices(sess.Player.RoomID))
		return
	}
	if len(args) > 0 && args[0] == "rules" {
		text := t.Config.BlackjackRulesText()
		if t.Config.Game == casino.GameBaccarat {
			text = t.Config.BaccaratRulesText()
		}
		sess.WriteLine(g.colorize(sess, "casino_action", text))
		return
	}
	g.emitCasinoEvents(t.Status(sess.Player.Name))
	if len(args) > 0 {
		sess.WriteLine(g.renderCasinoCommands(sess, "", "Table controls", []casino.MenuCommand{
			{Command: "bet / rebet", Desc: "Repeat the displayed wager"},
			{Command: "bet min / bet max", Desc: "Use the table minimum / affordable maximum"},
			{Command: "h / st / dd / sp", Desc: "Blackjack: hit / stand / double / split (s and d still move)"},
			{Command: "insurance / decline", Desc: "Answer the insurance offer explicitly"},
			{Command: "sitout", Desc: "Skip future rounds; bet to rejoin"},
			{Command: "stop", Desc: "Leave now, or after your committed round"},
			{Command: "option casino_display compact", Desc: "Short cards and action hints (or detailed for card art)"},
			{Command: "table rules", Desc: "Review this table’s rules"},
		}))
	}
}

func (g *Game) executeWatch(sess *net.Session, args []string, _ string) {
	if sess.Player == nil {
		return
	}
	if len(args) == 1 && args[0] == "off" {
		sess.CasinoWatchTable = ""
		sess.WriteLine(g.colorize(sess, "casino_action", "You stop watching table details."))
		return
	}
	if len(args) > 1 {
		sess.WriteLine("Use watch [game] or watch off.")
		return
	}
	choices := []casinoChoice{}
	for _, c := range g.casinoChoices(sess.Player.RoomID) {
		if c.table {
			choices = append(choices, c)
		}
	}
	input := ""
	if len(args) == 1 {
		input = args[0]
	}
	c, ok := resolveCasinoChoice(choices, input)
	if !ok {
		sess.WriteLine("Choose a table to watch: watch blackjack or watch baccarat.")
		return
	}
	t, ok := g.casinoTable(sess.Player.RoomID, c.address)
	if !ok {
		return
	}
	sess.CasinoWatchRoom = sess.Player.RoomID
	sess.CasinoWatchTable = t.Config.ID
	sess.WriteLine(g.colorize(sess, "casino_action", "Watching "+t.Config.Label()+". Detailed public cards are visible; watch off stops watching."))
	g.emitCasinoEvents(t.Status(sess.Player.Name))
}

func (g *Game) seesCasinoDetails(sess *net.Session, event casino.Event) bool {
	if sess.Player == nil {
		return false
	}
	// Slot output is unchanged; watch controls multiplayer tables only.
	if g.Casino.Table(event.RoomID, event.TableID) == nil {
		return true
	}
	if sess.CasinoWatchRoom == event.RoomID && sess.CasinoWatchTable == event.TableID {
		return true
	}
	// Settlement may have removed a queued departure before these events
	// are delivered. Their own final cards must still be visible.
	switch s := event.Snapshot.(type) {
	case *casino.BlackjackSnapshot:
		for _, p := range s.Players {
			if p.Name == sess.Player.Name {
				return true
			}
		}
	case *casino.BaccaratSnapshot:
		for _, p := range s.Bets {
			if p.Name == sess.Player.Name {
				return true
			}
		}
	}
	t, ok := g.Casino.Participation(sess.Player.Name)
	return ok && t != nil && t.RoomID == event.RoomID && t.Config.ID == event.TableID
}

var casinoTicksPattern = regexp.MustCompile(`([0-9]+) ticks`)

// Casino logic counts engine ticks; present deadlines in actual configured seconds.
func (g *Game) casinoTimeText(text string) string {
	ms := 600
	if g.Ticks != nil {
		ms = g.Ticks.TickLengthMilliseconds()
	}
	return casinoTicksPattern.ReplaceAllStringFunc(text, func(s string) string {
		n, _ := strconv.Atoi(strings.Fields(s)[0])
		return fmt.Sprintf("%g seconds", float64(n)*float64(ms)/1000)
	})
}

func (g *Game) casinoBalance(sess *net.Session) string {
	if sess.Player == nil {
		return ""
	}
	p := sess.Player
	return fmt.Sprintf("Available %s (%s chips + %s credits)", g.colorize(sess, "casino_chips", strconv.Itoa(casinoAffordable(p))), g.colorize(sess, "casino_chips", strconv.Itoa(countChips(p))), g.colorize(sess, "casino_credits", strconv.Itoa(p.Credits)))
}