BattleSim/test/bw/battle_engine.coffee

568 lines
20 KiB
CoffeeScript

require '../helpers'
{Battle} = require('../../server/bw/battle')
{Pokemon} = require('../../server/bw/pokemon')
{Status, Attachment} = require('../../server/bw/attachment')
{Conditions} = require '../../shared/conditions'
{Factory} = require '../factory'
should = require 'should'
shared = require '../shared'
{Protocol} = require '../../shared/protocol'
describe 'Mechanics', ->
describe 'an attack missing', ->
it 'deals no damage', ->
shared.create.call this,
team1: [Factory('Celebi')]
team2: [Factory('Magikarp')]
shared.biasRNG.call(this, 'randInt', 'miss', 100)
move = @battle.getMove('Leaf Storm')
originalHP = @p2.currentHP
@battle.performMove(@p1, @battle.getMove('Leaf Storm'))
@p2.currentHP.should.equal(originalHP)
it 'triggers effects dependent on the move missing', ->
shared.create.call this,
team1: [Factory('Hitmonlee')]
team2: [Factory('Magikarp')]
shared.biasRNG.call(this, 'randInt', 'miss', 100)
hiJumpKick = @battle.getMove('Hi Jump Kick')
mock = @sandbox.mock(hiJumpKick).expects('afterMiss').once()
@battle.performMove(@p1, hiJumpKick)
mock.verify()
it 'does not trigger effects dependent on the move hitting', ->
shared.create.call this,
team1: [Factory('Celebi')]
team2: [Factory('Gyarados')]
shared.biasRNG.call(this, 'randInt', 'miss', 100)
hiJumpKick = @battle.getMove('Hi Jump Kick')
mock = @sandbox.mock(hiJumpKick).expects('afterSuccessfulHit').never()
@battle.performMove(@p1, hiJumpKick)
mock.verify()
describe 'fainting', ->
it 'forces a new pokemon to be picked', ->
shared.create.call this,
team1: [Factory('Mew'), Factory('Heracross')]
team2: [Factory('Hitmonchan'), Factory('Heracross')]
spy = @sandbox.spy(@battle, 'tellPlayer')
@p2.currentHP = 1
@controller.makeMove(@id1, 'Psychic')
@controller.makeMove(@id2, 'Mach Punch')
spy.calledWith(@id2, Protocol.REQUEST_ACTIONS).should.be.true
it 'does not increment the turn count', ->
shared.create.call this,
team1: [Factory('Mew'), Factory('Heracross')]
team2: [Factory('Hitmonchan'), Factory('Heracross')]
turn = @battle.turn
@p2.currentHP = 1
@controller.makeMove(@id1, 'Psychic')
@controller.makeMove(@id2, 'Mach Punch')
@battle.turn.should.not.equal turn + 1
it 'removes the fainted pokemon from the action priority queue', ->
shared.create.call this,
team1: [Factory('Mew'), Factory('Heracross')]
team2: [Factory('Hitmonchan'), Factory('Heracross')]
@p1.currentHP = 1
@p2.currentHP = 1
@controller.makeMove(@id1, 'Psychic')
@controller.makeMove(@id2, 'Mach Punch')
@p1.currentHP.should.be.below 1
@p2.currentHP.should.equal 1
action = @battle.getAction(@p1)
should.not.exist(action)
it 'lets the player switch in a new pokemon', ->
shared.create.call this,
team1: [Factory('Mew'), Factory('Heracross')]
team2: [Factory('Hitmonchan'), Factory('Heracross')]
@p2.currentHP = 1
@controller.makeMove(@id1, 'Psychic')
@controller.makeMove(@id2, 'Mach Punch')
@controller.makeSwitch(@id2, 1)
@team2.first().species.should.equal 'Heracross'
it "occurs when a pokemon faints from passive damage", ->
shared.create.call(this)
@p2.currentHP = 1
@battle.performMove(@p1, @battle.getMove("Leech Seed"))
spy = @sandbox.spy(@p2, 'faint')
@battle.endTurn()
spy.calledOnce.should.be.true
it "occurs when a pokemon faints normally", ->
shared.create.call(this)
@p2.currentHP = 1
spy = @sandbox.spy(@p2, 'faint')
@battle.performMove(@p1, @battle.getMove("Tackle"))
spy.calledOnce.should.be.true
describe 'secondary effect attacks', ->
it 'can inflict effect on successful hit', ->
shared.create.call this,
team1: [Factory('Porygon-Z')]
team2: [Factory('Porygon-Z')]
shared.biasRNG.call(this, "next", 'secondary effect', 0) # 100% chance
@battle.performMove(@p1, @battle.getMove('Flamethrower'))
@p2.has(Status.Burn).should.be.true
describe 'the fang attacks', ->
it 'can inflict two effects at the same time', ->
shared.create.call this,
team1: [Factory('Gyarados')]
team2: [Factory('Gyarados')]
shared.biasRNG.call(this, "randInt", "secondary effect", 0) # 100% chance
shared.biasRNG.call(this, "randInt", "flinch", 0)
@battle.performMove(@p1, @battle.getMove("Ice Fang"))
@p2.has(Attachment.Flinch).should.be.true
@p2.has(Status.Freeze).should.be.true
describe 'a pokemon with technician', ->
it "doesn't increase damage if the move has bp > 60", ->
shared.create.call this,
team1: [Factory('Hitmontop')]
team2: [Factory('Mew')]
icePunch = @battle.getMove('Ice Punch')
icePunch.modifyBasePower(@battle, @p1, @p2).should.equal(0x1000)
it "increases damage if the move has bp <= 60", ->
shared.create.call this,
team1: [Factory('Hitmontop')]
team2: [Factory('Shaymin')]
bulletPunch = @battle.getMove('Bullet Punch')
bulletPunch.modifyBasePower(@battle, @p1, @p2).should.equal(0x1800)
describe 'STAB', ->
it "gets applied if the move and user share a type", ->
shared.create.call this,
team1: [Factory('Heracross')]
team2: [Factory('Regirock')]
megahorn = @battle.getMove("Megahorn")
megahorn.stabModifier(@battle, @p1, @p2).should.equal(0x1800)
it "doesn't get applied if the move and user are of different types", ->
shared.create.call this,
team1: [Factory('Hitmonchan')]
team2: [Factory('Mew')]
icePunch = @battle.getMove("Ice Punch")
icePunch.stabModifier(@battle, @p1, @p2).should.equal(0x1000)
describe 'turn order', ->
it 'randomly decides winner if pokemon have the same speed and priority', ->
shared.create.call this,
team1: [Factory('Mew')]
team2: [Factory('Mew')]
spy = @sandbox.spy(@battle, 'determineTurnOrder')
shared.biasRNG.call(this, "next", "turn order", .6)
@battle.recordMove(@id1, @battle.getMove('Psychic'))
@battle.recordMove(@id2, @battle.getMove('Psychic'))
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
@battle.pokemonActions = []
shared.biasRNG.call(this, "next", "turn order", .4)
@battle.recordMove(@id1, @battle.getMove('Psychic'))
@battle.recordMove(@id2, @battle.getMove('Psychic'))
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p1, @p2 ]
it 'decides winner by highest priority move', ->
shared.create.call this,
team1: [Factory('Hitmonchan')]
team2: [Factory('Hitmonchan')]
spy = @sandbox.spy(@battle, 'determineTurnOrder')
@battle.recordMove(@id1, @battle.getMove('Mach Punch'))
@battle.recordMove(@id2, @battle.getMove('Psychic'))
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p1, @p2 ]
@battle.pokemonActions = []
@battle.recordMove(@id1, @battle.getMove('Psychic'))
@battle.recordMove(@id2, @battle.getMove('Mach Punch'))
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
it 'decides winner by speed if priority is equal', ->
shared.create.call this,
team1: [Factory('Hitmonchan')]
team2: [Factory('Hitmonchan', evs: { speed: 4 })]
@battle.recordMove(@id1, @battle.getMove('ThunderPunch'))
@battle.recordMove(@id2, @battle.getMove('ThunderPunch'))
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
describe 'fainting all the opposing pokemon', ->
it "doesn't request any more actions from players", ->
shared.create.call this,
team1: [Factory('Hitmonchan')]
team2: [Factory('Mew')]
@p2.currentHP = 1
@controller.makeMove(@id1, 'Mach Punch')
@controller.makeMove(@id2, 'Psychic')
@battle.requests.should.not.have.property @id1.id
@battle.requests.should.not.have.property @id2.id
it 'ends the battle', ->
shared.create.call this,
team1: [Factory('Hitmonchan')]
team2: [Factory('Mew')]
@p2.currentHP = 1
mock = @sandbox.mock(@battle)
mock.expects('endBattle').once()
@controller.makeMove(@id1, 'Mach Punch')
@controller.makeMove(@id2, 'Psychic')
mock.verify()
describe 'a pokemon with a type immunity', ->
it 'cannot be damaged by a move of that type', ->
shared.create.call this,
team1: [Factory('Camerupt')]
team2: [Factory('Gyarados')]
@controller.makeMove(@id1, 'Earthquake')
@controller.makeMove(@id2, 'Dragon Dance')
@p2.currentHP.should.equal @p2.stat('hp')
describe 'a confused pokemon', ->
it "has a 50% chance of hurting itself", ->
shared.create.call(this)
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
@p1.attach(Attachment.Confusion, {@battle})
shared.biasRNG.call(this, "next", 'confusion', 0) # always hits
mock = @sandbox.mock(@battle.getMove('Tackle'))
mock.expects('execute').never()
@controller.makeMove(@id1, 'Tackle')
@controller.makeMove(@id2, 'Splash')
mock.verify()
@p1.currentHP.should.be.lessThan @p1.stat('hp')
@p2.currentHP.should.equal @p2.stat('hp')
it "deals a minimum of 1 damage", ->
shared.create.call(this, team1: [Factory("Shuckle", level: 1)])
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
@p1.attach(Attachment.Confusion, {@battle})
shared.biasRNG.call(this, "next", 'confusion', 0) # always hits
@sandbox.stub(@battle.confusionMove, 'calculateDamage', -> 0)
@battle.performMove(@p1, @battle.getMove('Tackle'))
@p1.currentHP.should.equal(@p1.stat('hp') - 1)
it "snaps out of confusion after a predetermined number of turns", ->
shared.create.call(this)
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
@p1.attach(Attachment.Confusion)
@controller.makeMove(@id1, 'Splash')
@controller.makeMove(@id2, 'Splash')
@controller.makeMove(@id1, 'Splash')
@controller.makeMove(@id2, 'Splash')
@p1.has(Attachment.Confusion).should.be.false
it "will not crit the confusion recoil", ->
shared.create.call(this)
@p1.attach(Attachment.Confusion)
shared.biasRNG.call(this, "next", 'confusion', 0) # always recoils
shared.biasRNG.call(this, 'next', 'ch', 0) # always crits
spy = @sandbox.spy(@battle.confusionMove, 'isCriticalHit')
@controller.makeMove(@id1, 'Tackle')
@controller.makeMove(@id2, 'Tackle')
spy.returned(false).should.be.true
it "will not error for not having unusual move properties", ->
shared.create.call(this, team1: [Factory("Magikarp", ability: "Iron Fist")])
@p1.attach(Attachment.Confusion)
shared.biasRNG.call(this, "next", 'confusion', 0) # always recoils
(=>
@controller.makeMove(@id1, 'Tackle')
@controller.makeMove(@id2, 'Tackle')
).should.not.throw()
describe 'a frozen pokemon', ->
it "will not execute moves", ->
shared.create.call(this)
@p1.attach(Status.Freeze)
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
mock = @sandbox.mock(@battle.getMove('Tackle'))
mock.expects('execute').never()
@controller.makeMove(@id1, 'Tackle')
@controller.makeMove(@id2, 'Splash')
mock.verify()
it "has a 20% chance of unfreezing", ->
shared.create.call(this)
@p1.attach(Status.Freeze)
shared.biasRNG.call(this, "next", 'unfreeze chance', 0) # always unfreezes
@controller.makeMove(@id1, 'Splash')
@controller.makeMove(@id2, 'Splash')
@p1.has(Status.Freeze).should.be.false
it "unfreezes if hit by a fire move", ->
shared.create.call(this)
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
@p1.attach(Status.Freeze)
@battle.performMove(@p2, @battle.getMove('Flamethrower'))
@p1.has(Status.Freeze).should.be.false
it "does not unfreeze if hit by a non-damaging move", ->
shared.create.call(this)
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
@p1.attach(Status.Freeze)
@battle.performMove(@p2, @battle.getMove('Will-O-Wisp'))
@p1.has(Status.Freeze).should.be.true
for moveName in ["Sacred Fire", "Flare Blitz", "Flame Wheel", "Fusion Flare", "Scald"]
it "automatically unfreezes if using #{moveName}", ->
shared.create.call(this)
@p1.attach(Status.Freeze)
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
@battle.performMove(@p1, @battle.getMove(moveName))
@p1.has(Status.Freeze).should.be.false
describe "a paralyzed pokemon", ->
it "has a 25% chance of being fully paralyzed", ->
shared.create.call(this)
@p1.attach(Status.Paralyze)
shared.biasRNG.call(this, "next", 'paralyze chance', 0) # always stays frozen
mock = @sandbox.mock(@battle.getMove('Tackle'))
mock.expects('execute').never()
@controller.makeMove(@id1, 'Tackle')
@controller.makeMove(@id2, 'Splash')
mock.verify()
it "has its speed quartered", ->
shared.create.call(this)
speed = @p1.stat('speed')
@p1.attach(Status.Paralyze)
@p1.stat('speed').should.equal Math.floor(speed / 4)
describe "a burned pokemon", ->
it "loses 1/8 of its HP each turn", ->
shared.create.call(this)
@p1.attach(Status.Burn)
hp = @p1.currentHP
eighth = Math.floor(hp / 8)
@battle.endTurn()
(hp - @p1.currentHP).should.equal(eighth)
@battle.endTurn()
(hp - @p1.currentHP).should.equal(2 * eighth)
it "loses 1 minimum HP", ->
shared.create.call(this, team1: [Factory("Shedinja")])
@p1.attach(Status.Burn)
@p1.currentHP.should.equal(1)
@battle.endTurn()
@p1.currentHP.should.equal(0)
describe "a sleeping pokemon", ->
it "sleeps for 1-3 turns", ->
shared.create.call(this)
shared.biasRNG.call(this, "randInt", 'sleep turns', 3)
@p1.attach(Status.Sleep)
tackle = @battle.getMove('Tackle')
for i in [0...3]
@battle.performMove(@p1, tackle)
@p1.has(Status.Sleep).should.be.true
@battle.performMove(@p1, tackle)
@p1.has(Status.Sleep).should.be.false
it "cannot move while asleep", ->
shared.create.call(this)
shared.biasRNG.call(this, "randInt", 'sleep turns', 3)
@p1.attach(Status.Sleep)
tackle = @battle.getMove('Tackle')
mock = @sandbox.mock(tackle).expects('execute').never()
for i in [0...3]
@battle.performMove(@p1, tackle)
mock.verify()
tackle.execute.restore()
mock = @sandbox.mock(tackle).expects('execute').once()
@battle.performMove(@p1, tackle)
mock.verify()
it "resets its counter when switching out", ->
shared.create.call this,
team1: [ Factory("Magikarp"), Factory("Magikarp") ]
shared.biasRNG.call(this, "randInt", 'sleep turns', 1)
@p1.attach(Status.Sleep)
tackle = @battle.getMove('Tackle')
@battle.performMove(@p1, tackle)
@battle.performSwitch(@team1.first(), 1)
@battle.performSwitch(@team1.first(), 1)
@battle.performMove(@team1.first(), tackle)
@p1.has(Status.Sleep).should.be.true
describe "a poisoned pokemon", ->
it "loses 1/8 of its HP each turn", ->
shared.create.call(this)
@p1.attach(Status.Poison)
hp = @p1.currentHP
eighth = Math.floor(hp / 8)
@battle.endTurn()
(hp - @p1.currentHP).should.equal(eighth)
@battle.endTurn()
(hp - @p1.currentHP).should.equal(2 * eighth)
it "loses 1 minimum HP", ->
shared.create.call(this, team1: [Factory("Shedinja")])
@p1.attach(Status.Poison)
@p1.currentHP.should.equal(1)
@battle.endTurn()
@p1.currentHP.should.equal(0)
describe "a badly poisoned pokemon", ->
it "loses 1/16 of its HP the first turn", ->
shared.create.call(this)
@p1.attach(Status.Toxic)
hp = @p1.currentHP
@battle.endTurn()
(hp - @p1.currentHP).should.equal(hp >> 4)
it "loses 1/16 of its max HP, rounded down, times x where x is the number of turns up to 15", ->
shared.create.call(this)
@p1.attach(Status.Toxic)
hp = @p1.currentHP
fraction = (hp >> 4)
for i in [1..16]
@battle.endTurn()
hpFraction = Math.min(fraction * i, fraction * 15)
(hp - @p1.currentHP).should.equal(hpFraction)
@p1.currentHP = hp
it "still increases the counter with poison heal", ->
shared.create.call(this, team1: [Factory("Magikarp", ability: "Poison Heal")])
@p1.attach(Status.Toxic)
hp = @p1.currentHP
fraction = (hp >> 4)
turns = 3
for i in [1...turns]
@battle.endTurn()
@p1.copyAbility(null)
@battle.endTurn()
hpFraction = Math.min(fraction * turns, fraction * 15)
(hp - @p1.currentHP).should.equal(hpFraction)
it "loses 1 minimum HP", ->
shared.create.call(this, team1: [Factory("Shedinja")])
@p1.attach(Status.Toxic)
@p1.currentHP.should.equal(1)
@battle.endTurn()
@p1.currentHP.should.equal(0)
it "resets its counter when switching out", ->
shared.create.call this,
team1: [ Factory("Magikarp"), Factory("Magikarp") ]
@p1.attach(Status.Toxic)
hp = @p1.currentHP
@battle.endTurn()
@battle.performSwitch(@team1.first(), 1)
@p1.currentHP = hp
@battle.performSwitch(@team1.first(), 1)
@battle.endTurn()
(hp - @p1.currentHP).should.equal(hp >> 4)
describe "Pokemon#turnsActive", ->
it "is 1 on start of battle", ->
shared.create.call(this)
@p1.turnsActive.should.equal 1
it "is set to 0 when switching", ->
shared.create.call(this, team1: (Factory("Magikarp") for x in [1..2]))
@p1.turnsActive = 4
@team1.switch(@p1, 0)
@team1.first().turnsActive.should.equal 0
it "increases by 1 when a turn ends", ->
shared.create.call(this)
@p1.turnsActive.should.equal 1
@battle.endTurn()
@p1.turnsActive.should.equal 2
describe "A move with 0 PP", ->
it "will not execute", ->
shared.create.call(this)
move = @p1.moves[0]
@p1.setPP(move, 0)
@sandbox.mock(move).expects('execute').never()
@sandbox.mock(@p1).expects('beforeMove').never()
@battle.performMove(@p1, move)
describe "A pokemon with no available moves", ->
it "can struggle", ->
shared.create.call(this)
@battle.removeRequest(@id1)
# Next turn, @p1 will have no available moves.
for move in @p1.moves
@p1.blockMove(move)
@p1.resetBlocks = ->
@p1.validMoves().should.be.empty
@battle.beginTurn()
request = @battle.requestFor(@p1)
should.exist(request)
request.should.have.property('moves')
request.moves.should.eql(["Struggle"])
describe "Ability activation at the beginning of a battle", ->
it "considers Abilities that are always active", ->
shared.create.call this,
team1: [Factory("Magikarp", ability: "Clear Body")]
team2: [Factory("Magikarp", ability: "Intimidate")]
@p1.stages.should.containEql(attack: 0)
xit "works the same way regardless of the entry order", ->
shared.create.call this,
team1: [Factory("Magikarp", ability: "Intimidate")]
team2: [Factory("Magikarp", ability: "Clear Body")]
@p2.stages.should.containEql(attack: 0)