require '../helpers' sinon = require('sinon') {Attachment} = require('../../server/bw/attachment') {Battle} = require('../../server/bw/battle') {Weather} = require('../../shared/weather') {Conditions} = require '../../shared/conditions' {Factory} = require('../factory') {BattleServer} = require('../../server/server') {Protocol} = require '../../shared/protocol' shared = require('../shared') should = require 'should' {_} = require 'underscore' ratings = require('../../server/ratings') describe 'Battle', -> beforeEach -> @server = new BattleServer() shared.create.call this, format: 'xy1000' team1: [Factory('Hitmonchan'), Factory('Heracross')] team2: [Factory('Hitmonchan'), Factory('Heracross')] it 'starts at turn 1', -> @battle.turn.should.equal 1 describe '#hasWeather(weatherName)', -> it 'returns true if the current battle weather is weatherName', -> @battle.weather = "Sunny" @battle.hasWeather("Sunny").should.be.true it 'returns false on non-None in presence of a weather-cancel ability', -> @battle.weather = "Sunny" @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true) @battle.hasWeather("Sunny").should.be.false it 'returns true on None in presence of a weather-cancel ability', -> @battle.weather = "Sunny" @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true) @battle.hasWeather("None").should.be.true describe '#recordMove', -> it "records a player's move", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1) should.exist(action) action.should.have.property("move") action.move.should.equal @battle.getMove('Tackle') it "does not record a move if player has already made an action", -> length = @battle.pokemonActions.length @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.pokemonActions.length.should.equal(1 + length) describe '#undoCompletedRequest', -> it "fails if the player didn't make any action", -> @battle.undoCompletedRequest(@id1).should.be.false it "fails on the second turn as well if the player didn't make any action", -> @controller.makeMove(@id1, "Mach Punch") @controller.makeMove(@id2, "Mach Punch") @battle.turn.should.equal 2 @battle.undoCompletedRequest(@id1).should.be.false it "succeeds if the player selected an action already", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.pokemonActions.should.not.be.empty @battle.undoCompletedRequest(@id1).should.be.true @battle.pokemonActions.should.be.empty it "can cancel an action multiple times", -> for i in [0..5] @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.pokemonActions.should.not.be.empty @battle.undoCompletedRequest(@id1).should.be.true @battle.pokemonActions.should.be.empty describe '#recordSwitch', -> it "records a player's switch", -> @battle.recordSwitch(@id1, 1) action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1) should.exist(action) action.should.have.property("to") action.to.should.equal 1 it "does not record a switch if player has already made an action", -> length = @battle.pokemonActions.length @battle.recordSwitch(@id1, 1) @battle.recordSwitch(@id1, 1) @battle.pokemonActions.length.should.equal(1 + length) describe '#performSwitch', -> it "swaps pokemon positions of a player's team", -> [poke1, poke2] = @team1.pokemon @battle.performSwitch(@p1, 1) @team1.pokemon.slice(0, 2).should.eql [poke2, poke1] it "calls the pokemon's switchOut() method", -> pokemon = @p1 mock = @sandbox.mock(pokemon) mock.expects('switchOut').once() @battle.performSwitch(@p1, 1) mock.verify() describe "#setWeather", -> it "can last a set number of turns", -> @battle.setWeather(Weather.SUN, 5) for i in [0...5] @battle.endTurn() @battle.weather.should.equal Weather.NONE describe "weather", -> it "damages pokemon who are not of a certain type", -> @battle.setWeather(Weather.SAND) @battle.endTurn() maxHP = @p1.stat('hp') (maxHP - @p1.currentHP).should.equal Math.floor(maxHP / 16) (maxHP - @p2.currentHP).should.equal Math.floor(maxHP / 16) @battle.setWeather(Weather.HAIL) @battle.endTurn() maxHP = @p1.stat('hp') (maxHP - @p1.currentHP).should.equal 2*Math.floor(maxHP / 16) (maxHP - @p2.currentHP).should.equal 2*Math.floor(maxHP / 16) describe "move PP", -> it "goes down after a pokemon uses a move", -> pokemon = @p1 move = @p1.moves[0] @battle.performMove(@p1, move) @p1.pp(move).should.equal(@p1.maxPP(move) - 1) describe "#performMove", -> it "records this move as the battle's last move", -> move = @p1.moves[0] @battle.performMove(@p1, move) should.exist @battle.lastMove @battle.lastMove.should.equal move describe "#bump", -> it "bumps a pokemon to the front of its priority bracket", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id2, @battle.getMove('Splash')) @battle.determineTurnOrder() # Get last pokemon to move and bump it up {pokemon} = @battle.pokemonActions[@battle.pokemonActions.length - 1] @battle.bump(pokemon) @battle.pokemonActions[0].pokemon.should.eql pokemon it "bumps a pokemon to the front of a specific priority bracket", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id2, @battle.getMove('Mach Punch')) queue = @battle.determineTurnOrder() @battle.bump(@p1, @battle.getMove('Mach Punch').priority) queue[0].pokemon.should.eql @p1 it "still works even if there's nothing in that bracket", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id2, @battle.getMove('Mach Punch')) queue = @battle.determineTurnOrder() queue.should.have.length(2) @battle.bump(@p1) queue.should.have.length(2) queue[0].pokemon.should.eql @p2 queue[1].pokemon.should.eql @p1 it "is a no-op if no actions", -> queue = @battle.determineTurnOrder() queue.should.have.length(0) @battle.bump(@p1) queue.should.have.length(0) describe "#delay", -> it "delays a pokemon to the end of its priority bracket", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id2, @battle.getMove('Splash')) @battle.determineTurnOrder() # Get first pokemon to move and delay it {pokemon} = @battle.pokemonActions[0] @battle.delay(pokemon) @battle.pokemonActions[1].pokemon.should.eql pokemon it "delays a pokemon to the end of a specific priority bracket", -> @battle.recordMove(@id1, @battle.getMove('Mach Punch')) @battle.recordMove(@id2, @battle.getMove('Tackle')) queue = @battle.determineTurnOrder() @battle.delay(@p1, @battle.getMove('Tackle').priority) queue[1].pokemon.should.eql @p1 it "still works even if there's nothing in that bracket", -> @battle.recordMove(@id1, @battle.getMove('Tackle')) @battle.recordMove(@id2, @battle.getMove('Mach Punch')) queue = @battle.determineTurnOrder() queue.should.have.length(2) @battle.delay(@p1) queue.should.have.length(2) queue[0].pokemon.should.eql @p2 queue[1].pokemon.should.eql @p1 it "is a no-op if no actions", -> queue = @battle.determineTurnOrder() queue.should.have.length(0) @battle.delay(@p1) queue.should.have.length(0) describe "#weatherUpkeep", -> it "does not damage Pokemon if a weather-cancel ability is on the field", -> @battle.setWeather(Weather.HAIL) @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true) @battle.endTurn() @p1.currentHP.should.not.be.lessThan @p1.stat('hp') @p2.currentHP.should.not.be.lessThan @p2.stat('hp') it "does not damage a Pokemon who is immune to a weather", -> @battle.setWeather(Weather.HAIL) @sandbox.stub(@p2, 'isWeatherDamageImmune', -> true) @battle.endTurn() @p1.currentHP.should.be.lessThan @p1.stat('hp') @p2.currentHP.should.not.be.lessThan @p2.stat('hp') describe "#add", -> it "adds the spectator to an internal array", -> connection = @stubSpark() spectator = @server.findOrCreateUser(id: 1, name: "derp", connection) length = @battle.sparks.length @battle.add(connection) @battle.sparks.should.have.length(length + 1) @battle.sparks.should.containEql(connection) it "gives the spectator battle information", -> connection = @stubSpark() spectator = @server.findOrCreateUser(id: 1, name: "derp", connection) spy = @sandbox.spy(connection, 'send') @battle.add(connection) {id, numActive, log} = @battle spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true it "receives the correct set of initial teams", -> connection = @stubSpark() spectator = @server.findOrCreateUser(id: 1, name: "derp", connection) spy = @sandbox.spy(connection, 'send') teams = @battle.getTeams().map((team) -> team.toJSON(hidden: true)) @team1.switch(@p1, 1) @battle.add(connection) {id, numActive, log} = @battle spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true it "does not add a spectator twice", -> connection = @stubSpark() spectator = @server.findOrCreateUser(id: 1, name: "derp", connection) length = @battle.sparks.length @battle.add(connection) @battle.add(connection) @battle.sparks.should.have.length(length + 1) describe "#getWinner", -> it "returns player 1 if player 2's team has all fainted", -> pokemon.faint() for pokemon in @team2.pokemon @battle.getWinner().should.equal(@id1) it "returns player 2 if player 1's team has all fainted", -> pokemon.faint() for pokemon in @team1.pokemon @battle.getWinner().should.equal(@id2) it "declares the pokemon dying of recoil the winner", -> shared.create.call this, team1: [Factory('Talonflame', moves: ["Brave Bird"])] team2: [Factory('Magikarp')] @p1.currentHP = @p2.currentHP = 1 spy = @sandbox.spy(@battle, 'getWinner') @controller.makeMove(@id1, "Brave Bird") @controller.makeMove(@id2, "Splash") spy.returned(@id1).should.be.true describe "#endBattle", -> it "cannot end multiple times", -> spy = @sandbox.spy(@battle, 'emit') spy.withArgs("end") @battle.endBattle() @battle.endBattle() spy.withArgs("end").calledOnce.should.be.true it "marks the battle as over", -> @battle.endBattle() @battle.isOver().should.be.true it "updates the winner and losers' ratings", (done) -> shared.create.call this, team1: [Factory('Hitmonchan')] team2: [Factory('Mew')] conditions: [ Conditions.RATED_BATTLE ] @battle.on "ratingsUpdated", => ratings.getPlayer @id1, (err, rating1) => ratings.getPlayer @id2, (err, rating2) => rating1.rating.should.be.greaterThan(ratings.DEFAULT_RATING) rating2.rating.should.be.lessThan(ratings.DEFAULT_RATING) done() @p2.currentHP = 1 mock = @sandbox.mock(@controller) @controller.makeMove(@id1, 'Mach Punch') @controller.makeMove(@id2, 'Psychic') it "doesn't update ratings if an unrated battle", (done) -> shared.create.call this, team1: [Factory('Hitmonchan')] team2: [Factory('Mew')] @battle.on 'end', => ratings.getPlayer @id1, (err, rating1) => ratings.getPlayer @id2, (err, rating2) => rating1.rating.should.equal(ratings.DEFAULT_RATING) rating2.rating.should.equal(ratings.DEFAULT_RATING) done() @p2.currentHP = 1 mock = @sandbox.mock(@controller) @controller.makeMove(@id1, 'Mach Punch') @controller.makeMove(@id2, 'Psychic') describe "#forfeit", -> it "prematurely ends the battle", -> spy = @sandbox.spy(@battle, 'tell') spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1)) @battle.forfeit(@id1) spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1)) .called.should.be.true it "does not forfeit if the player given is invalid", -> mock = @sandbox.mock(@battle).expects('tell').never() @battle.forfeit('this definitely should not work') mock.verify() it "cannot forfeit multiple times", -> spy = @sandbox.spy(@battle, 'tell') spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1)) @battle.forfeit(@id1) @battle.forfeit(@id1) spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1)) .calledOnce.should.be.true it "marks the battle as over", -> @battle.forfeit(@id1) @battle.isOver().should.be.true it "doesn't update the winner and losers' ratings if not a rated battle", (done) -> @battle.on 'end', => ratings.getPlayer @id1, (err, rating1) => ratings.getPlayer @id2, (err, rating2) => rating1.rating.should.equal(ratings.DEFAULT_RATING) rating2.rating.should.equal(ratings.DEFAULT_RATING) done() @battle.forfeit(@id2) describe "#hasStarted", -> it "returns false if the battle has not started", -> battle = new Battle('id', []) battle.hasStarted().should.be.false it "returns true if the battle has started", -> battle = new Battle('id', []) battle.begin() battle.hasStarted().should.be.true describe "#getAllAttachments", -> it "returns a list of attachments for all pokemon, teams, and battles", -> @battle.attach(Attachment.TrickRoom) @team2.attach(Attachment.Reflect) @p1.attach(Attachment.Ingrain) attachments = @battle.getAllAttachments() should.exist(attachments) attachments = attachments.map((a) -> a.constructor) attachments.length.should.be.greaterThan(2) attachments.should.containEql(Attachment.TrickRoom) attachments.should.containEql(Attachment.Reflect) attachments.should.containEql(Attachment.Ingrain) describe "#query", -> it "queries all attachments attached to a specific event", -> @battle.attach(Attachment.TrickRoom) @team2.attach(Attachment.Reflect) @p1.attach(Attachment.Ingrain) mocks = [] mocks.push @sandbox.mock(Attachment.TrickRoom.prototype) mocks.push @sandbox.mock(Attachment.Reflect.prototype) mocks.push @sandbox.mock(Attachment.Ingrain.prototype) mock.expects("endTurn").once() for mock in mocks attachments = @battle.query("endTurn") mock.verify() for mock in mocks describe "#getOpponents", -> it "returns all opponents of a particular pokemon as an array", -> @battle.getOpponents(@p1).should.be.an.instanceOf(Array) @battle.getOpponents(@p1).should.have.length(1) it "does not include fainted opponents", -> @p2.faint() @battle.getOpponents(@p1).should.have.length(0) describe "#sendRequestTo", -> it "sends all requests to a certain player", -> mock = @sandbox.mock(@battle).expects('tellPlayer').once() mock.withArgs(@id1, Protocol.REQUEST_ACTIONS) @battle.sendRequestTo(@id1) mock.verify() describe "expiration", -> beforeEach -> shared.create.call(this) it "ends ongoing battles after a specific amount", -> time = Battle::ONGOING_BATTLE_TTL @clock.tick(time) @battle.isOver().should.be.true it "sends BATTLE_EXPIRED after a specific amount after battle end", -> time = Battle::ENDED_BATTLE_TTL delta = 5000 spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED) @clock.tick(delta) @battle.forfeit(@id1) @battle.isOver().should.be.true spy.withArgs(Protocol.BATTLE_EXPIRED).called.should.be.false @battle.tell.restore() spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED) @clock.tick(time) @battle.isOver().should.be.true spy.withArgs(Protocol.BATTLE_EXPIRED).calledOnce.should.be.true it "doesn't call 'expire' if expiring twice", -> firstExpire = 24 * 60 * 60 * 1000 secondExpire = 48 * 60 * 60 * 1000 spy = @sandbox.spy(@battle, 'expire') @clock.tick(firstExpire) @battle.isOver().should.be.true @clock.tick(secondExpire) spy.calledOnce.should.be.true it "doesn't call 'end' twice", -> longExpire = 48 * 60 * 60 * 1000 spy = @sandbox.spy() @battle.on('end', spy) @battle.forfeit(@id1) @battle.isOver().should.be.true @clock.tick(longExpire) spy.calledOnce.should.be.true describe "Rated battles", -> beforeEach -> shared.create.call this, team1: [Factory('Hitmonchan')] team2: [Factory('Mew')] conditions: [ Conditions.RATED_BATTLE ] it "updates the winner and losers' ratings", (done) -> @battle.on "ratingsUpdated", => ratings.getPlayer @id1, (err, rating1) => ratings.getPlayer @id2, (err, rating2) => defaultPlayer = ratings.algorithm.createPlayer() rating1.rating.should.be.greaterThan(defaultPlayer.rating) rating2.rating.should.be.lessThan(defaultPlayer.rating) done() @battle.forfeit(@id2)