diff options
| author | historia <[not public]> | 2026-06-20 03:20:01 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-20 03:20:01 -0400 |
| commit | e4400c5f1e84c18d2126f2cb502ae10685977cbb (patch) | |
| tree | 0523e9d68f1cfdc6f81b862e3068fe12c1a85054 /internal/game/hacking/liars_dice.go | |
| parent | 5ea082ab4e82409aebc889b496f43e52b003d4f7 (diff) | |
| download | thehouseoficarus-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.go | 395 |
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 +} |
