From 32c69131873d91b223442d2058469bf5444e5797 Mon Sep 17 00:00:00 2001 From: James Daly Date: Wed, 29 May 2019 22:20:47 -0400 Subject: [PATCH] Moved move activation from the game engine to the move itself and added the ability to switch out --- FakeMon/src/mon/Game.scala | 4 +- FakeMon/src/mon/battle/Action.scala | 2 +- FakeMon/src/mon/battle/BattleEngine.scala | 126 +----------- FakeMon/src/mon/stat/MonsterPtr.scala | 6 + FakeMon/src/mon/stat/Move.scala | 225 +++++++++++++++++----- FakeMon/src/mon/stat/Party.scala | 46 +++-- FakeMon/src/mon/stat/package.scala | 6 + 7 files changed, 237 insertions(+), 178 deletions(-) create mode 100644 FakeMon/src/mon/stat/MonsterPtr.scala diff --git a/FakeMon/src/mon/Game.scala b/FakeMon/src/mon/Game.scala index 095f90d..e2c1d27 100644 --- a/FakeMon/src/mon/Game.scala +++ b/FakeMon/src/mon/Game.scala @@ -23,8 +23,8 @@ object Game { val movepool2 = IndexedSeq(Move("Absorb")) val p1 = TrainerID("Jaeda", Gender.Female, 0) val p2 = TrainerID("Wild Monster", Gender.Male, 0) - val party1 = new Party(p1, new Monster(new StorageMon("Allied Mon", Gene.randomGene(null, form1), form1, Statistic.emptyEvs, movepool1)), IndexedSeq()) - val party2 = new Party(p2, new Monster(new StorageMon("Wild Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2)), IndexedSeq( + val party1 = new Party(p1, new MonsterPtr(new Monster(new StorageMon("Allied Mon", Gene.randomGene(null, form1), form1, Statistic.emptyEvs, movepool1))), IndexedSeq()) + val party2 = new Party(p2, new MonsterPtr(new Monster(new StorageMon("Wild Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2))), IndexedSeq( new Monster(new StorageMon("Sideboard Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2)) )) val engine = new BattleEngine(party1, party2) diff --git a/FakeMon/src/mon/battle/Action.scala b/FakeMon/src/mon/battle/Action.scala index 73fdf61..04281e2 100644 --- a/FakeMon/src/mon/battle/Action.scala +++ b/FakeMon/src/mon/battle/Action.scala @@ -3,7 +3,7 @@ package mon.battle import mon.stat._ import mon.stat.Statistic.Speed -case class Action(user : Monster, move : Move, target : Monster) extends Comparable[Action] { +case class Action(user : MonsterPtr, move : MoveTurn, target : MonsterPtr) extends Comparable[Action] { override def compareTo(other : Action) = { if (move.prior == other.move.prior) { other.user(Speed) compareTo user(Speed) diff --git a/FakeMon/src/mon/battle/BattleEngine.scala b/FakeMon/src/mon/battle/BattleEngine.scala index c5f520b..c7fc44c 100644 --- a/FakeMon/src/mon/battle/BattleEngine.scala +++ b/FakeMon/src/mon/battle/BattleEngine.scala @@ -15,11 +15,11 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random } def playTurn() = { - val playerMove = player.pollAction - val playerTarget = player.pollTarget(playerMove, player.lead, enemy) - val enemyMove = enemy.pollAction - val enemyTarget = enemy.pollTarget(enemyMove, enemy.lead, player) - val actions = Seq(Action(player.lead, playerMove, playerTarget), Action(enemy.lead, enemyMove, enemyTarget)) + val playerAction = player.pollAction(enemy) + val enemyAction = enemy.pollAction(player) + println(enemyAction) + + val actions = Seq(playerAction, enemyAction) val queue = rng.shuffle(actions).sorted // Shuffle to randomize in the event of a tie queue.foreach(useMove) val eotQueue = Seq(player.lead, enemy.lead).sortBy(_(Speed)) @@ -28,7 +28,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random if (!player.lead.isAlive) { if (player.canFight) { val replace = player.pollReplacement(enemy) - player.swapIn(replace) + player.switchIn(replace) } else { println(s"${player.trainer} has lost!") } @@ -36,7 +36,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random if (!enemy.lead.isAlive) { if (enemy.canFight) { val replace = enemy.pollReplacement(player) - enemy.swapIn(replace) + enemy.switchIn(replace) } else { println(s"${enemy.trainer} has lost!") } @@ -51,117 +51,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random val move = action.move val target = action.target if (user.isAlive) { - println(s"$user used $move.") - if (attackRoll(user, move, target)) { - if (move.pow > 0 || move.powCallback != null) { - val dmg = damageRoll(user, move, target) - val actualDmg = target.takeDamage(dmg) - if (dmg == actualDmg) { - println(s"$target takes $actualDmg damage!") - } else { - println(s"$target takes $actualDmg (+${dmg - actualDmg} overkill) damage!") - } - if (move.drain > 0.frac) { - val healing = Math.max(move.drain * actualDmg, 1) - user.recoverDamage(healing) - println(s"$user recovered $healing damage.") - } else if (move.drain < 0.frac) { - val recoil = Math.max(-move.drain * actualDmg, 1) - user.takeDamage(recoil) - println(s"$user took $recoil damage from recoil!") - } - if (!user.isAlive) { - println(s"$user fainted!") - } - if (!target.isAlive) { - println(s"$target fainted!") - } - } - applyBoosts(target, move.boosts) - applyStatus(target, move.status) - if (move.selfEffect != null) { - applyEffect(user, move.selfEffect) - } - if (move.secondary != null && rng.chance(move.secondary.chance.%%)) { - applyEffect(target, move.secondary) - } - - // TODO : Support moves - // TODO : Multiparty - } else { - println("Missed!") - } - } - } - - def attackRoll(user: Monster, move: Move, target: Monster) = { - if (move.accuracy == 0) { - true - } else { - val acc = user.boosts.getOrElse(Accuracy, 0) - target.boosts.getOrElse(Evasion, 0) - val mod = if (acc > 0) Fraction(3 + acc, 3) else Fraction(3, 3 - acc) - val chance = move.accuracy.%% * mod - rng.chance(chance) - } - } - - def damageRoll(user: Monster, move: Move, target: Monster) = { - val atkStat = if (move.mvType == MoveType.Physical) PAtk else MAtk - val defStat = if (move.mvType == MoveType.Physical) PDef else MDef - // TODO : Fixed damage - val pow = if (move.powCallback != null) move.powCallback(user, target) else move.pow - val baseDmg = (2 * user.level / 5 + 2) * pow * user(atkStat) / (target(defStat) * 50) + 2 - val effectiveness = target.effectiveness(move.element) - val multiplier = effectiveness * critMultiplier(user, move, target) - if (effectiveness > 1.0) { - println("It's super effective!") - } else if (effectiveness < 1.0) { - println("It's not very effective.") - } - val maxDmg = (baseDmg * multiplier).toInt - val minDmg = (17 \ 20) * maxDmg - rng.nextInt(minDmg, maxDmg) - } - - def critMultiplier(user : Monster, move : Move, target: Monster) = { - // Percentage chance is different from Pokemon - val stage = move.crit - val chance = (1 << stage) \ 16 // Doubles per stage - if (rng.chance(chance)) { - println("A critical hit!") - 1.5 - } else { - 1.0 - } - } - - def applyEffect(target: Monster, effect: Secondary) { - applyBoosts(target, effect.boosts) - applyStatus(target, effect.status) - } - - def applyStatus(target: Monster, status: Status) { - if (target.isAlive) { - if (status != null && target.status == None) { - // apply status - target.status = Some(status) - status.onStart.map(_(target)) - } - } - } - - def applyBoosts(target: Monster, boosts: Map[Stat, Int]) { - if (target.isAlive) { - boosts.foreach { - case (s, b) => { - target.applyBoost(s, b) - if (b > 0) { - println(s"$target's $s rose!") - } else if (b < 0) { - println(s"$target's $s fell!") - } - } - } + move.useMove(user, target) } } } \ No newline at end of file diff --git a/FakeMon/src/mon/stat/MonsterPtr.scala b/FakeMon/src/mon/stat/MonsterPtr.scala new file mode 100644 index 0000000..ca81d34 --- /dev/null +++ b/FakeMon/src/mon/stat/MonsterPtr.scala @@ -0,0 +1,6 @@ +package mon.stat + +class MonsterPtr(var mon : Monster) { + + override def toString : String = mon.toString() +} \ No newline at end of file diff --git a/FakeMon/src/mon/stat/Move.scala b/FakeMon/src/mon/stat/Move.scala index 876f839..e0fd421 100644 --- a/FakeMon/src/mon/stat/Move.scala +++ b/FakeMon/src/mon/stat/Move.scala @@ -6,7 +6,9 @@ import scala.tools.reflect.ToolBox import com.fasterxml.jackson.module.scala.JsonScalaEnumeration import scala.io.Source +import scala.util.Random +import Statistic._ import mon.util._ object MoveType extends Enumeration { @@ -19,55 +21,190 @@ object Target extends Enumeration { } class TargetType extends TypeReference[Target.type] -abstract class Move { - val name : String - val desc : String - val mvType : MoveType - val pow : Int - val powCallback : (Monster, Monster) => Int - val prior : Int - val accuracy : Int - val pp : Int - val element : Element - val flags : Set[String] - val target : Target - val boosts : Map[Stat, Int] - val crit : Int - val status : Status - val drain : Fraction - val selfEffect : Secondary +trait MoveTurn { + def name: String + def prior: Int + def target: Target + def useMove(user: Monster, target: Monster)(implicit rng: Random): Unit +} + +class SwitchOut(val party: Party, val replacement: Monster) extends MoveTurn { + def name = "Swap" + def prior = +6 + def target = Target.Self + + def useMove(user: Monster, target: Monster)(implicit rng: Random): Unit = { + println(s"${party.trainer} withdrew $user!") + party.switchIn(replacement) + println(s"${party.trainer} sent out $replacement") + } + + override def toString: String = s"Swap -> $replacement" +} + +abstract class Move extends MoveTurn { + val name: String + val desc: String + val mvType: MoveType + val pow: Int + val powCallback: (Monster, Monster) => Int + val prior: Int + val accuracy: Int + val pp: Int + val element: Element + val flags: Set[String] + val target: Target + val boosts: Map[Stat, Int] + val crit: Int + val status: Status + val drain: Fraction + val selfEffect: Secondary val secondary: Secondary // boosts // onHit // onTryHit - + // zPower, zMoveEffect, zMoveBoost - + + override def useMove(user: Monster, target: Monster)(implicit rng: Random) = { + println(s"$user used $name.") + if (attackRoll(user, target)) { + if (pow > 0 || powCallback != null) { + val dmg = damageRoll(user, target) + val actualDmg = target.takeDamage(dmg) + if (dmg == actualDmg) { + println(s"$target takes $actualDmg damage!") + } else { + println(s"$target takes $actualDmg (+${dmg - actualDmg} overkill) damage!") + } + if (drain > 0.frac) { + val healing = Math.max(drain * actualDmg, 1) + user.recoverDamage(healing) + println(s"$user recovered $healing damage.") + } else if (drain < 0.frac) { + val recoil = Math.max(-drain * actualDmg, 1) + user.takeDamage(recoil) + println(s"$user took $recoil damage from recoil!") + } + if (!user.isAlive) { + println(s"$user fainted!") + } + if (!target.isAlive) { + println(s"$target fainted!") + } + } + applyBoosts(target, boosts) + applyStatus(target, status) + if (selfEffect != null) { + applyEffect(user, selfEffect) + } + if (secondary != null && rng.chance(secondary.chance.%%)) { + applyEffect(target, secondary) + } + + // TODO : Support moves + // TODO : Multiparty + } else { + println("Missed!") + } + } + + def attackRoll(user: Monster, target: Monster)(implicit rng: Random) = { + if (accuracy == 0) { + true + } else { + val acc = user.boosts.getOrElse(Accuracy, 0) - target.boosts.getOrElse(Evasion, 0) + val mod = if (acc > 0) Fraction(3 + acc, 3) else Fraction(3, 3 - acc) + val chance = accuracy.%% * mod + rng.chance(chance) + } + } + + def damageRoll(user: Monster, target: Monster)(implicit rng: Random) = { + val atkStat = if (mvType == MoveType.Physical) PAtk else MAtk + val defStat = if (mvType == MoveType.Physical) PDef else MDef + // TODO : Fixed damage + val actualPow = if (powCallback != null) powCallback(user, target) else pow + val baseDmg = (2 * user.level / 5 + 2) * actualPow * user(atkStat) / (target(defStat) * 50) + 2 + val effectiveness = target.effectiveness(element) + val multiplier = effectiveness * critMultiplier(user, target) + if (effectiveness > 1.0) { + println("It's super effective!") + } else if (effectiveness < 1.0) { + println("It's not very effective.") + } + val maxDmg = (baseDmg * multiplier).toInt + val minDmg = (17 \ 20) * maxDmg + rng.nextInt(minDmg, maxDmg) + } + + def critMultiplier(user: Monster, target: Monster)(implicit rng: Random) = { + // Percentage chance is different from Pokemon + val stage = crit + val chance = (1 << stage) \ 16 // Doubles per stage + if (rng.chance(chance)) { + println("A critical hit!") + 1.5 + } else { + 1.0 + } + } + + def applyEffect(target: Monster, effect: Secondary) { + applyBoosts(target, effect.boosts) + applyStatus(target, effect.status) + } + + def applyStatus(target: Monster, status: Status) { + if (target.isAlive) { + if (status != null && target.status == None) { + // apply status + target.status = Some(status) + status.onStart.map(_(target)) + } + } + } + + def applyBoosts(target: Monster, boosts: Map[Stat, Int]) { + if (target.isAlive) { + boosts.foreach { + case (s, b) => { + target.applyBoost(s, b) + if (b > 0) { + println(s"$target's $s rose!") + } else if (b < 0) { + println(s"$target's $s fell!") + } + } + } + } + } + override def toString = { name } } case class MoveToken( - val name : String, - val shortDesc : String, - @JsonScalaEnumeration(classOf[MoveTypeType]) val category : MoveType, - val basePower : Option[Int], - val basePowerCallback : String, - val priority : Int, - val accuracy : Option[Int], - val pp : Int, - val `type` : String, - val flags : Map[String, Int], - val status : String, - val self : SecondaryToken, - val secondary : SecondaryToken, - val drain : Fraction, - val recoil : Fraction, - @JsonScalaEnumeration(classOf[TargetType]) val target : Target = Target.Normal, - val boosts : Map[String, Int] = Map(), - val critRatio : Int = 0) { - + val name: String, + val shortDesc: String, + @JsonScalaEnumeration(classOf[MoveTypeType]) val category: MoveType, + val basePower: Option[Int], + val basePowerCallback: String, + val priority: Int, + val accuracy: Option[Int], + val pp: Int, + val `type`: String, + val flags: Map[String, Int], + val status: String, + val self: SecondaryToken, + val secondary: SecondaryToken, + val drain: Fraction, + val recoil: Fraction, + @JsonScalaEnumeration(classOf[TargetType]) val target: Target = Target.Normal, + val boosts: Map[String, Int] = Map(), + val critRatio: Int = 0) { + def instantiate() = { val token = this new Move { @@ -82,7 +219,7 @@ case class MoveToken( val accuracy = token.accuracy.getOrElse(100) val flags = token.flags.keySet val target = token.target - val boosts = if (token.boosts != null) token.boosts.map{case (s, i) => (Statistic(s), i)} else Map() + val boosts = if (token.boosts != null) token.boosts.map { case (s, i) => (Statistic(s), i) } else Map() val crit = token.critRatio val status = if (token.status != null) Status(token.status) else null val drain = if (token.drain != null) token.drain else if (token.recoil != null) -token.recoil else 0 \ 1 @@ -95,14 +232,14 @@ case class MoveToken( object Move { private var moves = Map[String, Move]() val tokens = YamlHelper.extractSeq[MoveToken](Source.fromInputStream(Move.getClass.getResourceAsStream("data/moves.yaml"))).map(t => (t.name, t)).toMap - - def apply(name : String) : Move = { + + def apply(name: String): Move = { if (!moves.contains(name)) { moves = moves.updated(name, tokens(name).instantiate()) } moves(name) } - + def compilePowCallback(code: String): (Monster, Monster) => Int = { if (code != null) { val tb = runtimeMirror(getClass.getClassLoader).mkToolBox() @@ -117,11 +254,11 @@ object Move { """.stripMargin) val f = tb.compile(tree) val wrapper = f() - + wrapper.asInstanceOf[(Monster, Monster) => Int] } else { null } - + } } \ No newline at end of file diff --git a/FakeMon/src/mon/stat/Party.scala b/FakeMon/src/mon/stat/Party.scala index 8f57e1f..6196d82 100644 --- a/FakeMon/src/mon/stat/Party.scala +++ b/FakeMon/src/mon/stat/Party.scala @@ -2,30 +2,50 @@ package mon.stat import scala.util.Random -import mon.battle.rngDice +import mon.battle.Action import mon.stat.Target._ -class Party(val trainer : TrainerID, var lead : Monster, var sideboard : IndexedSeq[Monster]) { - def pollAction(implicit rng : Random) : Move = rng.pick(lead.base.moves) - def pollTarget(move : Move, user : Monster, them: Party)(implicit rng : Random) : Monster = { +class Party(val trainer: TrainerID, val lead: MonsterPtr, var sideboard: IndexedSeq[Monster]) { + def pollAction(them: Party)(implicit rng: Random): Action = { + if (hasBackup && shouldSwitch(them)) { + val sub = pollReplacement(them: Party) + val move = new SwitchOut(this, sub) + Action(lead, move, lead) + } else { + val move = pollMove + val target = pollTarget(move, lead, them) + Action(lead, move, target) + } + } + + def pollMove(implicit rng: Random): MoveTurn = rng.pick(lead.base.moves) + def pollTarget(move: MoveTurn, user: MonsterPtr, them: Party)(implicit rng: Random): MonsterPtr = { if (move.target == Self) { user } else { them.lead } } - - def pollReplacement(them : Party)(implicit rng : Random) : Monster = { + + def pollReplacement(them: Party)(implicit rng: Random): Monster = { sideboard.filter(_.isAlive).head } - - def swapIn(mon : Monster) = { + + def switchIn(mon: Monster) = { val index = sideboard.indexOf(mon) - sideboard = sideboard.updated(index, lead) - lead = mon + sideboard = sideboard.updated(index, lead.mon) + lead.mon = mon } - - def canFight : Boolean = { - lead.isAlive || sideboard.exists(_.isAlive) + + def shouldSwitch(them: Party)(implicit rng: Random): Boolean = { + true + } + + def canFight: Boolean = { + lead.isAlive || hasBackup + } + + def hasBackup: Boolean = { + sideboard.exists(_.isAlive) } } \ No newline at end of file diff --git a/FakeMon/src/mon/stat/package.scala b/FakeMon/src/mon/stat/package.scala index fed4240..07e8d6b 100644 --- a/FakeMon/src/mon/stat/package.scala +++ b/FakeMon/src/mon/stat/package.scala @@ -3,6 +3,10 @@ package mon import org.json4s.DefaultFormats import org.json4s.ext.EnumNameSerializer +import scala.util.Random + +import mon.util.Dice + package object stat { type Stat = Statistic.Value type MoveType = MoveType.Value @@ -10,4 +14,6 @@ package object stat { type Target = Target.Value implicit val formats = DefaultFormats + new EnumNameSerializer(MoveType) + new EnumNameSerializer(Gender) + implicit def rngDice(rng : Random) = new Dice(rng) + implicit def ptrToMonster(ptr : MonsterPtr) : Monster = ptr.mon } \ No newline at end of file