forge.game.Game.java Source code

Java tutorial

Introduction

Here is the source code for forge.game.Game.java

Source

/*
 * Forge: Play Magic: the Gathering.
 * Copyright (C) 2011  Forge Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package forge.game;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.eventbus.EventBus;

import forge.card.CardRarity;
import forge.card.CardType.Supertype;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CardView;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.event.Event;
import forge.game.event.GameEventGameOutcome;
import forge.game.phase.Phase;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.phase.Upkeep;
import forge.game.player.IGameEntitiesFactory;
import forge.game.player.Player;
import forge.game.player.PlayerView;
import forge.game.player.RegisteredPlayer;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.Ability;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.SpellAbilityView;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.game.zone.MagicStack;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.trackable.Tracker;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import forge.util.Visitor;
import java.util.HashMap;

/**
 * Represents the state of a <i>single game</i>, a new instance is created for each game.
 */
public class Game {
    private final GameRules rules;
    private final FCollection<Player> allPlayers = new FCollection<Player>();
    private final FCollection<Player> ingamePlayers = new FCollection<Player>();

    private List<Card> activePlanes = null;

    public final Phase cleanup;
    public final Phase endOfCombat;
    public final Phase endOfTurn;
    public final Untap untap;
    public final Upkeep upkeep;
    public final MagicStack stack;
    private final PhaseHandler phaseHandler;
    private final StaticEffects staticEffects = new StaticEffects();
    private final TriggerHandler triggerHandler = new TriggerHandler(this);
    private final ReplacementHandler replacementHandler = new ReplacementHandler(this);
    private final EventBus events = new EventBus("game events");
    private final GameLog gameLog = new GameLog();

    private final Zone stackZone = new Zone(ZoneType.Stack, this);

    private Direction turnOrder = Direction.getDefaultDirection();

    private long timestamp = 0;
    public final GameAction action;
    private final Match match;
    private GameStage age = GameStage.BeforeMulligan;
    private GameOutcome outcome;

    private final GameView view;
    private final Tracker tracker = new Tracker();

    public final Ability PLAY_LAND_SURROGATE = new Ability(null, (Cost) null) {
        @Override
        public boolean canPlay() {
            return true; //if this ability is added anywhere, it can be assumed that land can be played
        }

        @Override
        public void resolve() {
            throw new RuntimeException("This ability is intended to indicate \"land to play\" choice only");
        }

        @Override
        public String toUnsuppressedString() {
            return "Play land";
        }
    };

    private final GameEntityCache<Player, PlayerView> playerCache = new GameEntityCache<>();

    public Player getPlayer(PlayerView playerView) {
        return playerCache.get(playerView);
    }

    public void addPlayer(int id, Player player) {
        playerCache.put(Integer.valueOf(id), player);
    }

    private final GameEntityCache<Card, CardView> cardCache = new GameEntityCache<>();

    public Card getCard(CardView cardView) {
        return cardCache.get(cardView);
    }

    public void addCard(int id, Card card) {
        cardCache.put(Integer.valueOf(id), card);
    }

    public CardCollection getCardList(Iterable<CardView> cardViews) {
        CardCollection list = new CardCollection();
        cardCache.addToList(cardViews, list);
        return list;
    }

    // methods that deal with saving, retrieving and clearing LKI information about cards on zone change
    private final HashMap<Integer, Card> changeZoneLKIInfo = new HashMap<>();

    public final void setChangeZoneLKIInfo(Card c) {
        if (c == null) {
            return;
        }
        changeZoneLKIInfo.put(c.getId(), CardUtil.getLKICopy(c));
    }

    public final Card getChangeZoneLKIInfo(Card c) {
        if (c == null) {
            return null;
        }
        return changeZoneLKIInfo.containsKey(c.getId()) ? changeZoneLKIInfo.get(c.getId()) : c;
    }

    public final void clearChangeZoneLKIInfo() {
        changeZoneLKIInfo.clear();
    }

    private final GameEntityCache<SpellAbility, SpellAbilityView> spabCache = new GameEntityCache<>();

    public SpellAbility getSpellAbility(final SpellAbilityView view) {
        return spabCache.get(view);
    }

    public void addSpellAbility(int id, SpellAbility spellAbility) {
        spabCache.put(Integer.valueOf(id), spellAbility);
    }

    public Game(List<RegisteredPlayer> players0, GameRules rules0, Match match0) { /* no more zones to map here */
        rules = rules0;
        match = match0;

        spabCache.put(PLAY_LAND_SURROGATE.getId(), PLAY_LAND_SURROGATE);

        int highestTeam = -1;
        for (RegisteredPlayer psc : players0) {
            // Track highest team number for auto assigning unassigned teams
            int teamNum = psc.getTeamNumber();
            if (teamNum > highestTeam) {
                highestTeam = teamNum;
            }
        }

        int plId = 0;
        for (RegisteredPlayer psc : players0) {
            IGameEntitiesFactory factory = (IGameEntitiesFactory) psc.getPlayer();
            Player pl = factory.createIngamePlayer(this, plId++);
            allPlayers.add(pl);
            ingamePlayers.add(pl);

            pl.setStartingLife(psc.getStartingLife());
            pl.setMaxHandSize(psc.getStartingHand());
            pl.setStartingHandSize(psc.getStartingHand());

            int teamNum = psc.getTeamNumber();
            if (teamNum == -1) {
                // RegisteredPlayer doesn't have an assigned team, set it to 1 higher than the highest found team number
                teamNum = ++highestTeam;
                psc.setTeamNumber(teamNum);
            }

            pl.setTeam(teamNum);
        }

        action = new GameAction(this);
        stack = new MagicStack(this);
        phaseHandler = new PhaseHandler(this);

        untap = new Untap(this);
        upkeep = new Upkeep(this);
        cleanup = new Phase(PhaseType.CLEANUP);
        endOfCombat = new Phase(PhaseType.COMBAT_END);
        endOfTurn = new Phase(PhaseType.END_OF_TURN);

        view = new GameView(this);

        subscribeToEvents(gameLog.getEventVisitor());
    }

    public GameView getView() {
        return view;
    }

    public Tracker getTracker() {
        return tracker;
    }

    /**
     * Gets the players who are still fighting to win.
     */
    public final FCollectionView<Player> getPlayers() {
        return ingamePlayers;
    }

    /**
     * Gets the players who are still fighting to win, in turn order.
     */
    public final FCollectionView<Player> getPlayersInTurnOrder() {
        if (turnOrder.isDefaultDirection()) {
            return ingamePlayers;
        }
        final FCollection<Player> players = new FCollection<Player>(ingamePlayers);
        Collections.reverse(players);
        return players;
    }

    /**
     * Gets the nonactive players who are still fighting to win, in turn order.
     */
    public final FCollectionView<Player> getNonactivePlayers() {
        // Don't use getPlayersInTurnOrder to prevent copying the player collection twice
        final FCollection<Player> players = new FCollection<>(ingamePlayers);
        players.remove(phaseHandler.getPlayerTurn());
        if (!turnOrder.isDefaultDirection()) {
            Collections.reverse(players);
        }
        return players;
    }

    /**
     * Gets the players who participated in match (regardless of outcome).
     * <i>Use this in UI and after match calculations</i>
     */
    public final List<Player> getRegisteredPlayers() {
        return allPlayers;
    }

    public final Untap getUntap() {
        return untap;
    }

    public final Upkeep getUpkeep() {
        return upkeep;
    }

    public final Phase getEndOfCombat() {
        return endOfCombat;
    }

    public final Phase getEndOfTurn() {
        return endOfTurn;
    }

    public final Phase getCleanup() {
        return cleanup;
    }

    public final PhaseHandler getPhaseHandler() {
        return phaseHandler;
    }

    public final void updateTurnForView() {
        view.updateTurn(phaseHandler);
    }

    public final void updatePhaseForView() {
        view.updatePhase(phaseHandler);
    }

    public final void updatePlayerTurnForView() {
        view.updatePlayerTurn(phaseHandler);
    }

    public final MagicStack getStack() {
        return stack;
    }

    public final void updateStackForView() {
        view.updateStack(stack);
    }

    public final StaticEffects getStaticEffects() {
        return staticEffects;
    }

    public final TriggerHandler getTriggerHandler() {
        return triggerHandler;
    }

    public final Combat getCombat() {
        return getPhaseHandler().getCombat();
    }

    public final void updateCombatForView() {
        view.updateCombat(getCombat());
    }

    public final GameLog getGameLog() {
        return gameLog;
    }

    public final void updateGameLogForView() {
        view.updateGameLog(gameLog);
    }

    public final Zone getStackZone() {
        return stackZone;
    }

    public CardCollectionView getCardsPlayerCanActivateInStack() {
        return CardLists.filter(stackZone.getCards(), new Predicate<Card>() {
            @Override
            public boolean apply(final Card c) {
                for (final SpellAbility sa : c.getSpellAbilities()) {
                    final ZoneType restrictZone = sa.getRestrictions().getZone();
                    if (ZoneType.Stack == restrictZone) {
                        return true;
                    }
                }
                return false;
            }
        });
    }

    /**
     * The Direction in which the turn order of this Game currently proceeds.
     */
    public final Direction getTurnOrder() {
        return turnOrder;
    }

    public final void reverseTurnOrder() {
        turnOrder = turnOrder.getOtherDirection();
    }

    public final void resetTurnOrder() {
        turnOrder = Direction.getDefaultDirection();
    }

    /**
     * Create and return the next timestamp.
     */
    public final long getNextTimestamp() {
        timestamp = getTimestamp() + 1;
        return getTimestamp();
    }

    public final long getTimestamp() {
        return timestamp;
    }

    public final GameOutcome getOutcome() {
        return outcome;
    }

    public ReplacementHandler getReplacementHandler() {
        return replacementHandler;
    }

    public synchronized boolean isGameOver() {
        return age == GameStage.GameOver;
    }

    public synchronized void setGameOver(GameEndReason reason) {
        age = GameStage.GameOver;
        for (Player p : allPlayers) {
            p.setMindSlaveMaster(null); // for correct totals
        }

        for (Player p : getPlayers()) {
            p.onGameOver();
        }

        final GameOutcome result = new GameOutcome(reason, getRegisteredPlayers());
        result.setTurnsPlayed(getPhaseHandler().getTurn());

        outcome = result;
        match.addGamePlayed(this);

        view.updateGameOver(this);

        // The log shall listen to events and generate text internally
        fireEvent(new GameEventGameOutcome(result, match.getPlayedGames()));
    }

    public Zone getZoneOf(final Card card) {
        return card.getZone();
    }

    public synchronized CardCollectionView getCardsIn(final ZoneType zone) {
        if (zone == ZoneType.Stack) {
            return getStackZone().getCards();
        } else {
            CardCollection cards = new CardCollection();
            for (final Player p : getPlayers()) {
                cards.addAll(p.getZone(zone).getCards());
            }
            return cards;
        }
    }

    public CardCollectionView getCardsIncludePhasingIn(final ZoneType zone) {
        if (zone == ZoneType.Stack) {
            return getStackZone().getCards();
        } else {
            CardCollection cards = new CardCollection();
            for (final Player p : getPlayers()) {
                cards.addAll(p.getCardsIncludePhasingIn(zone));
            }
            return cards;
        }
    }

    public CardCollectionView getCardsIn(final Iterable<ZoneType> zones) {
        CardCollection cards = new CardCollection();
        for (final ZoneType z : zones) {
            cards.addAll(getCardsIn(z));
        }
        return cards;
    }

    public boolean isCardExiled(final Card c) {
        return getCardsIn(ZoneType.Exile).contains(c);
    }

    public boolean isCardInPlay(final String cardName) {
        for (final Player p : getPlayers()) {
            if (p.isCardInPlay(cardName)) {
                return true;
            }
        }
        return false;
    }

    public boolean isCardInCommand(final String cardName) {
        for (final Player p : getPlayers()) {
            if (p.isCardInCommand(cardName)) {
                return true;
            }
        }
        return false;
    }

    public CardCollectionView getColoredCardsInPlay(final String color) {
        final CardCollection cards = new CardCollection();
        for (Player p : getPlayers()) {
            cards.addAll(p.getColoredCardsInPlay(color));
        }
        return cards;
    }

    public Card getCardState(final Card card) {
        for (final Card c : getCardsInGame()) {
            if (card.equals(c)) {
                return c;
            }
        }
        return card;
    }

    // Allows visiting cards in game without allocating a temporary list.
    public void forEachCardInGame(Visitor<Card> visitor) {
        for (final Player player : getPlayers()) {
            visitor.visitAll(player.getZone(ZoneType.Graveyard).getCards());
            visitor.visitAll(player.getZone(ZoneType.Hand).getCards());
            visitor.visitAll(player.getZone(ZoneType.Library).getCards());
            visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false));
            visitor.visitAll(player.getZone(ZoneType.Exile).getCards());
            visitor.visitAll(player.getZone(ZoneType.Command).getCards());
        }
        visitor.visitAll(getStackZone().getCards());
    }

    public CardCollectionView getCardsInGame() {
        final CardCollection all = new CardCollection();
        Visitor<Card> visitor = new Visitor<Card>() {
            @Override
            public void visit(Card card) {
                all.add(card);
            }
        };
        forEachCardInGame(visitor);
        return all;
    }

    public final GameAction getAction() {
        return action;
    }

    public final Match getMatch() {
        return match;
    }

    /**
     * Get the player whose turn it is after a given player's turn, taking turn
     * order into account.
     * @param playerTurn a {@link Player}, or {@code null}.
     * @return A {@link Player}, whose turn comes after the current player, or
     * {@code null} if there are no players in the game.
     */
    public Player getNextPlayerAfter(final Player playerTurn) {
        return getNextPlayerAfter(playerTurn, turnOrder);
    }

    /**
     * Get the player whose turn it is after a given player's turn, taking turn
     * order into account.
     * @param playerTurn a {@link Player}, or {@code null}.
     * @param turnOrder a {@link Direction}
     * @return A {@link Player}, whose turn comes after the current player, or
     * {@code null} if there are no players in the game.
     */
    public Player getNextPlayerAfter(final Player playerTurn, final Direction turnOrder) {
        int iPlayer = ingamePlayers.indexOf(playerTurn);

        if (ingamePlayers.isEmpty()) {
            return null;
        }

        final int shift = turnOrder.getShift();
        if (-1 == iPlayer) { // if playerTurn has just lost
            final int totalNumPlayers = allPlayers.size();
            int iAlive;
            iPlayer = allPlayers.indexOf(playerTurn);
            do {
                iPlayer = (iPlayer + shift) % totalNumPlayers;
                if (iPlayer < 0) {
                    iPlayer += totalNumPlayers;
                }
                iAlive = ingamePlayers.indexOf(allPlayers.get(iPlayer));
            } while (iAlive < 0);
            iPlayer = iAlive;
        } else { // for the case playerTurn hasn't died
            final int numPlayersInGame = ingamePlayers.size();
            iPlayer = (iPlayer + shift) % numPlayersInGame;
            if (iPlayer < 0) {
                iPlayer += numPlayersInGame;
            }
        }

        return ingamePlayers.get(iPlayer);
    }

    public int getPosition(Player player, Player startingPlayer) {
        int startPosition = ingamePlayers.indexOf(startingPlayer);
        int position = (ingamePlayers.indexOf(player) + startPosition) % ingamePlayers.size() + 1;
        return position;
    }

    public void onPlayerLost(Player p) {
        ingamePlayers.remove(p);

        final Map<String, Object> runParams = new TreeMap<String, Object>();
        runParams.put("Player", p);
        getTriggerHandler().runTrigger(TriggerType.LosesGame, runParams, false);
    }

    /**
     * Fire only the events after they became real for gamestate and won't get replaced.<br>
     * The events are sent to UI, log and sound system. Network listeners are under development.
     */
    public void fireEvent(final Event event) {
        events.post(event);
    }

    public void subscribeToEvents(final Object subscriber) {
        events.register(subscriber);
    }

    public GameRules getRules() {
        return rules;
    }

    public List<Card> getActivePlanes() {
        return activePlanes;
    }

    public void setActivePlanes(List<Card> activePlane0) {
        activePlanes = activePlane0;
    }

    public void archenemy904_10() {
        //904.10. If a non-ongoing scheme card is face up in the
        //command zone, and it isn't the source of a triggered ability
        //that has triggered but not yet left the stack, that scheme card
        //is turned face down and put on the bottom of its owner's scheme
        //deck the next time a player would receive priority.
        //(This is a state-based action. See rule 704.)

        for (int i = 0; i < getCardsIn(ZoneType.Command).size(); i++) {
            Card c = getCardsIn(ZoneType.Command).get(i);
            if (c.isScheme() && !c.getType().hasSupertype(Supertype.Ongoing)) {
                boolean foundonstack = false;
                for (SpellAbilityStackInstance si : stack) {
                    if (si.getSourceCard().equals(c)) {
                        foundonstack = true;
                        break;
                    }
                }
                if (!foundonstack) {
                    getTriggerHandler().suppressMode(TriggerType.ChangesZone);
                    c.getController().getZone(ZoneType.Command).remove(c);
                    i--;
                    getTriggerHandler().clearSuppression(TriggerType.ChangesZone);

                    c.getController().getZone(ZoneType.SchemeDeck).add(c);
                }
            }
        }
    }

    public GameStage getAge() {
        return age;
    }

    public void setAge(GameStage value) {
        age = value;
    }

    private int cardIdCounter = 0, hiddenCardIdCounter = 0;

    public int nextCardId() {
        return ++cardIdCounter;
    }

    public int nextHiddenCardId() {
        return ++hiddenCardIdCounter;
    }

    public Multimap<Player, Card> chooseCardsForAnte(final boolean matchRarity) {
        Multimap<Player, Card> anteed = ArrayListMultimap.create();

        if (matchRarity) {

            boolean onePlayerHasTimeShifted = false;

            List<CardRarity> validRarities = new ArrayList<>(Arrays.asList(CardRarity.values()));
            for (final Player player : getPlayers()) {
                final Set<CardRarity> playerRarity = getValidRarities(player.getCardsIn(ZoneType.Library));
                if (!onePlayerHasTimeShifted) {
                    onePlayerHasTimeShifted = playerRarity.contains(CardRarity.Special);
                }
                validRarities.retainAll(playerRarity);
            }

            if (validRarities.size() == 0) { //If no possible rarity matches were found, use the original method to choose antes
                for (Player player : getPlayers()) {
                    chooseRandomCardsForAnte(player, anteed);
                }
                return anteed;
            }

            //If possible, don't ante basic lands
            if (validRarities.size() > 1) {
                validRarities.remove(CardRarity.BasicLand);
            }

            if (validRarities.contains(CardRarity.Special)) {
                onePlayerHasTimeShifted = false;
            }

            CardRarity anteRarity = validRarities.get(new Random().nextInt(validRarities.size()));

            System.out.println("Rarity chosen for ante: " + anteRarity.name());

            for (final Player player : getPlayers()) {
                CardCollection library = new CardCollection(player.getCardsIn(ZoneType.Library));
                CardCollection toRemove = new CardCollection();

                //Remove all cards that aren't of the chosen rarity
                for (Card card : library) {
                    if (onePlayerHasTimeShifted && card.getRarity() == CardRarity.Special) {
                        //Since Time Shifted cards don't have a traditional rarity, they're wildcards
                        continue;
                    } else if (anteRarity == CardRarity.MythicRare || anteRarity == CardRarity.Rare) {
                        //Rare and Mythic Rare cards are considered the same rarity, just as in booster packs
                        //Otherwise it's possible to never lose Mythic Rare cards if you choose opponents carefully
                        //It also lets you win Mythic Rare cards when you don't have any to ante
                        if (card.getRarity() != CardRarity.MythicRare && card.getRarity() != CardRarity.Rare) {
                            toRemove.add(card);
                        }
                    } else {
                        if (card.getRarity() != anteRarity) {
                            toRemove.add(card);
                        }
                    }
                }

                library.removeAll((Collection<?>) toRemove);

                if (library.size() > 0) { //Make sure that matches were found. If not, use the original method to choose antes
                    Card ante = library.get(new Random().nextInt(library.size()));
                    anteed.put(player, ante);
                } else {
                    chooseRandomCardsForAnte(player, anteed);
                }

            }
        } else {
            for (Player player : getPlayers()) {
                chooseRandomCardsForAnte(player, anteed);
            }
        }
        return anteed;
    }

    private void chooseRandomCardsForAnte(final Player player, final Multimap<Player, Card> anteed) {
        final CardCollectionView lib = player.getCardsIn(ZoneType.Library);
        Predicate<Card> goodForAnte = Predicates.not(CardPredicates.Presets.BASIC_LANDS);
        Card ante = Aggregates.random(Iterables.filter(lib, goodForAnte));
        if (ante == null) {
            getGameLog().add(GameLogEntryType.ANTE, "Only basic lands found. Will ante one of them");
            ante = Aggregates.random(lib);
        }
        anteed.put(player, ante);
    }

    private static Set<CardRarity> getValidRarities(final Iterable<Card> cards) {
        final Set<CardRarity> rarities = new HashSet<>();
        for (final Card card : cards) {
            if (card.getRarity() == CardRarity.Rare || card.getRarity() == CardRarity.MythicRare) {
                //Since both rare and mythic rare are considered the same, adding both rarities
                //massively increases the odds chances of the game picking rare cards to ante.
                //This is a little unfair, so we add just one of the two.
                rarities.add(CardRarity.Rare);
            } else {
                rarities.add(card.getRarity());
            }
        }
        return rarities;
    }
}