Skip to content

Commit 8a23b3a

Browse files
committed
Add Dungeon Mayhem game
1 parent a78cd9a commit 8a23b3a

File tree

3 files changed

+107
-46
lines changed

3 files changed

+107
-46
lines changed

games-core/src/main/kotlin/net/zomis/games/impl/DungeonMayhem.kt

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ package net.zomis.games.impl
33
import net.zomis.games.WinResult
44
import net.zomis.games.cards.Card
55
import net.zomis.games.cards.CardZone
6-
import net.zomis.games.dsl.ActionSerialization
7-
import net.zomis.games.dsl.ReplayableScope
8-
import net.zomis.games.dsl.createActionType
9-
import net.zomis.games.dsl.createGame
6+
import net.zomis.games.dsl.*
107
import net.zomis.games.dsl.sourcedest.next
118
import kotlin.math.min
129

1310
data class DungeonMayhemConfig(
1411
val drawNewImmediately: Boolean = false,
15-
val pickPocketIgnoresCleverDisguise: Boolean = true
12+
val pickPocketIgnoresCleverDisguise: Boolean = false
1613
)
1714
private infix fun String.card(symbol: DungeonMayhemSymbol): DungeonMayhemCard {
1815
return this card listOf(symbol)
@@ -50,14 +47,14 @@ enum class DungeonMayhemSymbol {
5047
operator fun times(count: Int): List<DungeonMayhemSymbol> = (1..count).map { this }
5148
fun availableTargets(game: DungeonMayhem): List<DungeonMayhemTarget>? {
5249
return when (this) {
53-
ATTACK -> game.players.minus(game.currentPlayer).flatMap { player ->
50+
ATTACK -> game.players.minus(game.currentPlayer).filter { it.index == game.attackedPlayer || game.attackedPlayer == null }.filter { it.health > 0 }.flatMap { player ->
5451
if (player.shields.size == 0) listOf(DungeonMayhemTarget(player.index, null, null))
5552
else player.shields.indices.map { DungeonMayhemTarget(player.index, it, null) }
5653
}
57-
DESTROY_SINGLE_SHIELD, STEAL_SHIELD -> game.players.flatMap { player -> player.shields.indices.map { player.index to it } }.map {
54+
DESTROY_SINGLE_SHIELD, STEAL_SHIELD -> game.players.filter { it.health > 0 }.flatMap { player -> player.shields.indices.map { player.index to it } }.map {
5855
DungeonMayhemTarget(it.first, it.second, null)
5956
}
60-
SWAP_HITPOINTS, STEAL_CARD -> game.players.minus(game.currentPlayer).map { DungeonMayhemTarget(it.index, null, null) }
57+
SWAP_HITPOINTS, STEAL_CARD -> game.players.minus(game.currentPlayer).filter { it.health > 0 }.map { DungeonMayhemTarget(it.index, null, null) }
6158
PICK_UP_CARD -> game.currentPlayer.discard.cards.mapIndexed { index, _ -> DungeonMayhemTarget(game.currentPlayerIndex, null, index) }
6259
else -> null
6360
}
@@ -70,33 +67,41 @@ enum class DungeonMayhemSymbol {
7067
when (this) {
7168
HEAL -> player.health = min(player.health + count, 10)
7269
DRAW -> player.drawCard(replayable, "drawEffect", count)
73-
FIREBALL -> repeat(count) { game.players.forEach { it.damage(3) } }
74-
DESTROY_ALL_SHIELDS -> game.players.forEach { it.shields.asSequence().forEach { c -> c.card.destroy(c) } }
75-
// PROTECTION_ONE_TURN ->
70+
FIREBALL -> repeat(count) { game.players.filter { !it.protected }.forEach { it.damage(3) } }
71+
DESTROY_ALL_SHIELDS -> game.players.filter { !it.protected }.forEach { it.shields.asSequence().forEach { c -> c.card.destroy(c) } }
72+
PROTECTION_ONE_TURN -> player.protected = true
7673
HEAL_AND_ATTACK_FOR_EACH_OPPONENT -> {
7774
val opponents = game.players.minus(player)
7875
player.heal(opponents.size)
79-
opponents.forEach { it.damage(1) }
76+
opponents.filter { !it.protected }.forEach { it.damage(1) }
77+
}
78+
ALL_DISCARD_AND_DRAW -> repeat(count) {
79+
game.players.filter { !it.protected }.forEach { it.hand.moveAllTo(it.discard); it.drawCard(replayable, "allDraw", 3) }
8080
}
81-
ALL_DISCARD_AND_DRAW -> repeat(count) { game.players.forEach { it.hand.moveAllTo(it.discard); it.drawCard(replayable, "allDraw", 3) } }
8281
SHIELD -> return true
8382
else -> return false
8483
}
8584
return true
8685
}
8786

88-
fun resolve(game: DungeonMayhem, count: Int, target: DungeonMayhemTarget) {
87+
fun resolve(game: DungeonMayhem, scope: GameRuleTriggerScope<DungeonMayhem, DungeonMayhemEffect>, playEffect: GameRuleTrigger<DungeonMayhem, DungeonMayhemPlayCard>) {
88+
val target = scope.trigger.target
8989
val player: DungeonMayhemPlayer = game.players[target.player]
90-
when (this) {
91-
ATTACK -> player.damage(count)
90+
return when (this) {
91+
ATTACK -> {
92+
if (target.shieldCard != null) player.shields[target.shieldCard].card.health -= scope.trigger.count
93+
else player.damage(scope.trigger.count)
94+
}
9295
DESTROY_SINGLE_SHIELD -> player.shields[target.shieldCard!!].let { it.card.destroy(it) }
9396
STEAL_SHIELD -> player.shields[target.shieldCard!!].moveTo(game.currentPlayer.shields)
9497
SWAP_HITPOINTS -> {
9598
val temp = game.currentPlayer.health
9699
game.currentPlayer.health = player.health
97100
player.health = temp
98101
}
99-
// STEAL_CARD ->
102+
STEAL_CARD -> playEffect(DungeonMayhemPlayCard(game.currentPlayer, player,
103+
player.deck.random(scope.replayable, 1, "topCard") { it.name }.first()
104+
)).let { }
100105
PICK_UP_CARD -> player.discard[target.discardedCard!!].moveTo(player.hand)
101106
else -> throw IllegalStateException("No targets required for $this")
102107
}
@@ -130,6 +135,7 @@ class DungeonMayhemPlayer(val index: Int) {
130135
this.health = min(this.health + amount, 10)
131136
}
132137

138+
var protected: Boolean = false
133139
lateinit var color: String
134140
var health: Int = 10
135141
val deck = CardZone<DungeonMayhemCard>()
@@ -209,13 +215,24 @@ object DungeonMayhemDecks {
209215

210216
}
211217

218+
data class DungeonMayhemResolveSymbol(val player: DungeonMayhemPlayer, val symbol: DungeonMayhemSymbol)
212219
class DungeonMayhem(playerCount: Int, val config: DungeonMayhemConfig) {
213220
val players = (0 until playerCount).map { DungeonMayhemPlayer(it) }
214221
var currentPlayerIndex: Int = 0
215-
val symbolsToResolve = mutableListOf<DungeonMayhemSymbol>()
222+
val symbolsToResolve = mutableListOf<DungeonMayhemResolveSymbol>()
216223
val currentPlayer: DungeonMayhemPlayer get() = players[currentPlayerIndex]
224+
var attackedPlayer: Int? = null
217225
}
218226
data class DungeonMayhemTarget(val player: Int, val shieldCard: Int?, val discardedCard: Int?)
227+
data class DungeonMayhemPlayCard(val player: DungeonMayhemPlayer, val ownedByPlayer: DungeonMayhemPlayer, val card: Card<DungeonMayhemCard>)
228+
data class DungeonMayhemEffect(
229+
val game: DungeonMayhem,
230+
val byPlayer: DungeonMayhemPlayer,
231+
val cardOwner: DungeonMayhemPlayer,
232+
val symbol: DungeonMayhemSymbol,
233+
val count: Int,
234+
val target: DungeonMayhemTarget
235+
)
219236

220237
object DungeonMayhemDsl {
221238

@@ -231,20 +248,42 @@ object DungeonMayhemDsl {
231248
rules {
232249
allActions.requires { action.playerIndex == game.currentPlayerIndex }
233250
view("currentPlayer") { game.currentPlayerIndex }
251+
val newTurnDrawCard = trigger(Unit::class).effect {
252+
game.currentPlayer.drawCard(replayable, "turnStart", 1)
253+
game.currentPlayer.protected = false
254+
}
255+
val playTrigger = trigger(DungeonMayhemPlayCard::class)
256+
val effectTrigger = trigger(DungeonMayhemEffect::class).effect {
257+
trigger.symbol.resolve(game, this, playTrigger)
258+
}
259+
effectTrigger.map {
260+
if (trigger.symbol == DungeonMayhemSymbol.ATTACK) {
261+
val player = game.players[trigger.target.player]
262+
val health = player.shields.cards.getOrNull(trigger.target.shieldCard ?: -1)?.health
263+
if (health != null && health < trigger.count) trigger.copy(count = health) else trigger
264+
} else trigger
265+
}
266+
effectTrigger.ignoreEffectIf { game.players[trigger.target.player].protected }
267+
effectTrigger.after { (1..trigger.count).forEach { game.symbolsToResolve.remove(game.symbolsToResolve.first { it.symbol == trigger.symbol }) } }
268+
effectTrigger.after { if (game.symbolsToResolve.any { it.symbol == trigger.symbol }) game.attackedPlayer = trigger.target.player }
234269

235270
gameStart {
236271
// How to choose player decks? Before game as player options or first action in game?
237272
// just shuffle characters in the beginning (playing it like this for a while might make me more motivated for real solution later)
238273
val decks = listOf(DungeonMayhemDecks.blue(), DungeonMayhemDecks.purple(),
239274
DungeonMayhemDecks.red(), DungeonMayhemDecks.yellow()).shuffled()
240275
val deckStrings = replayable.strings("characters") { decks.map { it.first } }
276+
.let { listOf("purple", "yellow", "red", "blue") }
241277

242278
game.players.forEachIndexed { index, player ->
243279
player.color = deckStrings[index]
244280
player.deck.cards.addAll(decks.first { it.first == deckStrings[index] }.second)
245281
player.drawCard(replayable, "gameStart", 3)
246282
}
247-
game.players[0].drawCard(replayable, "turnStart", 1)
283+
newTurnDrawCard(Unit)
284+
285+
val pl = game.players.find { it.color == "purple" }!!
286+
pl.deck.let { it.card(it.cards.find { it.name == "Clever Disguise" }!!).moveTo(pl.hand) }
248287
}
249288
fun CardZone<DungeonMayhemCard>.view(): List<Map<String, Any>> {
250289
return this.cards.map {
@@ -268,40 +307,54 @@ object DungeonMayhemDsl {
268307
"shields" to it.shields.view()
269308
)}
270309
}
271-
view("stack") { game.symbolsToResolve }
310+
view("stack") { game.symbolsToResolve.map { it.symbol } }
272311

273312
action(play).options { game.currentPlayer.hand.cards }
274-
action(play).effect { game.symbolsToResolve.remove(DungeonMayhemSymbol.PLAY_AGAIN) }
275-
action(play).effect { game.symbolsToResolve.addAll(action.parameter.symbols) }
276-
action(play).effect {
277-
val shields = action.parameter.symbols.count { it == DungeonMayhemSymbol.SHIELD }
313+
action(play).effect { game.symbolsToResolve.remove(game.symbolsToResolve.firstOrNull { it.symbol == DungeonMayhemSymbol.PLAY_AGAIN }) }
314+
action(play).effect { playTrigger(DungeonMayhemPlayCard(game.currentPlayer, game.currentPlayer,
315+
game.currentPlayer.hand.card(action.parameter)))
316+
}
317+
playTrigger.effect { game.symbolsToResolve.addAll(trigger.card.card.symbols.map { DungeonMayhemResolveSymbol(trigger.ownedByPlayer, it) }) }
318+
playTrigger.effect {
319+
val shields = trigger.card.card.symbols.count { it == DungeonMayhemSymbol.SHIELD }
278320
if (shields > 0) {
279-
game.currentPlayer.shields.cards.add(
280-
DungeonMayhemShield(game.currentPlayer.discard, game.currentPlayer.hand.card(action.parameter).remove(), shields)
321+
trigger.player.shields.cards.add(
322+
DungeonMayhemShield(trigger.ownedByPlayer.discard, trigger.card.remove(), shields)
281323
)
282-
} else game.currentPlayer.hand.card(action.parameter).moveTo(game.currentPlayer.played)
324+
} else trigger.card.moveTo(trigger.ownedByPlayer.played)
283325
}
284-
action(play).after {
326+
playTrigger.after {
285327
// Auto-clear effects that does not need targets
286-
val autoResolve = game.symbolsToResolve.filter { it.availableTargets(game) == null }.groupBy { it }
328+
val autoResolve = game.symbolsToResolve.filter {symbol ->
329+
symbol.symbol.availableTargets(game).let { it == null || it.isEmpty() }
330+
}.groupBy { it }
287331
autoResolve.forEach {
288-
if (it.key.autoResolve(it.value.size, game, action.playerIndex, replayable)) {
332+
if (it.key.symbol.autoResolve(it.value.size, game, trigger.player.index, replayable)) {
289333
game.symbolsToResolve.removeAll(it.value)
290334
}
291335
}
292336
}
293-
action(target).options { game.symbolsToResolve.mapNotNull { it.availableTargets(game) }.firstOrNull() ?: emptyList() }
294-
action(target).forceUntil { game.symbolsToResolve.none { it.availableTargets(game) != null } }
337+
action(target).options { game.symbolsToResolve.mapNotNull { it.symbol.availableTargets(game) }.firstOrNull() ?: emptyList() }
338+
action(target).forceUntil { game.symbolsToResolve.none { it.symbol.availableTargets(game) != null } }
295339
action(target).effect {
296-
val symbol = game.symbolsToResolve.first { it.availableTargets(game) != null }
340+
val symbol = game.symbolsToResolve.first { it.symbol.availableTargets(game) != null }
297341
val count = game.symbolsToResolve.count { it == symbol }
298-
symbol.resolve(game, count, action.parameter)
299-
}
300-
action(target).after {
301-
val symbol = game.symbolsToResolve.first { it.availableTargets(game) != null }
302-
game.symbolsToResolve.removeAll { it == symbol }
342+
effectTrigger(DungeonMayhemEffect(game, game.players[action.playerIndex], symbol.player, symbol.symbol, count, action.parameter))
303343
}
344+
action(target).after { if (game.symbolsToResolve.none { it.symbol == DungeonMayhemSymbol.ATTACK }) game.attackedPlayer = null }
304345

346+
allActions.after {
347+
if (game.currentPlayer.hand.size == 0) {
348+
game.currentPlayer.drawCard(replayable, "empty-hand", 2)
349+
}
350+
}
351+
allActions.after {
352+
game.players.forEach { player ->
353+
player.shields.cards.filter { it.health <= 0 }.asSequence().forEach { shield ->
354+
player.shields.card(shield).let { it.card.destroy(it) }
355+
}
356+
}
357+
}
305358
allActions.after {
306359
val lost = game.players.filter { it.health <= 0 }
307360
.filter { player -> eliminations.eliminations().none { it.playerIndex == player.index } }
@@ -314,21 +367,18 @@ object DungeonMayhemDsl {
314367
allActions.after {
315368
if (game.symbolsToResolve.isEmpty()) {
316369
game.players.forEach { it.played.moveAllTo(it.discard) }
317-
game.currentPlayerIndex = game.currentPlayerIndex.next(game.players.size)
318-
game.currentPlayer.drawCard(replayable, "turnStart", 1)
370+
do {
371+
game.currentPlayerIndex = game.currentPlayerIndex.next(game.players.size)
372+
} while (eliminations.remainingPlayers().isNotEmpty() && !eliminations.remainingPlayers().contains(game.currentPlayerIndex))
373+
newTurnDrawCard(Unit)
319374
}
320375
}
321376
}
322377
}
323378
}
324379
/*
325380
Problems:
326-
- Attacking shields, should destroy or damage shield and then allow target next shield, etc.
327-
- set "attacked player" and only remove limited number of symbols
328-
- Protection
329-
- trigger system? and trigger prevention?
330381
- Tracing what's happening (effects and why actions are - not - allowed)
331382
- name each rule and log it?
332-
- Steal card and play it -- auto resolve, and equip as shield
333-
- trigger system?
383+
- Also send to frontend?
334384
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import net.zomis.games.dsl.*
44
import net.zomis.games.dsl.impl.GameSetupImpl
55
import net.zomis.games.dsl.sourcedest.ArtaxGame
66
import net.zomis.games.dsl.sourcedest.TTSourceDestinationGames
7+
import net.zomis.games.impl.DungeonMayhemDsl
78
import net.zomis.games.impl.HanabiGame
89
import net.zomis.games.impl.SetGame
910
import net.zomis.games.impl.SkullGame
@@ -20,6 +21,7 @@ object ServerGames {
2021
"Hanabi" to HanabiGame.game,
2122
"Set" to SetGame.game,
2223
"Skull" to SkullGame.game,
24+
"Dungeon Mayhem" to DungeonMayhemDsl.game,
2325
"Splendor" to DslSplendor.splendorGame,
2426
"DSL-TTT3D" to DslTTT3D().game,
2527
"DSL-UR" to DslUR().gameUR

games-vue-client/src/supportedGames.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ const supportedGames = {
133133
},
134134
component: TreeViewGame
135135
},
136+
"Dungeon Mayhem": {
137+
dsl: true,
138+
enabled: false,
139+
actions: {
140+
play: (index) => "play-" + index,
141+
target: (index) => "target-" + index
142+
},
143+
component: TreeViewGame
144+
},
136145
"Artax": {
137146
displayName: "Artax",
138147
dsl: gamejs.net.zomis.games.dsl.sourcedest.ArtaxGame.gameArtax,

0 commit comments

Comments
 (0)