Skip to content

Commit 2fb19c1

Browse files
committed
Add Liar's Dice
1 parent 188db41 commit 2fb19c1

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package net.zomis.games.impl
2+
3+
import net.zomis.games.WinResult
4+
import net.zomis.games.common.PlayerIndex
5+
import net.zomis.games.common.next
6+
import net.zomis.games.dsl.ActionRuleScope
7+
import net.zomis.games.dsl.GameCreator
8+
import net.zomis.games.dsl.ReplayableScope
9+
import net.zomis.games.dsl.Viewable
10+
import kotlin.random.Random
11+
12+
data class LiarsDiceBet(val amount: Int, val value: Int): Comparable<LiarsDiceBet> {
13+
override fun compareTo(other: LiarsDiceBet): Int {
14+
if (amount != other.amount) return amount - other.amount
15+
return value - other.value
16+
}
17+
}
18+
19+
class LiarsDicePlayer(val index: Int, var dice: MutableList<Int>) {
20+
fun removeDie() {
21+
if (dice.isEmpty()) return
22+
dice.removeAt(dice.lastIndex)
23+
}
24+
25+
val eliminated: Boolean get() = dice.isEmpty()
26+
}
27+
class LiarsDice(val config: LiarsDiceConfig, val playerCount: Int): Viewable {
28+
val players = (0 until playerCount).map { LiarsDicePlayer(it, (1..5).toMutableList()) }
29+
var currentPlayerIndex: Int = 0
30+
val currentPlayer get() = players[currentPlayerIndex]
31+
32+
val diceCount get() = players.sumBy { it.dice.size }
33+
34+
var bet: Pair<LiarsDicePlayer, LiarsDiceBet>? = null
35+
fun currentBet(): LiarsDiceBet? = bet?.second
36+
37+
fun isSpotOn() = currentBet() == correctBet(currentBet()!!.value)
38+
fun isLie() = correctBet(currentBet()!!.value) < currentBet()!!
39+
40+
fun correctBet(value: Int) = LiarsDiceBet(players.sumBy { player -> player.dice.count { it == value } }, value)
41+
fun nextPlayer() {
42+
do {
43+
this.currentPlayerIndex = this.currentPlayerIndex.next(players.size)
44+
} while (this.currentPlayer.eliminated)
45+
}
46+
47+
override fun toView(viewer: PlayerIndex): Any? {
48+
return mapOf(
49+
"currentPlayer" to currentPlayerIndex,
50+
"config" to config,
51+
"better" to bet?.first?.index,
52+
"bet" to bet?.second,
53+
"players" to players.map {
54+
mapOf(
55+
"playerIndex" to it.index,
56+
"dice" to if (it.index == viewer) it.dice else it.dice.map { 0 }
57+
)
58+
}
59+
)
60+
}
61+
62+
}
63+
64+
data class LiarsDiceConfig(
65+
val allowHigherQuantityAnyFace: Boolean = true,
66+
val allowHigherFaceAnyQuantity: Boolean = false,
67+
val onesAreWild: Boolean = false,
68+
val allowSpotOn: Boolean = true,
69+
val spotOnEveryoneLoses: Boolean = false,
70+
val callingWildsFirstResetsThem: Boolean = false, // point 5 at https://www.wikihow.com/Play-Liar%27s-Dice
71+
val twoDiceLeftBetOnSum: Boolean = false // If 2 players left with only one die each, bet on sum of the dice. (point 9 in wikihow)
72+
)
73+
74+
object LiarsDiceGame {
75+
val random = Random.Default
76+
fun newRound(game: LiarsDice, replayable: ReplayableScope) {
77+
game.bet = null
78+
game.players.forEach {
79+
it.dice = replayable.ints("player-" + it.index) { it.dice.map { random.nextInt(1, 6) } }.toMutableList()
80+
}
81+
}
82+
83+
val factory = GameCreator(LiarsDice::class)
84+
val liar = factory.singleAction("liar")
85+
val spotOn = factory.singleAction("spotOn")
86+
val bet = factory.action("bet", LiarsDiceBet::class)
87+
88+
val game = factory.game("LiarsDice") {
89+
setup(LiarsDiceConfig::class) {
90+
players(2..16)
91+
defaultConfig { LiarsDiceConfig() }
92+
init { LiarsDice(config, playerCount) }
93+
}
94+
rules {
95+
gameStart {
96+
newRound(game, replayable)
97+
}
98+
99+
allActions.precondition { game.currentPlayerIndex == playerIndex }
100+
101+
action(liar) {
102+
precondition { game.bet != null }
103+
effect {
104+
logRevealAllDice("liar", this)
105+
if (game.isLie()) {
106+
val losingPlayer = game.bet!!.first
107+
log { "$player called liar and is correct! ${player(losingPlayer.index)} loses one die." }
108+
losingPlayer.removeDie()
109+
game.currentPlayerIndex = losingPlayer.index
110+
} else {
111+
log { "$player called liar incorrectly and loses one die." }
112+
game.currentPlayer.removeDie()
113+
}
114+
newRound(game, replayable)
115+
}
116+
}
117+
action(spotOn) {
118+
precondition { game.bet != null }
119+
precondition { game.config.allowSpotOn }
120+
effect {
121+
logRevealAllDice("spot-on", this)
122+
if (game.isSpotOn()) {
123+
val losingPlayer = game.bet!!.first
124+
if (game.config.spotOnEveryoneLoses) {
125+
log { "$player called spot-on and was correct! Everyone loses one die." }
126+
game.players.filter { it != game.currentPlayer }.forEach { it.removeDie() }
127+
} else {
128+
log { "$player called spot-on and was correct! ${player(losingPlayer.index)} loses one die." }
129+
losingPlayer.removeDie()
130+
}
131+
game.currentPlayerIndex = losingPlayer.index
132+
} else {
133+
log { "$player called spot-on incorrectly and loses one die." }
134+
game.currentPlayer.removeDie()
135+
}
136+
newRound(game, replayable)
137+
}
138+
}
139+
action(bet) {
140+
choose {
141+
val bet = context.game.bet?.second ?: LiarsDiceBet(1, 0)
142+
options({ bet.amount..context.game.diceCount }) {amount ->
143+
val min = if (amount == bet.amount) bet.value + 1 else 1
144+
options({ min..6 }) {value ->
145+
parameter(LiarsDiceBet(amount, value))
146+
}
147+
}
148+
}
149+
effect {
150+
game.bet = game.currentPlayer to action.parameter
151+
game.nextPlayer()
152+
log { "$player bets ${action.amount} ${action.value}'s" }
153+
}
154+
}
155+
allActions.after {
156+
val remaining = eliminations.remainingPlayers()
157+
game.players.filter { it.eliminated && remaining.contains(it.index) }.forEach {
158+
eliminations.result(it.index, WinResult.LOSS)
159+
}
160+
if (eliminations.remainingPlayers().size == 1) {
161+
eliminations.eliminateRemaining(WinResult.WIN)
162+
}
163+
while (game.currentPlayer.eliminated) {
164+
game.currentPlayerIndex = game.currentPlayerIndex.next(game.playerCount)
165+
}
166+
}
167+
}
168+
}
169+
170+
private fun logRevealAllDice(call: String, scope: ActionRuleScope<LiarsDice, *>) {
171+
scope.log { "$player calls $call and everyone reveals their dice!" }
172+
scope.game.players.filter { !it.eliminated }.forEach {
173+
scope.log { "${player(it.index)} had ${it.dice.sorted().joinToString(", ")}" }
174+
}
175+
}
176+
177+
}

games-server/src/main/kotlin/net/zomis/games/server2/ServerGames.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ object ServerGames {
99

1010
val games = mutableMapOf<String, Any>(
1111
"Set" to SetGame.game,
12+
"LiarsDice" to LiarsDiceGame.game,
1213
"Dungeon Mayhem" to DungeonMayhemDsl.game,
1314
"Skull" to SkullGame.game,
1415
"Splendor" to DslSplendor.splendorGame,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<template>
2+
<v-container fluid>
3+
<v-row>
4+
<v-col v-for="(player, playerIndex) in view.players" :key="playerIndex"
5+
:class="{ currentPlayer: playerIndex == view.currentPlayer }">
6+
<v-card>
7+
<v-card-title>
8+
<PlayerProfile show-name :player="context.players[playerIndex]" />
9+
</v-card-title>
10+
<v-card-text>
11+
<CardZone v-if="Array.isArray(player.dice)">
12+
<span v-for="(value, index) in player.dice" :key="index" class="list-complete-item">
13+
{{ value }}
14+
</span>
15+
</CardZone>
16+
<CardZone v-else>
17+
<v-icon v-for="(index) in player.dice" :key="index" class="list-complete-item">
18+
mdi-crosshairs-question
19+
</v-icon>
20+
</CardZone>
21+
22+
<Actionable button v-if="context.players[playerIndex].controllable" actionable="liar" :actions="actions">Liar</Actionable>
23+
<Actionable button v-if="context.players[playerIndex].controllable" actionable="spotOn" :actions="actions">Spot-On!</Actionable>
24+
<Actionable button v-if="context.players[playerIndex].controllable" :actionType="['bet']" :actions="actions">Bet</Actionable>
25+
</v-card-text>
26+
</v-card>
27+
</v-col>
28+
</v-row>
29+
<v-row>
30+
Current Bet: {{ view.bet }} by {{ view.better }}
31+
</v-row>
32+
<v-row>
33+
Config: {{ view.config }}
34+
</v-row>
35+
</v-container>
36+
</template>
37+
<script>
38+
import CardZone from "@/components/games/common/CardZone"
39+
import PlayerProfile from "@/components/games/common/PlayerProfile"
40+
import Actionable from "@/components/games/common/Actionable"
41+
42+
export default {
43+
name: "LiarsDice",
44+
props: ["view", "actions", "context"],
45+
components: {
46+
PlayerProfile, CardZone,
47+
Actionable
48+
}
49+
}
50+
</script>
51+
<style scoped>
52+
.actionable {
53+
border-style: solid !important;
54+
border-width: thick !important;
55+
border-color: #ffd166 !important;
56+
}
57+
</style>

games-vue-client/src/supportedGames.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import TreeViewGame from "@/components/games/TreeViewGame";
1616
import Skull from "@/components/games/skull/Skull";
1717
import DSLTTT from "@/components/games/DSLTTT";
1818
import TTT3D from "@/components/games/TTT3D";
19+
import LiarsDice from "@/components/games/LiarsDice";
1920

2021
// ViewTypes for ActionLog
2122
import SplendorCard from "@/components/games/splendor/SplendorCard"
@@ -134,6 +135,19 @@ const supportedGames = {
134135
},
135136
component: Skull
136137
},
138+
"LiarsDice": {
139+
displayName: "Liar's Dice",
140+
dsl: true,
141+
actions: {
142+
bet: (amount) => ({
143+
key: 'amount-' + amount,
144+
next: (face) => `bet ${amount}x ${face}'s`
145+
}),
146+
liar: () => "liar",
147+
spotOn: () => "spotOn"
148+
},
149+
component: LiarsDice
150+
},
137151
"Dungeon Mayhem": {
138152
dsl: gamejs.net.zomis.games.impl.DungeonMayhemDsl.game,
139153
enabled: true,

0 commit comments

Comments
 (0)