com.wx3.galacdecks.game.GameRules.java Source code

Java tutorial

Introduction

Here is the source code for com.wx3.galacdecks.game.GameRules.java

Source

/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2015 Kevin Lin
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * 
 */
/**
 * 
 */
package com.wx3.galacdecks.game;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.wx3.galacdecks.Bootstrap;
import com.wx3.galacdecks.game.EntityTag;
import com.wx3.galacdecks.gameevents.ClientPrefabEvent;
import com.wx3.galacdecks.gameevents.ConsumeResourceEvent;
import com.wx3.galacdecks.gameevents.DamageCausedEvent;
import com.wx3.galacdecks.gameevents.DamageTakenEvent;
import com.wx3.galacdecks.gameevents.DeathEvent;
import com.wx3.galacdecks.gameevents.DiscardEvent;
import com.wx3.galacdecks.gameevents.DrawCardEvent;
import com.wx3.galacdecks.gameevents.EndTurnEvent;
import com.wx3.galacdecks.gameevents.GameEvent;
import com.wx3.galacdecks.gameevents.GameOverEvent;
import com.wx3.galacdecks.gameevents.MoveEvent;
import com.wx3.galacdecks.gameevents.PlayCardEvent;
import com.wx3.galacdecks.gameevents.ReturnCardEvent;
import com.wx3.galacdecks.gameevents.ShieldRechargeEvent;
import com.wx3.galacdecks.gameevents.StartGameEvent;
import com.wx3.galacdecks.gameevents.SummonUnitEvent;
import com.wx3.galacdecks.gameevents.UpdateResourceEvent;

/**
 * The GameRules is the logic of the game. It modifies the game state
 * based on actions and the resulting {@link GameEvent}. No persistent
 * state is stored in a GameRules instance-- the state of any game
 * should be defined entirely by its gamestate.
 * 
 * 
 * All GameEvent logic uses the public methods of the GameRules to 
 * effect game play.
 * 
 * @author Kevin
 *
 */
public class GameRules {

    private static ScriptEngine scriptEngine;
    private static final int MAX_EVENTS = 1000;

    // Name of the phase that buff rules listen for:
    public static final String BUFF_PHASE = "BuffPhase";
    public static final String DEFAULT_SHIP = "EXPLORER";
    public static final int MAX_CARDS = 8;
    public static final int MAX_RESOURCES = 10;

    private GameSystem gameSystem;
    private Map<String, EntityPrototype> cards;
    private Map<String, EntityRule> entityRules;
    private GameState gameState;
    private GameOverHandler gameOverHandler;

    private List<GameEvent> processedEvents = new ArrayList<>();

    final static Logger logger = LoggerFactory.getLogger(GameRules.class);

    private static ScriptEngine getScriptEngine() {
        if (scriptEngine == null) {
            NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
            scriptEngine = factory.getScriptEngine(new RestrictiveFilter());
            if (scriptEngine == null) {
                throw new RuntimeException("Unable to get script engine");
            }
        }
        return scriptEngine;
    }

    /**
     * Don't allow scripts to access general Java classes.
     *  
     * @author Kevin
     *
     */
    static class RestrictiveFilter implements ClassFilter {
        @Override
        public boolean exposeToScripts(String s) {
            return false;
        }
    }

    public GameRules(GameSystem gameSystem) {
        this.gameSystem = gameSystem;
        this.cards = gameSystem.getPrototypes();
        if (cards == null || cards.size() == 0) {
            throw new RuntimeException("GameSystem did not define any prototypes");
        }
        this.entityRules = gameSystem.getRules();
        List<EntityCoordinates> coords = new ArrayList<EntityCoordinates>();
        for (int y = 0; y < 5; y++) {
            for (int x = 0; x < 9; x++) {
                coords.add(new EntityCoordinates(x, y));
            }
        }
        coords.remove(new EntityCoordinates(0, 4));
        coords.remove(new EntityCoordinates(1, 4));
        coords.remove(new EntityCoordinates(3, 4));
        coords.remove(new EntityCoordinates(5, 4));
        coords.remove(new EntityCoordinates(7, 4));
        coords.remove(new EntityCoordinates(8, 4));
        coords.remove(new EntityCoordinates(0, 0));
        coords.remove(new EntityCoordinates(8, 0));

        gameState = new GameState(coords);
    }

    public GameRules(GameRules original) {
        this.cards = original.cards;
        this.entityRules = original.entityRules;
        this.gameState = new GameState(original.getGameState());
    }

    public void setGameOverHandler(GameOverHandler handler) {
        this.gameOverHandler = handler;
    }

    /**
     * Get a ScriptContext with the GameRules bound to it so its methods are available.
     *
     * @return
     * @throws NoSuchMethodException
     * @throws ScriptException
     */
    ScriptContext getScriptContext() throws NoSuchMethodException, ScriptException {
        ScriptEngine script = getScriptEngine();
        ScriptContext scriptContext = new SimpleScriptContext();
        scriptContext.setBindings(script.createBindings(), ScriptContext.ENGINE_SCOPE);
        bindGameToScript(scriptContext);
        return scriptContext;
    }

    /**
     * Binds to the javascript global object so that GameInstance methods
     * can be called without needing to preface every one with an object reference,
     * e.g.: "endTurn()" instead of "game.endTurn()".
     * 
     * @throws ScriptException
     * @throws NoSuchMethodException
     */
    private void bindGameToScript(ScriptContext scriptContext) throws ScriptException, NoSuchMethodException {
        ScriptEngine engine = getScriptEngine();
        Object global = engine.eval("this", scriptContext);
        Object jsObject = engine.eval("Object", scriptContext);
        ((Invocable) engine).invokeMethod(jsObject, "bindProperties", global, this);
    }

    /**
     * Create the root entity with the appropriate rules and put it in play.
     */
    void start() {
        GameEntity rootEntity = spawnEntity();
        rootEntity.setName("ROOT");
        rootEntity.setTag(EntityTag.ROOT);
        for (String rootRule : gameSystem.rootRules) {
            addRule(rootEntity, rootRule);
        }
        gameState.putInPlay(rootEntity);
        addEvent(new StartGameEvent());
        gameState.start();
        processEvents();
        startTurn();
        processEvents();
        processedEvents.clear();
        logger.info("Game started for system " + gameSystem);
    }

    synchronized PlayValidator getValidPlaysForPlayer(PlayerState playerState) {
        PlayValidator validPlays = PlayValidator.GenerateValidPlays(this, playerState);
        return validPlays;
    }

    public synchronized List<GameEvent> handleCommand(GameCommand command) throws BadCommandException {
        if (!isStarted()) {
            throw new BadCommandException("Game not started");
        }
        PlayerState playerState = gameState.getPlayer(command.getPlayerPosition());
        if (gameState.getCurrentPlayer() != playerState) {
            throw new BadCommandException("Not player's turn");
        }
        command.execute(this);
        processEvents();
        List<GameEvent> events = new ArrayList<>(processedEvents);
        processedEvents.clear();
        return events;
    }

    public GameSystem getSystem() {
        return gameSystem;
    }

    public boolean isStarted() {
        return gameState.isStarted();
    }

    void setPlayer(int pos, PlayerInstance player) {
        PlayerShip playerShip = new PlayerShip();

        playerShip.setStartingDeck(Bootstrap.GetDefaultDeck());
        player.setShip(playerShip);
        initPlayer(player);
    }

    /**
     * Send a string to the logger. This is useful for testing rule scripts.
     * @param message
     */
    public void debug(String message) {
        logger.info(message);
    }

    private void calculateMaxResource() {
        PlayerState currentPlayer = gameState.getCurrentPlayer();
        int energy = 0;
        for (GameEntity entity : gameState.getInPlay(currentPlayer)) {
            energy += entity.getEnergyResource();
        }
        if (energy > MAX_RESOURCES)
            energy = MAX_RESOURCES;
        currentPlayer.setMaxResource(EntityStat.ENERGY_RESOURCE, energy);
    }

    private void recalculateMinerals(PlayerState player) {
        int mineralResource = 0;
        int mineralCost = 0;
        for (GameEntity entity : gameState.getInPlay(player)) {
            mineralResource += entity.getMineralResource();
            mineralCost += entity.getMineralCost();
        }
        if (mineralResource > MAX_RESOURCES)
            mineralResource = MAX_RESOURCES;
        player.setMaxResource(EntityStat.MINERAL_RESOURCE, mineralResource);
        int mineralRemaining = mineralResource - mineralCost;
        if (mineralRemaining < 0)
            mineralRemaining = 0;
        player.setResource(EntityStat.MINERAL_RESOURCE, mineralRemaining);
    }

    void startTurn() {
        logger.info(this + " starting turn " + gameState.getTurn());
        PlayerState currentPlayer = gameState.getCurrentPlayer();
        calculateMaxResource();
        for (GameEntity entity : gameState.getInPlay()) {
            entity.turnReset();
        }
        currentPlayer.setResource(EntityStat.ENERGY_RESOURCE, 0);
        addEnergy(currentPlayer, currentPlayer.getMaxResource(EntityStat.ENERGY_RESOURCE));

        for (GameEntity entity : gameState.getInPlay()) {
            int amount = entity.getMaxShields() - entity.getCurrentShields();
            rechargeShields(entity, amount);
        }
    }

    /// Methods executed by commands:

    void endTurn() {
        // Recalc max resources for the player who just ended their turn:
        calculateMaxResource();
        for (GameEntity entity : gameState.getInPlay()) {
            entity.deductDisable();
        }
        ++gameState.turn;
        addEvent(new EndTurnEvent(gameState));
        logger.info("Turn is now " + gameState.turn);
    }

    /**
     * Play a unit from a card in hand
     * @param entity
     * @param column
     * @param row
     */
    void playUnitFromHand(GameEntity card, int column, int row) {
        card.clearTag(EntityTag.IN_HAND);
        if (!card.getOwner().hand.remove(card)) {
            throw new RuntimeException("Tried to summon a unit that wasn't in hand.");
        }
        EntityCoordinates coord = new EntityCoordinates(column, row);
        consumeEnergy(card, card.getStat(EntityStat.ENERGY_COST));
        consumeMinerals(card, card.getStat(EntityStat.MINERAL_COST));
        addEvent(new PlayCardEvent(card, column, row));

        gameState.placeEntity(card, coord);
        EntityPrototype prototype = cards.get(card.getPrototypeId());
        addEvent(new SummonUnitEvent(card, prototype));
    }

    void playPowerFromHand(GameEntity card, int x, int y) {
        card.clearTag(EntityTag.IN_HAND);
        if (!card.getOwner().hand.remove(card)) {
            throw new RuntimeException("Tried to play a card that wasn't in hand.");
        }
        consumeEnergy(card, card.getStat(EntityStat.ENERGY_COST));
        consumeMinerals(card, card.getStat(EntityStat.MINERAL_COST));
        GameEntity target = null;
        if (x >= 0) {
            target = gameState.getEntityAt(x, y);
        }
        gameState.putInPlay(card);
        addEvent(new PlayCardEvent(card, x, y, target));
    }

    void discardCard(GameEntity card, int x, int y) {
        card.clearTag(EntityTag.IN_HAND);
        if (!card.getOwner().hand.remove(card)) {
            throw new RuntimeException("Tried to discard a card that wasn't in hand.");
        }
        GameEntity target = null;
        if (x >= 0) {
            target = gameState.getEntityAt(x, y);
        }
        if (target == null) {
            throw new RuntimeException("Discard requires a target.");
        }
        target.useDiscard();
        addEvent(new DiscardEvent(card, target));
    }

    /// End methods executed by commands 

    public Map<String, EntityPrototype> getCards() {
        return Collections.unmodifiableMap(cards);
    }

    public EntityPrototype getEntityPrototype(GameEntity entity) {
        if (cards.containsKey(entity.getPrototypeId())) {
            return cards.get(entity.getPrototypeId());
        }
        return null;
    }

    public PlayerState getPlayerState(int playerPos) {
        return gameState.getPlayer(playerPos);
    }

    /**
     * Draw a card for a player, removing from their deck and adding it to their
     * hand. If triggerEvent is TRUE, produce the draw card event (the default, 
     * except for at the start of game).
     * 
     * @param player
     * @param triggerEvent Should this produce an event?
     * @return The card drawn
     */
    public GameEntity playerDrawCard(PlayerState player, boolean triggerEvent) {
        if (player.hand.size() >= MAX_CARDS) {
            return null;
        }
        if (player.deck.size() < 1) {
            return null;
        }
        GameEntity draw = player.deck.remove(0);
        draw.clearTag(EntityTag.IN_DECK);
        draw.setTag(EntityTag.IN_HAND);
        player.hand.add(draw);
        if (triggerEvent) {
            addEvent(new DrawCardEvent(player, draw));
            //processEvents();
        }
        return draw;
    }

    public GameEntity playerDrawCard(PlayerState player) {
        return playerDrawCard(player, true);
    }

    /**
     * Put a card at the top of a player's deck.
     * @param player
     * @param cardId
     * @return
     */
    public GameEntity addDeck(PlayerState player, String cardId) {
        GameEntity entity = createCard(cardId);
        entity.setOwner(player);
        player.deck.add(0, entity);
        return entity;
    }

    public void shuffleDeck(PlayerState player) {
        Collections.shuffle(player.deck);
    }

    /**
     * Remove the cards from the player's hand, and draw one for each card 
     * removed.
     * 
     * @param player
     * @param cards
     */
    public void mulligan(PlayerState player, List<GameEntity> cards) {
        // Iterate over a copy, to avoid concurrent mod if we're passed the hand:
        List<GameEntity> cardList = new ArrayList<>(cards);
        int count = cardList.size();
        // First, remove the cards from the player's hand:
        for (GameEntity card : cardList) {
            if (player.hand.contains(card)) {
                card.clearTag(EntityTag.IN_HAND);
                player.hand.remove(card);
                addEvent(new ReturnCardEvent(card));
            } else {
                throw new RuntimeException("Cannot mulligan card not in hand");
            }
        }
        // Then shuffle the deck and draw the appropriate number of cards:
        Collections.shuffle(player.deck);
        for (int i = 0; i < count; i++) {
            playerDrawCard(player);
        }
        // Finally return the cards to the deck:
        for (GameEntity card : cardList) {
            card.setTag(EntityTag.IN_DECK);
            player.deck.add(card);
        }
        Collections.shuffle(player.deck);
    }

    public void addRule(GameEntity entity, String ruleId) {
        if (!entityRules.containsKey(ruleId)) {
            throw new RuntimeException("Unknown ruleId '" + ruleId + "'");
        }
        EntityRule rule = entityRules.get(ruleId);
        entity.addRule(rule);
        recalculateStats();
    }

    /**
     * Summon a specific unit by id for a player at a location.
     * @param player
     * @param cardId
     * @param x
     * @param y
     * @return
     */
    public GameEntity summonUnit(PlayerState player, String cardId, int x, int y) {
        EntityCoordinates coord = new EntityCoordinates(x, y);
        GameEntity unit = summonUnit(cardId, coord);
        unit.setOwner(player);
        return unit;
    }

    public GameEntity summonUnit(PlayerState player, String cardId, EntityCoordinates coord) {
        GameEntity unit = summonUnit(cardId, coord);
        unit.setOwner(player);
        return unit;
    }

    public GameEntity summonUnit(String cardId, int x, int y) {
        return summonUnit(cardId, new EntityCoordinates(x, y));
    }

    /**
     * Summon an uncontrolled unit at the supplied location
     * @param cardId
     * @param x
     * @param y
     * @return
     */
    public GameEntity summonUnit(String cardId, EntityCoordinates coord) {
        GameEntity unit = createCard(cardId);
        unit.clearTag(EntityTag.IN_DECK);
        gameState.placeEntity(unit, coord);
        EntityPrototype prototype = cards.get(unit.getPrototypeId());
        addEvent(new SummonUnitEvent(unit, prototype));
        return unit;
    }

    public void moveUnit(GameEntity entity, EntityCoordinates coord) {
        gameState.placeEntity(entity, coord);
        addEvent(new MoveEvent(entity, coord));
    }

    public void dealDamage(GameEntity cause, GameEntity target, int amount) {
        String damagePrefab = "Default Attack";
        EntityPrototype prototype = cards.get(cause.getPrototypeId());
        if (!prototype.getProjectile().isEmpty()) {
            damagePrefab = prototype.getProjectile();
        }
        dealDamage(cause, target, amount, damagePrefab);
    }

    public void dealDamage(GameEntity cause, GameEntity target, int amount, String prefabName) {
        int blocked = target.getCurrentShields();
        if (blocked > amount)
            blocked = amount;
        target.dischargeShields(blocked);
        int damageDealt = amount - blocked;
        target.damage(damageDealt);
        DamageEffect damage = new DamageEffect(target.getId(), damageDealt, blocked);
        addEvent(new DamageCausedEvent(cause, damage, prefabName));
        if (damage.damageTaken > 0) {
            addEvent(new DamageTakenEvent(target, damage));
        }
        if (target.getCurrentHealth() <= 0) {
            killEntity(target);
        }
    }

    public void repair(GameEntity target, int amount) {
        target.repair(amount);
    }

    /**
     * Attempts to recharge the entity's shields by amount. If the shields 
     * were recharged by more than 0, adds an event.
     * 
     * @param entity
     * @param amount
     */
    public void rechargeShields(GameEntity entity, int amount) {
        amount = entity.rechargeShields(amount);
        if (amount > 0) {
            addEvent(new ShieldRechargeEvent(entity, amount));
        }
    }

    public void killEntity(GameEntity entity) {
        removeEntity(entity);
        addEvent(new DeathEvent(entity));
    }

    public void clientEvent(String prefab, GameEntity entity) {
        addEvent(new ClientPrefabEvent(prefab, entity));
    }

    public void clientEvent(String prefab) {
        addEvent(new ClientPrefabEvent(prefab, null));
    }

    GameEntity spawnEntity() {
        return spawnEntity(null);
    }

    GameEntity spawnEntity(EntityPrototype prototype) {
        GameEntity entity = gameState.createEntity(prototype);
        return entity;
    }

    public GameEntity getEntity(int id) {
        return gameState.getEntity(id);
    }

    void removeEntity(GameEntity entity) {
        entity.setTag(EntityTag.REMOVED);
    }

    public GameState getGameState() {
        return gameState;
    }

    public PlayerState getOpponent() {
        return gameState.getOpponent(gameState.getCurrentPlayer());
    }

    public PlayerState getOpponent(PlayerState player) {
        return gameState.getOpponent(player);
    }

    public GameEntity getHomeworld(PlayerState player) {
        return gameState.getEntity(player.homeworldId);
    }

    public int getEnergy(PlayerState player) {
        return player.getResource(EntityStat.ENERGY_RESOURCE);
    }

    public int getMinerals(PlayerState player) {
        return player.getResource(EntityStat.MINERAL_RESOURCE);
    }

    /**
     * Returns a stream of all entities in play (on board, plus invisible entities such as root 
     * and powers that haven't been cleaned up).
     * @return
     */
    public Stream<GameEntity> inPlay() {
        return gameState.getInPlay().stream();
    }

    /**
     * Returns a stream of all units on the board.
     * @return
     */
    public Stream<GameEntity> onBoard() {
        return gameState.getAllEntities().stream().filter(x -> x.onBoard() && !x.isRemoved());
    }

    /**
     * Returns the entity at the supplied coordinates or null if empty:
     * 
     * @param coord
     * @return
     */
    public GameEntity getEntityAt(EntityCoordinates coord) {
        return gameState.getEntityAt(coord);
    }

    public GameEntity getEntityAt(int x, int y) {
        EntityCoordinates coord = new EntityCoordinates(x, y);
        return getEntityAt(coord);
    }

    /**
     * Return true if the supplied coordinates are on the board.
     * 
     * @param coord
     * @return
     */
    public boolean isOnBoard(EntityCoordinates coord) {
        return gameState.isOnBoard(coord);
    }

    /**
     * Returns a stream of all units on the board owned by owner.
     * 
     * @param owner
     * @return
     */
    public Stream<GameEntity> onBoard(PlayerState owner) {
        return gameState.getAllEntities().stream().filter(x -> x.onBoard() && x.getOwner() == owner);
    }

    /**
     * Return a list of all entities on the board matching the predicate
     * 
     * @param predicate
     * @return
     */
    public List<GameEntity> onBoard(Predicate<GameEntity> predicate) {
        List<GameEntity> entities = onBoard().collect(Collectors.toList());
        Collections.shuffle(entities);
        return entities.stream().filter(predicate).collect(Collectors.toList());
    }

    /**
     * Returns a stream of all units on the board that are owned by the enemy of owner.
     * @param owner
     * @return
     */
    public Stream<GameEntity> enemyUnits(PlayerState owner) {
        return onBoard(gameState.getOpponent(owner));
    }

    /**
     * Return a random unit on the board that matches the supplied predicate, or null.
     * 
     * @param predicate
     * @return
     */
    public GameEntity randomUnit(Predicate<GameEntity> predicate) {
        List<GameEntity> entities = onBoard().collect(Collectors.toList());
        Collections.shuffle(entities);
        return entities.stream().filter(predicate).findFirst().orElse(null);
    }

    public EntityCoordinates randomCoordinates(Predicate<EntityCoordinates> predicate) {
        List<EntityCoordinates> coordinates = new ArrayList<EntityCoordinates>(gameState.getAllCoordinates());
        Collections.shuffle(coordinates);
        return coordinates.stream().filter(predicate).findFirst().orElse(null);
    }

    /**
     * Get a list of coordinates that border the supplied coordinates.
     * @param coord
     * @return
     */
    public List<EntityCoordinates> getNeighbors(EntityCoordinates coord, boolean includeSelf) {
        List<EntityCoordinates> neighbors = new ArrayList<>();
        if (includeSelf) {
            neighbors.add(coord);
        }
        for (HexDirection dir : HexDirection.values()) {
            EntityCoordinates candidate = coord.add(dir);
            if (isOnBoard(candidate)) {
                neighbors.add(candidate);
            }
        }
        return neighbors;
    }

    public List<EntityCoordinates> getNeighbors(EntityCoordinates coord) {
        return getNeighbors(coord, false);
    }

    public List<GameEntity> getNeighborEntities(EntityCoordinates coord) {
        List<GameEntity> entities = new ArrayList<>();
        for (HexDirection dir : HexDirection.values()) {
            EntityCoordinates candidate = coord.add(dir);
            GameEntity e = getEntityAt(candidate);
            if (e != null)
                entities.add(e);
        }
        return entities;
    }

    /**
     * Are the supplied coordinates neighbors?
     * @param coord1
     * @param coord2
     * @return
     */
    public boolean areNeighbors(EntityCoordinates coord1, EntityCoordinates coord2) {
        for (HexDirection dir : HexDirection.values()) {
            if (coord1.add(dir).equals(coord2))
                return true;
        }
        return false;
    }

    public boolean areNeighbors(GameEntity entity1, GameEntity entity2) {
        if (entity1 == null || entity2 == null)
            return false;
        return areNeighbors(entity1.getCoordinates(), entity2.getCoordinates());
    }

    public void addEnergy(PlayerState player, int amount) {
        addResource(player, EntityStat.ENERGY_RESOURCE, amount);
        addEvent(new UpdateResourceEvent(player, EntityStat.ENERGY_RESOURCE,
                player.getResource(EntityStat.ENERGY_RESOURCE)));
    }

    public void addMinerals(PlayerState player, int amount) {
        addResource(player, EntityStat.MINERAL_RESOURCE, amount);
        addEvent(new UpdateResourceEvent(player, EntityStat.MINERAL_RESOURCE,
                player.getResource(EntityStat.MINERAL_RESOURCE)));
    }

    public void consumeEnergy(GameEntity consumer, int amount) {
        PlayerState player = consumer.getOwner();
        consumeResource(player, EntityStat.ENERGY_RESOURCE, amount);
        addEvent(new ConsumeResourceEvent(consumer, consumer.getOwner(), EntityStat.ENERGY_RESOURCE, amount));
    }

    public void consumeMinerals(GameEntity consumer, int amount) {
        PlayerState player = consumer.getOwner();
        consumeResource(player, EntityStat.MINERAL_RESOURCE, amount);
        addEvent(new ConsumeResourceEvent(consumer, consumer.getOwner(), EntityStat.MINERAL_RESOURCE, amount));
    }

    private void addResource(PlayerState player, EntityStat resource, int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount must be positve");
        }
        player.setResource(resource, player.getResource(resource) + amount);
    }

    /**
     * Deduct amount from the player's resource. 
     * 
     * @param player
     * @param resourceName
     * @param amount
     */
    private void consumeResource(PlayerState player, EntityStat resource, int amount) {
        int current = player.getResource(resource);
        current -= amount;
        if (current < 0) {
            throw new RuntimeException(resource + " consumption is greater than available.");
        }
        player.setResource(resource, current);
    }

    public void addEvent(GameEvent event) {
        gameState.eventQueue.add(event);
    }

    private void createPlayerDeck(PlayerState player, List<String> startingDeck) {
        player.deck = new ArrayList<GameEntity>();
        for (String id : startingDeck) {
            if (!cards.containsKey(id)) {
                throw new RuntimeException("Could not find card named '" + id + "' in card lookup.");
            }
            GameEntity entity = createCard(id);
            entity.setOwner(player);
            player.deck.add(entity);
        }
        Collections.shuffle(player.deck);
    }

    private void initPlayer(PlayerInstance player) {
        PlayerShip ship = player.getShip();
        if (ship == null) {
            throw new RuntimeException("Cannot init player without ship");
        }
        PlayerState playerState = gameState.getPlayer(player.getPosition());
        if (!playerState.isDeckInitialized() && gameSystem.usePlayerDecks) {
            createPlayerDeck(playerState, ship.getStartingDeck());
        }
        playerState.name = player.getPlayerName();
        playerState.shipPrefab = ship.getShipPrefab();

    }

    private GameEntity createCard(String cardId) {
        EntityPrototype prototype = cards.get(cardId);
        if (prototype == null) {
            throw new RuntimeException("Unable to find prototype '" + cardId + "'");
        }
        GameEntity entity = gameState.createEntity(prototype);
        entity.setTag(EntityTag.IN_DECK);
        return entity;
    }

    /**
     * The player concedes the game and loses
     * @param player
     * @return
     */
    public void concede(PlayerState player) {
        GameEntity homeworld = gameState.getEntity(player.homeworldId);
        removeEntity(homeworld);
        gameOverCheck();
    }

    /**
     * Check whether the game is over (one of the player's homeworlds removed), if so
     * mark the game as over and return true. Otherwise, return false;
     * @return
     */
    boolean gameOverCheck() {
        if (gameState.isGameOver())
            return true;
        for (PlayerState playerState : gameState.getPlayers()) {
            GameEntity homeworld = gameState.getEntity(playerState.homeworldId);
            if (homeworld == null || homeworld.isRemoved()) {
                PlayerState opponent = gameState.getOpponent(playerState);
                GameEntity opponentHomeworld = gameState.getEntity(opponent.homeworldId);
                if (opponentHomeworld == null || opponentHomeworld.isRemoved()) {
                    gameState.gameOver(null);
                    return true;
                } else {
                    gameState.gameOver(opponent);
                    return true;
                }
            }
        }
        return false;
    }

    void processEvents() {
        int i = 0;
        while (!gameState.eventQueue.isEmpty()) {
            GameEvent event = gameState.eventQueue.poll();
            // Iterate over a copy of entities to avoid ConcurrentModification exceptions
            // if a rule spawns an entity:
            List<GameEntity> entityList = new ArrayList<GameEntity>(gameState.getInPlay());
            for (GameEntity entity : entityList) {
                for (EntityRule rule : entity.getRules()) {
                    if (!entity.isRemoved() && !entity.isDisabled()) {
                        processRule(event, rule, entity);
                    }
                }
            }
            recalculateStats();
            recalculateMinerals(gameState.getCurrentPlayer());
            recalculateFickle();
            processedEvents.add(event);
            ++i;
            if (i > MAX_EVENTS) {
                throw new RuntimeException("Exceeded max events: " + MAX_EVENTS);
            }
        }
        // After all events are processed, remove any entities marked for removal and powers.
        List<GameEntity> entityList = new ArrayList<GameEntity>(gameState.getInPlay());
        for (GameEntity entity : entityList) {
            if (entity.isPower() || entity.isRemoved()) {
                gameState.removeEntity(entity);
            }
        }
        if (gameOverCheck()) {
            logger.info("Game over");
            processedEvents.add(new GameOverEvent(gameState.getWinner()));
            if (gameOverHandler != null) {
                gameOverHandler.gameOver();
            }
        }
    }

    /**
     * Change ownership of fickle units
     */
    private void recalculateFickle() {
        List<GameEntity> fickleEntities = gameState.getInPlayWithTag(EntityTag.FICKLE);
        PlayerState p1 = getPlayerState(1);
        PlayerState p2 = getPlayerState(2);
        for (GameEntity fickle : fickleEntities) {
            int influence = 0;
            for (GameEntity e : getNeighborEntities(fickle.getCoordinates())) {
                if (!e.isRemoved()) {
                    if (e.getOwner().position == 1) {
                        influence += e.getAttack();
                    }
                    if (e.getOwner().position == 2) {
                        influence -= e.getAttack();
                    }
                }
            }
            if (influence > 0) {
                fickle.setOwner(p1);
            } else if (influence < 0) {
                fickle.setOwner(p2);
            } else {
                fickle.setOwner(null);
            }
        }
    }

    /**
     * Evaluate the {@link EntityRule} attached to an entity, in the context of a 
     * triggering event.
     * 
     * @param event
     * @param rule
     * @param entity
     */
    private void processRule(GameEvent event, EntityRule rule, GameEntity entity) {
        try {
            if (rule.isTriggered(event)) {
                ScriptContext scriptContext = getScriptContext();
                Bindings scriptScope = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
                // Let the rule access the event, game and entity objects:
                scriptScope.put("event", event);
                scriptScope.put("card", null);
                scriptScope.put("entity", entity);
                scriptScope.put("coord", entity.getCoordinates());
                scriptScope.put("owner", entity.getOwner());
                getScriptEngine().eval(rule.getScript(), scriptContext);
            }
        } catch (final ScriptException se) {
            throw new RuntimeException("Error in rule: " + rule.getId(), se.getCause());
        } catch (Exception ex) {
            throw new RuntimeException(
                    "Unexpected error in processing rule: " + rule.getId() + ", " + ex.getMessage(), ex.getCause());
        }
    }

    boolean validatePlayCard(GameEntity card, EntityCoordinates coord, ValidatorScript validator) {
        try {
            ScriptContext scriptContext = getScriptContext();
            Bindings scriptScope = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
            GameEntity target = getEntityAt(coord);
            scriptScope.put("event", null);
            scriptScope.put("card", card);
            scriptScope.put("owner", card.getOwner());
            scriptScope.put("coord", coord);
            scriptScope.put("target", target);
            scriptScope.put("error", null);
            scriptScope.put("valid", false);
            getScriptEngine().eval(validator.getScript(), scriptContext);
            boolean valid = (boolean) scriptScope.get("valid");
            return valid;
        } catch (final ScriptException se) {
            logger.error("Scripting exception: " + se.getMessage());
            return false;
        } catch (Exception ex) {
            logger.error("Non-scripting exception: " + ex.getMessage());
            return false;
        }
    }

    boolean validateDiscard(GameEntity card, GameEntity target, ValidatorScript validator) {
        try {
            ScriptContext scriptContext = getScriptContext();
            Bindings scriptScope = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
            scriptScope.put("event", null);
            scriptScope.put("card", card);
            scriptScope.put("owner", target.getOwner());
            scriptScope.put("target", target);
            scriptScope.put("error", null);
            scriptScope.put("valid", false);
            getScriptEngine().eval(validator.getScript(), scriptContext);
            boolean valid = (boolean) scriptScope.get("valid");
            return valid;
        } catch (final ScriptException se) {
            logger.error("Scripting exception: " + se.getMessage());
            return false;
        } catch (Exception ex) {
            logger.error("Non-scripting exception: " + ex.getMessage());
            return false;
        }
    }

    /**
     * Resets all entities' stats to their base values, then evaluates
     * all buff rules to update them.
     */
    void recalculateStats() {
        int beforeSize = gameState.eventQueue.size();
        // First, reset all stats. This sets each stat to the base value:
        for (GameEntity entity : gameState.getAllEntities()) {
            entity.resetStats();
        }
        // Then iterate over all entities in play and trigger any buff rules. These rules
        // may modify one or more entity's stats:
        for (GameEntity entity : gameState.getInPlay()) {
            for (EntityRule rule : entity.getRules()) {
                if (rule.getEventTrigger().equals(BUFF_PHASE)) {
                    try {
                        ScriptContext scriptContext = getScriptContext();
                        Bindings scriptScope = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
                        scriptScope.put("event", null);
                        scriptScope.put("rules", this);
                        scriptScope.put("entity", entity);
                        scriptScope.put("coord", entity.getCoordinates());
                        scriptScope.put("owner", entity.getOwner());
                        getScriptEngine().eval(rule.getScript(), scriptContext);
                    } catch (Exception ex) {
                        throw new RuntimeException("Exception processing buff " + rule + ":" + ex.getMessage());
                    }
                    if (gameState.eventQueue.size() != beforeSize) {
                        throw new RuntimeException(rule + " modified event queue, not allowed during buff phase.");
                    }
                }
            }
        }
    }

}