aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hacking/liars_dice.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-20 03:20:01 -0400
committerhistoria <[not public]>2026-06-20 03:20:01 -0400
commite4400c5f1e84c18d2126f2cb502ae10685977cbb (patch)
tree0523e9d68f1cfdc6f81b862e3068fe12c1a85054 /internal/game/hacking/liars_dice.go
parent5ea082ab4e82409aebc889b496f43e52b003d4f7 (diff)
downloadthehouseoficarus-e4400c5f1e84c18d2126f2cb502ae10685977cbb.tar.gz
refactor: combined all station crafting
Diffstat (limited to 'internal/game/hacking/liars_dice.go')
-rw-r--r--internal/game/hacking/liars_dice.go395
1 files changed, 395 insertions, 0 deletions
diff --git a/internal/game/hacking/liars_dice.go b/internal/game/hacking/liars_dice.go
new file mode 100644
index 0000000..bcfbd40
--- /dev/null
+++ b/internal/game/hacking/liars_dice.go
@@ -0,0 +1,395 @@
+package hacking
+
+import (
+ "fmt"
+ "math/rand"
+ "strconv"
+ "strings"
+)
+
+type LiarsDiceGame struct {
+ playerDice []int
+ aiDice []int
+ currentBid LiarsBid
+ hasBid bool
+ playerTurn bool
+ round int
+ gameOver bool
+ won bool
+ playerLost int
+ completed bool
+}
+
+type LiarsBid struct {
+ Quantity int
+ Face int
+}
+
+func NewLiarsDiceGame() *LiarsDiceGame {
+ return &LiarsDiceGame{
+ playerDice: make([]int, 5),
+ aiDice: make([]int, 5),
+ }
+}
+
+func (l *LiarsDiceGame) Name() string {
+ return "Signal Bluff"
+}
+
+func (l *LiarsDiceGame) Init(level int) string {
+ l.playerDice = make([]int, 5)
+ l.aiDice = make([]int, 5)
+ l.round = 0
+ l.gameOver = false
+ l.playerTurn = true
+ l.hasBid = false
+ l.playerLost = 0
+ l.completed = false
+
+ var sb strings.Builder
+ sb.WriteString("=== SIGNAL BLUFF ===\n")
+ sb.WriteString("\n")
+ sb.WriteString("You've connected to a secure channel running a bluffing protocol.\n")
+ sb.WriteString("You and the system each have 5 signal fragments (dice). Each round,\n")
+ sb.WriteString("fragments are scrambled. You see only yours. Take turns making claims\n")
+ sb.WriteString("about the total count of a specific signal across ALL fragments.\n")
+ sb.WriteString("\n")
+ sb.WriteString("1s are wild -- they count as any signal.\n")
+ sb.WriteString("\n")
+ sb.WriteString("Commands:\n")
+ sb.WriteString(" bid <qty> <face> - Claim at least N fragments show face F\n")
+ sb.WriteString(" (e.g., 'bid 3 4' = \"at least three 4s\")\n")
+ sb.WriteString(" call - Challenge the last bid (reveal all)\n")
+ sb.WriteString(" exact - Claim the bid is exactly right\n")
+ sb.WriteString(" status - Show your fragments and current bid\n")
+ sb.WriteString(" jack out - Disconnect (forfeit)\n")
+ sb.WriteString("\n")
+ sb.WriteString(l.startRound())
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) HandleInput(input string) (string, bool, bool) {
+ input = strings.TrimSpace(strings.ToLower(input))
+ parts := strings.Fields(input)
+
+ if len(parts) == 0 {
+ return "Commands: bid <qty> <face>, call, exact, status, jack out", false, false
+ }
+
+ cmd := parts[0]
+
+ switch cmd {
+ case "bid", "b":
+ if len(parts) < 3 {
+ return "Usage: bid <quantity> <face> (e.g., bid 3 4)", false, false
+ }
+ qty, err1 := strconv.Atoi(parts[1])
+ face, err2 := strconv.Atoi(parts[2])
+ if err1 != nil || err2 != nil {
+ return "Usage: bid <quantity> <face> (e.g., bid 3 4)", false, false
+ }
+ return l.doBid(qty, face)
+ case "call", "c", "intercept", "liar":
+ return l.doCall(false)
+ case "exact", "e":
+ return l.doExact()
+ case "status":
+ return l.doStatus(), false, false
+ default:
+ if len(parts) == 2 {
+ qty, err1 := strconv.Atoi(parts[0])
+ face, err2 := strconv.Atoi(parts[1])
+ if err1 == nil && err2 == nil {
+ return l.doBid(qty, face)
+ }
+ }
+ return "Commands: bid <qty> <face>, call, exact, status, jack out", false, false
+ }
+}
+
+func (l *LiarsDiceGame) doBid(qty, face int) (string, bool, bool) {
+ if face < 1 || face > 6 {
+ return "Face must be 1-6.", false, false
+ }
+ if qty < 1 {
+ return "Quantity must be at least 1.", false, false
+ }
+ if !l.playerTurn {
+ return "It's not your turn to bid. Use 'call' or 'exact'.", false, false
+ }
+
+ if l.hasBid {
+ if qty < l.currentBid.Quantity || (qty == l.currentBid.Quantity && face <= l.currentBid.Face) {
+ return "You must bid higher (higher quantity or same quantity with higher face).", false, false
+ }
+ }
+
+ l.currentBid = LiarsBid{Quantity: qty, Face: face}
+ l.hasBid = true
+ l.playerTurn = false
+
+ return l.aiTurn()
+}
+
+func (l *LiarsDiceGame) doCall(player bool) (string, bool, bool) {
+ if !l.hasBid {
+ return "No bid to call yet. Make a bid first.", false, false
+ }
+
+ var sb strings.Builder
+ if player {
+ sb.WriteString("System intercepts!\n")
+ } else {
+ sb.WriteString("You intercept!\n")
+ }
+
+ sb.WriteString(l.showAllDice())
+ sb.WriteString(fmt.Sprintf("\nBid was: %d %s\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face)))
+
+ total := l.countFace(l.currentBid.Face)
+ sb.WriteString(fmt.Sprintf("Actual count: %d (%d %ss + %d wild 1s)\n\n",
+ total,
+ l.countFaceNonWild(l.currentBid.Face),
+ faceName(l.currentBid.Face),
+ l.countFace(1)-l.countFaceNonWild(1)))
+
+ if total >= l.currentBid.Quantity {
+ sb.WriteString("The bid holds! ")
+ if player {
+ sb.WriteString("System loses a fragment.")
+ l.aiDice = l.aiDice[:len(l.aiDice)-1]
+ } else {
+ sb.WriteString("You lose a fragment.")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ }
+ } else {
+ sb.WriteString("Bluff called! ")
+ if player {
+ sb.WriteString("You lose a fragment.")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ } else {
+ sb.WriteString("System loses a fragment.")
+ l.aiDice = l.aiDice[:len(l.aiDice)-1]
+ }
+ }
+
+ return l.checkGameOver(sb)
+}
+
+func (l *LiarsDiceGame) doExact() (string, bool, bool) {
+ if !l.hasBid {
+ return "No bid to claim exact on. Make a bid first.", false, false
+ }
+
+ var sb strings.Builder
+ sb.WriteString("You claim exact match!\n")
+
+ sb.WriteString(l.showAllDice())
+ total := l.countFace(l.currentBid.Face)
+ sb.WriteString(fmt.Sprintf("\nBid was: %d %s\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face)))
+ sb.WriteString(fmt.Sprintf("Actual count: %d\n\n", total))
+
+ if total == l.currentBid.Quantity {
+ sb.WriteString("Exact match! You recover a fragment.\n")
+ if len(l.playerDice) < 5 {
+ l.playerDice = append(l.playerDice, rand.Intn(6)+1)
+ }
+ } else {
+ sb.WriteString("Not an exact match. You lose a fragment.\n")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ }
+
+ return l.checkGameOver(sb)
+}
+
+func (l *LiarsDiceGame) checkGameOver(sb strings.Builder) (string, bool, bool) {
+ if len(l.playerDice) == 0 {
+ sb.WriteString("\nYou've lost all your fragments. Connection terminated.")
+ l.gameOver = true
+ l.won = false
+ l.completed = true
+ return sb.String(), true, false
+ }
+ if len(l.aiDice) == 0 {
+ sb.WriteString("\nSystem has no fragments left. You win!")
+ l.gameOver = true
+ l.won = true
+ l.completed = true
+ return sb.String(), true, true
+ }
+
+ sb.WriteString(l.startRound())
+ return sb.String(), false, false
+}
+
+func (l *LiarsDiceGame) showAllDice() string {
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("Your fragments: %s\n", l.diceStr(l.playerDice)))
+ sb.WriteString(fmt.Sprintf("System fragments: %s\n", l.diceStr(l.aiDice)))
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) diceStr(dice []int) string {
+ var parts []string
+ for _, d := range dice {
+ parts = append(parts, fmt.Sprintf("[%d]", d))
+ }
+ return strings.Join(parts, " ")
+}
+
+func (l *LiarsDiceGame) doStatus() string {
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("--- Cycle %d ---\n", l.round))
+ sb.WriteString(fmt.Sprintf("Your fragments: %s (You: %d, System: %d)\n",
+ l.diceStr(l.playerDice), len(l.playerDice), len(l.aiDice)))
+ if l.hasBid {
+ who := "System"
+ if !l.playerTurn {
+ who = "System"
+ } else {
+ who = "you"
+ }
+ sb.WriteString(fmt.Sprintf("Current bid: %d %s (by %s)\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face), who))
+ }
+ if l.playerTurn {
+ sb.WriteString("Your turn: make a bid.")
+ } else {
+ sb.WriteString("Your turn: bid higher, call, or exact.")
+ }
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) startRound() string {
+ l.round++
+ for i := range l.playerDice {
+ l.playerDice[i] = rand.Intn(6) + 1
+ }
+ for i := range l.aiDice {
+ l.aiDice[i] = rand.Intn(6) + 1
+ }
+ l.hasBid = false
+ l.playerTurn = true
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("--- Cycle %d ---\n", l.round))
+ sb.WriteString(fmt.Sprintf("Your fragments: %s (You: %d, System: %d)\n",
+ l.diceStr(l.playerDice), len(l.playerDice), len(l.aiDice)))
+ sb.WriteString("You go first. Make a bid.\n")
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) aiTurn() (string, bool, bool) {
+ totalDice := len(l.playerDice) + len(l.aiDice)
+
+ aiCount := l.countFace(l.currentBid.Face)
+ unknownDice := len(l.playerDice)
+ estimated := float64(aiCount) + float64(unknownDice)/3.0
+
+ if float64(l.currentBid.Quantity) > estimated*1.5 {
+ return l.doCall(true)
+ }
+
+ bestFace, bestCount := l.aiBestFace()
+ newQty := l.currentBid.Quantity
+ newFace := l.currentBid.Face
+
+ if bestFace > newFace && bestCount >= newQty {
+ newFace = bestFace
+ } else {
+ newQty++
+ }
+
+ if newQty > totalDice {
+ return l.doCall(true)
+ }
+
+ l.currentBid = LiarsBid{Quantity: newQty, Face: newFace}
+ l.playerTurn = true
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("System broadcasts: %d %s.\n", newQty, faceNamePlural(newFace)))
+ sb.WriteString("Your turn: bid higher, call, or exact.\n")
+ return sb.String(), false, false
+}
+
+func (l *LiarsDiceGame) aiBestFace() (face int, count int) {
+ counts := make(map[int]int)
+ for _, d := range l.aiDice {
+ if d == 1 {
+ continue
+ }
+ counts[d]++
+ }
+ wilds := 0
+ for _, d := range l.aiDice {
+ if d == 1 {
+ wilds++
+ }
+ }
+ best := 2
+ bestN := counts[2] + wilds
+ for f := 3; f <= 6; f++ {
+ n := counts[f] + wilds
+ if n > bestN || (n == bestN && f > best) {
+ best = f
+ bestN = n
+ }
+ }
+ return best, bestN
+}
+
+func (l *LiarsDiceGame) countFace(face int) int {
+ count := 0
+ for _, d := range l.playerDice {
+ if d == face || d == 1 {
+ count++
+ }
+ }
+ for _, d := range l.aiDice {
+ if d == face || d == 1 {
+ count++
+ }
+ }
+ return count
+}
+
+func (l *LiarsDiceGame) countFaceNonWild(face int) int {
+ count := 0
+ for _, d := range l.playerDice {
+ if d == face {
+ count++
+ }
+ }
+ for _, d := range l.aiDice {
+ if d == face {
+ count++
+ }
+ }
+ return count
+}
+
+func faceName(f int) string {
+ names := []string{"", "one", "two", "three", "four", "five", "six"}
+ if f >= 1 && f <= 6 {
+ return names[f]
+ }
+ return fmt.Sprint(f)
+}
+
+func faceNamePlural(f int) string {
+ names := []string{"", "ones", "twos", "threes", "fours", "fives", "sixes"}
+ if f >= 1 && f <= 6 {
+ return names[f]
+ }
+ return fmt.Sprint(f)
+}
+
+func (l *LiarsDiceGame) BonusXP() int {
+ if l.completed && l.won && l.playerLost == 0 {
+ return 150
+ }
+ return 0
+}