Skip to content

Commit 6dd59ec

Browse files
committed
Add a fair amount of Dungeon Mayhem implementation
1 parent c0ec2c1 commit 6dd59ec

File tree

1 file changed

+334
-0
lines changed

1 file changed

+334
-0
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
package net.zomis.games.impl
2+
3+
import net.zomis.games.WinResult
4+
import net.zomis.games.cards.Card
5+
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
10+
import net.zomis.games.dsl.sourcedest.next
11+
import kotlin.math.min
12+
13+
data class DungeonMayhemConfig(
14+
val drawNewImmediately: Boolean = false,
15+
val pickPocketIgnoresCleverDisguise: Boolean = true
16+
)
17+
private infix fun String.card(symbol: DungeonMayhemSymbol): DungeonMayhemCard {
18+
return this card listOf(symbol)
19+
}
20+
private infix fun String.card(symbols: List<DungeonMayhemSymbol>): DungeonMayhemCard {
21+
return DungeonMayhemCard(this, symbols)
22+
}
23+
enum class DungeonMayhemSymbol {
24+
ATTACK, // target shields first, then players
25+
HEAL,
26+
PLAY_AGAIN,
27+
DRAW,
28+
SHIELD,
29+
30+
// Specials - Yellow
31+
FIREBALL, // target shields first? auto-resolve?
32+
STEAL_SHIELD, // target shield if available
33+
SWAP_HITPOINTS, // target player
34+
35+
// Specials - Red
36+
PICK_UP_CARD, // target discarded card
37+
DESTROY_ALL_SHIELDS,
38+
39+
// Specials - Purple
40+
PROTECTION_ONE_TURN,
41+
DESTROY_SINGLE_SHIELD, // target shield if available
42+
STEAL_CARD, // target player
43+
44+
// Specials - Blue
45+
HEAL_AND_ATTACK_FOR_EACH_OPPONENT,
46+
ALL_DISCARD_AND_DRAW,
47+
;
48+
49+
operator fun plus(other: DungeonMayhemSymbol): List<DungeonMayhemSymbol> = listOf(this, other)
50+
operator fun times(count: Int): List<DungeonMayhemSymbol> = (1..count).map { this }
51+
fun availableTargets(game: DungeonMayhem): List<DungeonMayhemTarget>? {
52+
return when (this) {
53+
ATTACK -> game.players.minus(game.currentPlayer).flatMap { player ->
54+
if (player.shields.size == 0) listOf(DungeonMayhemTarget(player.index, null, null))
55+
else player.shields.indices.map { DungeonMayhemTarget(player.index, it, null) }
56+
}
57+
DESTROY_SINGLE_SHIELD, STEAL_SHIELD -> game.players.flatMap { player -> player.shields.indices.map { player.index to it } }.map {
58+
DungeonMayhemTarget(it.first, it.second, null)
59+
}
60+
SWAP_HITPOINTS, STEAL_CARD -> game.players.minus(game.currentPlayer).map { DungeonMayhemTarget(it.index, null, null) }
61+
PICK_UP_CARD -> game.currentPlayer.discard.cards.mapIndexed { index, _ -> DungeonMayhemTarget(game.currentPlayerIndex, null, index) }
62+
else -> null
63+
}
64+
}
65+
66+
fun autoResolve(count: Int, game: DungeonMayhem, playerIndex: Int, replayable: ReplayableScope): Boolean {
67+
val player = game.players[playerIndex]
68+
val targets = this.availableTargets(game)
69+
if (targets?.isEmpty() == true) return true
70+
when (this) {
71+
HEAL -> player.health = min(player.health + count, 10)
72+
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 ->
76+
HEAL_AND_ATTACK_FOR_EACH_OPPONENT -> {
77+
val opponents = game.players.minus(player)
78+
player.heal(opponents.size)
79+
opponents.forEach { it.damage(1) }
80+
}
81+
ALL_DISCARD_AND_DRAW -> repeat(count) { game.players.forEach { it.hand.moveAllTo(it.discard); it.drawCard(replayable, "allDraw", 3) } }
82+
SHIELD -> return true
83+
else -> return false
84+
}
85+
return true
86+
}
87+
88+
fun resolve(game: DungeonMayhem, count: Int, target: DungeonMayhemTarget) {
89+
val player: DungeonMayhemPlayer = game.players[target.player]
90+
when (this) {
91+
ATTACK -> player.damage(count)
92+
DESTROY_SINGLE_SHIELD -> player.shields[target.shieldCard!!].let { it.card.destroy(it) }
93+
STEAL_SHIELD -> player.shields[target.shieldCard!!].moveTo(game.currentPlayer.shields)
94+
SWAP_HITPOINTS -> {
95+
val temp = game.currentPlayer.health
96+
game.currentPlayer.health = player.health
97+
player.health = temp
98+
}
99+
// STEAL_CARD ->
100+
PICK_UP_CARD -> player.discard[target.discardedCard!!].moveTo(player.hand)
101+
else -> throw IllegalStateException("No targets required for $this")
102+
}
103+
}
104+
}
105+
class DungeonMayhemCard(val name: String, val symbols: List<DungeonMayhemSymbol>)
106+
class DungeonMayhemShield(val discard: CardZone<DungeonMayhemCard>, val card: DungeonMayhemCard, var health: Int) {
107+
fun destroy(card: Card<DungeonMayhemShield>) {
108+
discard.cards.add(card.remove().card)
109+
}
110+
}
111+
112+
class DungeonMayhemPlayer(val index: Int) {
113+
fun drawCard(replayable: ReplayableScope, keyPrefix: String, count: Int) {
114+
val state = replayable.strings("$keyPrefix-$index") {
115+
val beforeReshuffle = min(deck.size, count)
116+
val fromDeck = deck.cards.shuffled().take(beforeReshuffle)
117+
val fromDiscard = discard.cards.shuffled().take(count - beforeReshuffle)
118+
(fromDeck + fromDiscard).map { it.name }
119+
}
120+
repeat(count) {iteration ->
121+
if (deck.size == 0) discard.moveAllTo(deck)
122+
deck.card(deck.cards.first { it.name == state[iteration] }).moveTo(hand)
123+
}
124+
}
125+
126+
fun damage(amount: Int) {
127+
this.health = this.health - amount
128+
}
129+
fun heal(amount: Int) {
130+
this.health = min(this.health + amount, 10)
131+
}
132+
133+
lateinit var color: String
134+
var health: Int = 10
135+
val deck = CardZone<DungeonMayhemCard>()
136+
val hand = CardZone<DungeonMayhemCard>()
137+
val shields = CardZone<DungeonMayhemShield>()
138+
val played = CardZone<DungeonMayhemCard>()
139+
val discard = CardZone<DungeonMayhemCard>()
140+
}
141+
142+
private typealias s = DungeonMayhemSymbol
143+
private operator fun Int.times(card: DungeonMayhemCard): List<DungeonMayhemCard> = (1..this).map { card }
144+
object DungeonMayhemDecks {
145+
146+
fun yellow() = "yellow" to listOf(
147+
2 * ("Fireball" card s.FIREBALL),
148+
2 * ("Evil Sneer" card s.HEAL + s.PLAY_AGAIN),
149+
3 * ("Speed of Thought" card s.PLAY_AGAIN * 2),
150+
2 * ("Charm" card s.STEAL_SHIELD),
151+
3 * ("Magic Missile" card s.ATTACK + s.PLAY_AGAIN),
152+
3 * ("Burning Hands" card s.ATTACK * 2),
153+
3 * ("Knowledge Is Power" card s.DRAW * 3),
154+
2 * ("Shield" card s.SHIELD + s.DRAW),
155+
1 * ("Mirror Image" card s.SHIELD * 3),
156+
1 * ("Stone Skin" card s.SHIELD * 2),
157+
4 * ("Lightning Bolt" card s.ATTACK * 3),
158+
2 * ("Vampiric Touch" card s.SWAP_HITPOINTS)
159+
).flatten()
160+
161+
fun red() = "red" to listOf(
162+
2 * ("For The Most Justice" card s.ATTACK * 3),
163+
2 * ("Divine Inspiration" card s.HEAL * 2 + s.PICK_UP_CARD),
164+
1 * ("Divine Smite" card s.ATTACK * 3 + s.HEAL),
165+
4 * ("For Even More Justice" card s.ATTACK * 2),
166+
2 * ("Spinning Parry" card s.SHIELD + s.DRAW),
167+
3 * ("Fighting Words" card s.ATTACK * 2 + s.HEAL),
168+
2 * ("High Carisma" card s.DRAW * 2),
169+
1 * ("Fluffy" card s.SHIELD * 2),
170+
1 * ("Cure Wounds" card s.DRAW * 2 + s.HEAL),
171+
2 * ("Finger-Wag of Judgement" card s.PLAY_AGAIN * 2),
172+
2 * ("Divine Shield" card s.SHIELD * 3),
173+
3 * ("For Justice" card s.ATTACK + s.PLAY_AGAIN),
174+
3 * ("Banishing Smite" card s.DESTROY_ALL_SHIELDS + s.PLAY_AGAIN)
175+
).flatten()
176+
177+
fun purple() = "purple" to listOf(
178+
5 * ("One Thrown Dagger" card s.ATTACK + s.PLAY_AGAIN),
179+
3 * ("All The Thrown Daggers" card s.ATTACK * 3),
180+
2 * ("Winged Serpent" card s.SHIELD + s.DRAW),
181+
1 * ("My Little Friend" card s.SHIELD * 3),
182+
4 * ("Two Thrown Daggers" card s.ATTACK * 2),
183+
2 * ("The Goon Squad" card s.SHIELD * 2),
184+
2 * ("Stolen Potion" card s.HEAL + s.PLAY_AGAIN),
185+
2 * ("Cunning Action" card s.PLAY_AGAIN * 2),
186+
2 * ("Clever Disguise" card s.PROTECTION_ONE_TURN),
187+
2 * ("Sneak Attack" card s.DESTROY_SINGLE_SHIELD + s.PLAY_AGAIN),
188+
2 * ("Pick Pocket" card s.STEAL_CARD),
189+
1 * ("Even More Daggers" card s.DRAW * 2 + s.HEAL)
190+
).flatten()
191+
192+
fun blue() = "blue" to listOf(
193+
5 * ("Big Axe Is The Best Axe" card s.ATTACK * 3),
194+
2 * ("Brutal Punch" card s.ATTACK * 2),
195+
1 * ("Riff" card s.SHIELD * 3),
196+
1 * ("Raff" card s.SHIELD * 3),
197+
1 * ("Snack Time" card s.DRAW * 2 + s.HEAL),
198+
2 * ("Flex!" card s.HEAL + s.DRAW),
199+
2 * ("Whirling Axes" card s.HEAL_AND_ATTACK_FOR_EACH_OPPONENT),
200+
2 * ("Head Butt" card s.ATTACK + s.PLAY_AGAIN),
201+
2 * ("Rage" card s.ATTACK * 4),
202+
2 * ("Open The Armory" card s.DRAW * 2),
203+
1 * ("Spiked Shield" card s.SHIELD * 2),
204+
1 * ("Bag of Rats" card s.SHIELD + s.DRAW),
205+
2 * ("Two Axes Are Better Than One" card s.PLAY_AGAIN * 2),
206+
2 * ("Mighty Toss" card s.DESTROY_SINGLE_SHIELD + s.DRAW),
207+
2 * ("Battle Roar" card s.ALL_DISCARD_AND_DRAW + s.PLAY_AGAIN)
208+
).flatten()
209+
210+
}
211+
212+
class DungeonMayhem(playerCount: Int, val config: DungeonMayhemConfig) {
213+
val players = (0 until playerCount).map { DungeonMayhemPlayer(it) }
214+
var currentPlayerIndex: Int = 0
215+
val symbolsToResolve = mutableListOf<DungeonMayhemSymbol>()
216+
val currentPlayer: DungeonMayhemPlayer get() = players[currentPlayerIndex]
217+
}
218+
data class DungeonMayhemTarget(val player: Int, val shieldCard: Int?, val discardedCard: Int?)
219+
220+
object DungeonMayhemDsl {
221+
222+
val play = createActionType("play", DungeonMayhemCard::class, ActionSerialization<DungeonMayhemCard, DungeonMayhem>({ it.name }, { key -> game.currentPlayer.hand.cards.first { it.name == key } }))
223+
val target = createActionType("target", DungeonMayhemTarget::class)
224+
val game = createGame<DungeonMayhem>("Dungeon Mayhem") {
225+
setup(DungeonMayhemConfig::class) {
226+
players(2..4)
227+
defaultConfig { DungeonMayhemConfig() }
228+
init { DungeonMayhem(playerCount, config) }
229+
}
230+
231+
rules {
232+
allActions.requires { action.playerIndex == game.currentPlayerIndex }
233+
view("currentPlayer") { game.currentPlayerIndex }
234+
235+
gameStart {
236+
// How to choose player decks? Before game as player options or first action in game?
237+
// just shuffle characters in the beginning (playing it like this for a while might make me more motivated for real solution later)
238+
val decks = listOf(DungeonMayhemDecks.blue(), DungeonMayhemDecks.purple(),
239+
DungeonMayhemDecks.red(), DungeonMayhemDecks.yellow()).shuffled()
240+
val deckStrings = replayable.strings("characters") { decks.map { it.first } }
241+
242+
game.players.forEachIndexed { index, player ->
243+
player.color = deckStrings[index]
244+
player.deck.cards.addAll(decks.first { it.first == deckStrings[index] }.second)
245+
player.drawCard(replayable, "gameStart", 3)
246+
}
247+
game.players[0].drawCard(replayable, "turnStart", 1)
248+
}
249+
fun CardZone<DungeonMayhemCard>.view(): List<Map<String, Any>> {
250+
return this.cards.map {
251+
mapOf("name" to it.name, "symbols" to it.symbols)
252+
}
253+
}
254+
fun CardZone<DungeonMayhemShield>.view(): List<Map<String, Any>> {
255+
return this.cards.map {
256+
mapOf("name" to it.card.name, "health" to it.health)
257+
}
258+
}
259+
260+
view("players") {
261+
game.players.map { mapOf(
262+
"color" to it.color,
263+
"health" to it.health,
264+
"deck" to it.deck.size,
265+
"hand" to if (viewer == it.index) it.hand.view() else it.hand.size,
266+
"discard" to it.discard.view(),
267+
"played" to it.played.view(),
268+
"shields" to it.shields.view()
269+
)}
270+
}
271+
view("stack") { game.symbolsToResolve }
272+
273+
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 }
278+
if (shields > 0) {
279+
game.currentPlayer.shields.cards.add(
280+
DungeonMayhemShield(game.currentPlayer.discard, game.currentPlayer.hand.card(action.parameter).remove(), shields)
281+
)
282+
} else game.currentPlayer.hand.card(action.parameter).moveTo(game.currentPlayer.played)
283+
}
284+
action(play).after {
285+
// Auto-clear effects that does not need targets
286+
val autoResolve = game.symbolsToResolve.filter { it.availableTargets(game) == null }.groupBy { it }
287+
autoResolve.forEach {
288+
if (it.key.autoResolve(it.value.size, game, action.playerIndex, replayable)) {
289+
game.symbolsToResolve.removeAll(it.value)
290+
}
291+
}
292+
}
293+
action(target).options { game.symbolsToResolve.mapNotNull { it.availableTargets(game) }.firstOrNull() ?: emptyList() }
294+
action(target).forceUntil { game.symbolsToResolve.none { it.availableTargets(game) != null } }
295+
action(target).effect {
296+
val symbol = game.symbolsToResolve.first { it.availableTargets(game) != null }
297+
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 }
303+
}
304+
305+
allActions.after {
306+
val lost = game.players.filter { it.health <= 0 }
307+
.filter { player -> eliminations.eliminations().none { it.playerIndex == player.index } }
308+
eliminations.eliminateMany(lost.map { it.index }, WinResult.LOSS)
309+
}
310+
allActions.after {
311+
val remaining = game.players.filter { it.health > 0 }
312+
if (remaining.size == 1) eliminations.eliminateRemaining(WinResult.WIN)
313+
}
314+
allActions.after {
315+
if (game.symbolsToResolve.isEmpty()) {
316+
game.players.forEach { it.played.moveAllTo(it.discard) }
317+
game.currentPlayerIndex = game.currentPlayerIndex.next(game.players.size)
318+
game.currentPlayer.drawCard(replayable, "turnStart", 1)
319+
}
320+
}
321+
}
322+
}
323+
}
324+
/*
325+
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?
330+
- Tracing what's happening (effects and why actions are - not - allowed)
331+
- name each rule and log it?
332+
- Steal card and play it -- auto resolve, and equip as shield
333+
- trigger system?
334+
*/

0 commit comments

Comments
 (0)