|
| 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