001    package core;
002    
003    import java.io.File;
004    import java.io.IOException;
005    import java.math.BigInteger;
006    import java.util.Arrays;
007    import java.util.HashMap;
008    import java.util.LinkedList;
009    import java.util.List;
010    import java.util.Map;
011    import java.util.Stack;
012    import java.util.Map.Entry;
013    
014    import org.apache.xmlbeans.XmlException;
015    
016    import rules.Board;
017    import rules.Empty;
018    import rules.GameState;
019    import rules.PieceFactory;
020    
021    /**
022     * A mutable class containing all the information about a game. This keeps track
023     * of game stuff that generally isn't game specific (like time, move history
024     * etc.)
025     * 
026     * @specfield game_state : Object // the current state of the board and related
027     *            data
028     * @specfield players : set // the 2 players playing the game and corresponding
029     *            colors
030     * @specfield current_player : player // the current player to move next
031     * @specfield move_history : list // the previous moves in this game
032     * @specfield time_for_players : ints // the amount of time left for each player
033     * @specfield winner : enum // if there is a winner, who it is
034     * @specfield win_reason : enum // if someone won, how they won
035     * @specfield xml : string // an xml representation of the board
036     * @specfield timed : boolean // wether or not the game is timed
037     * 
038     * Abstract Invariant: <br>
039     * <li> One player must be white and the other black (with white player first).
040     * <li> If there is a winner, there must be a reason, and vica versa.
041     * <li> If the time for a player is 0 or less, the other player is the winner.
042     * <li> If the game_state says that there is a winner for a given reason, then
043     * game must give the same winner and reason.
044     * <li> The outcome of move_history must yield the current game_state, and the
045     * same current player.
046     * <li> Saving the game completely captures all its state, and loading restores
047     * it
048     */
049    public class Game
050    {
051            // Abstraction Function:
052            // game_state -> gameState
053            // players -> players.values()
054            // current_player -> gameState.getCurrentPlayer()
055            // move_history -> moveHistory
056            // time_for_players -> time
057            // winner -> getWinner()
058            // win_reason -> getWinReason()
059            // xml -> getXML()
060            // timed -> timed
061            //
062            // Representation Invariant:
063            // None of the internal fields are null.
064            // Same as the abstraction invariants (as mapped through abstraction
065            // function).
066            // startTime for each player is > the time of the players first moves in
067            // moveHistory
068    
069            private GameState gameState;
070            final private Map<Color, Player> players;
071            final private Stack<HistoryElement> moveHistory;
072            final private Map<Player, Integer> time;
073            final private Map<Player, Integer> startTime;
074            private Color timeWinner = Color.NONE;
075            private boolean timed;
076    
077            /**
078             * Creates a new Game instance with Players and no times.
079             * 
080             * @param player1
081             *            the player that goes first, must have Color WHITE
082             * @param player2
083             *            the player that goes second, must have Color BLACK
084             * 
085             * @requires players not null
086             */
087            public Game(Player player1, Player player2)
088            {
089                    this(player1, -1, player2, -1);
090    
091                    timed = false;
092                    assert (checkRep());
093            }
094    
095            /**
096             * Creates a new Game instance with Players and starting times.
097             * 
098             * @param player1
099             *            the player that goes first, must have Color WHITE
100             * @param player1Time
101             *            time for player1
102             * @param player2
103             *            the player that goes second, must have Color BLACK
104             * @param player2Time
105             *            time for player2
106             * 
107             * @requires players not null
108             */
109            public Game(Player player1, int player1Time, Player player2, int player2Time)
110            {
111                    this(player1, player1Time, player2, player2Time, "");
112            }
113    
114            public Game(Player player1, int player1Time, Player player2,
115                            int player2Time, String powerups)
116            {
117                    assert (player1.getColor() == Color.WHITE);
118                    assert (player2.getColor() == Color.BLACK);
119    
120                    Map<Position, Piece> powerupMap = PowerupHelper.stringToMap(powerups);
121    
122                    gameState = new GameState(powerupMap);
123    
124                    players = new HashMap<Color, Player>(2);
125                    // assumes player 1 is white and player 2 is black
126                    players.put(Color.WHITE, player1);
127                    players.put(Color.BLACK, player2);
128    
129                    moveHistory = new Stack<HistoryElement>();
130                    if (player1Time == 0)
131                    {
132                            player1Time = -1;
133                    }
134                    if (player2Time == 0)
135                    {
136                            player2Time = -1;
137                    }
138                    startTime = new HashMap<Player, Integer>(2);
139                    startTime.put(player1, player1Time);
140                    startTime.put(player2, player2Time);
141                    time = new HashMap<Player, Integer>(startTime);
142    
143                    timed = true;
144    
145                    assert (checkRep());
146            }
147    
148            /**
149             * Checks to make sure as much as possible of rep invariant is satisfied.
150             */
151            private boolean checkRep()
152            {
153                    if (gameState == null || players == null || moveHistory == null
154                                    || time == null || startTime == null || timeWinner == null)
155                            throw new NullPointerException("Internal game fields are null.");
156    
157                    Color w = Color.WHITE, b = Color.BLACK, n = Color.NONE;
158                    Player wp = players.get(Color.WHITE);
159                    Player bp = players.get(Color.BLACK);
160    
161                    // One player must be white and the other black (with white player
162                    // first).
163                    assert (wp == getPlayers().get(0) && bp == getPlayers().get(1));
164                    // If there is a winner, there must be a reason, and vica
165                    // versa.
166                    assert ((getWinner() == n && getWinReason() == GameOver.NONE) || ((getWinner() != n && getWinReason() != GameOver.NONE)));
167                    // If the time for a player is 0, the other player
168                    // is the winner.
169                    assert (getWinReason() != GameOver.TIME_EXPIRED
170                                    || (time.get(wp) == 0 && getWinner() == b && getWinReason() == GameOver.TIME_EXPIRED) || (time
171                                    .get(bp) == 0
172                                    && getWinner() == w && getWinReason() == GameOver.TIME_EXPIRED));
173                    // If the game_state says that there is a winner for a given
174                    // reason, then game must give the same winner and reason.
175                    if (getWinReason() != GameOver.NONE)
176                    {
177                            assert ((getWinReason() == GameOver.CHECKMATE && gameState
178                                            .checkmater() != n)
179                                            || (getWinReason() == GameOver.PIECES_LOST && gameState
180                                                            .piecesWinner() != n) || getWinReason() == GameOver.TIME_EXPIRED);
181                    }
182                    // The outcome of move_history must yield the current
183                    // game_state, and the same current player.
184                    // Saving the game completely captures all its state,
185                    // and loading restores it
186                    // This can't actually be tested because it creates an infinite loop (as
187                    // loadGame calls this checkrep when making a new Game). If it could, it
188                    // would be:
189                    // try
190                    // {
191                    // Game loadedGame = loadGame(wp, bp, getXML());
192                    // assert (loadedGame.getCurrentPlayer() == getCurrentPlayer() &&
193                    // loadedGame.gameState
194                    // .boardString().equals(gameState.boardString()));
195                    // } catch (GameLoadException e)
196                    // {
197                    // throw new RuntimeException(e);
198                    // }
199                    return true;
200            }
201    
202            /**
203             * Loads a game given an xml file and the initial players. See the xml
204             * format in
205             * http://web.mit.edu/6.170/www/assignments/antichess/antichess.html#game-file-example
206             * 
207             * @param player1
208             *            white player
209             * @param player2
210             *            black player
211             * @param xmlFile
212             *            file with file format linked to above
213             * @throws GameLoadException
214             *             contains a user intended error message
215             * 
216             * @requires no arguments null
217             * 
218             * @returns loaded game
219             */
220            public static Game loadGame(Player player1, Player player2, File xmlFile)
221                            throws GameLoadException
222            {
223                    try
224                    {
225                            return loadGame(player1, player2, noNamespace.GameDocument.Factory
226                                            .parse(xmlFile));
227                    } catch (XmlException e)
228                    {
229                            throw new GameLoadException(
230                                            "Error processing game file. (xml error)", e);
231                    } catch (IOException e)
232                    {
233                            throw new GameLoadException(
234                                            "Error processing game file. (io error)", e);
235                    }
236            }
237    
238            /**
239             * Loads a game given an xml string and the initial players. See the xml
240             * format in
241             * http://web.mit.edu/6.170/www/assignments/antichess/antichess.html#game-file-example
242             * 
243             * @param player1
244             *            white player
245             * @param player2
246             *            black player
247             * @param xmlString
248             *            string with format linked to above
249             * @throws GameLoadException
250             *             contains a user intended error message
251             * 
252             * @requires no arguments null
253             * 
254             * @returns loaded game
255             */
256            public static Game loadGame(Player player1, Player player2, String xmlString)
257                            throws GameLoadException
258            {
259                    try
260                    {
261                            return loadGame(player1, player2, noNamespace.GameDocument.Factory
262                                            .parse(xmlString));
263                    } catch (XmlException e)
264                    {
265                            throw new GameLoadException(
266                                            "Error processing game file. (xml error)", e);
267                    }
268            }
269    
270            /**
271             * Loads a game given an xml document and the initial players.
272             * http://web.mit.edu/6.170/www/assignments/antichess/antichess.html#game-file-example
273             * To be used internally by other load methods.
274             * 
275             * @param player1
276             *            white player
277             * @param player2
278             *            black player
279             * @param xmlFile
280             *            file with file format linked to above
281             * @throws GameLoadException
282             *             contains a user intended error message
283             * 
284             * @requires no arguments null
285             * 
286             * @returns loaded game
287             */
288            private static Game loadGame(Player player1, Player player2,
289                            noNamespace.GameDocument gd) throws GameLoadException
290            {
291                    // Coding Conventions:
292                    // Fully qualified names for xml types, and abbreviations for their
293                    // instances.
294                    // Relative names for actual types, and names for their instances.
295    
296                    if (!gd.validate())
297                            throw new GameLoadException(
298                                            "Error processing game file. (validation error)");
299    
300                    noNamespace.GameDocument.Game g = gd.getGame();
301                    noNamespace.GameDocument.Game.Time t = g.getTime();
302                    Game game;
303                    if (t.getTimed())
304                            game = new Game(player1, t.getInitWhite().intValue(), player2, t
305                                            .getInitBlack().intValue());
306                    else
307                            game = new Game(player1, player2);
308    
309                    Board board = new Board();
310                    for (noNamespace.GameDocument.Game.Pieces.Square s : g.getPieces()
311                                    .getSquareList())
312                    {
313                            board.put(Position.fromString(s.getId()), PieceFactory
314                                            .pieceFromString(s.getPiece().toString(), Color
315                                                            .fromString(s.getSide().toString())));
316    
317                    }
318                    board = Board.fromString(board.toString());
319    
320                    int moves = 0;
321                    Color prev = Color.BLACK;
322                    for (noNamespace.GameDocument.Game.MoveHistory.Move m : g
323                                    .getMoveHistory().getMoveList())
324                    {
325                            Move move = Move.fromString(m.getValue());
326                            Player movingPlayer = game.getPlayerByColor(Color.valueOf(m
327                                            .getSide().toString().toUpperCase()));
328                            Color movingColor = movingPlayer.getColor();
329                            Player otherPlayer = game.getPlayerByColor(movingColor);
330    
331                            // check for pass
332                            if (movingColor == prev)
333                            {
334                                    game.moveHistory.push(game.new HistoryElement(movingColor
335    
336                                    .otherColor(), new Move(), game
337                                                    .getTimeLeftForPlayer(otherPlayer)));
338                                    moves++;
339                            }
340                            game.moveHistory.push(game.new HistoryElement(movingColor, move, m
341                                            .getTime().intValue()));
342    
343                            // check for moving into king or rook position
344                            // Position[] castlePositions = { Position.fromString("h1") };
345                            // for (Position p : castlePositions)
346                            // if (move.getTo().equals(p))
347                            board.put(move.getTo(), PieceFactory.newPiece(board.get(move
348                                            .getTo()), moves));
349    
350                            prev = movingColor;
351                            moves++;
352    
353                            // USELESS BECAUSE OF SCHEMA ISSUES
354                            // // if current player is not the next player to move, then current
355                            // // player must have passed, so have the gamestate pass
356                            // if (movingPlayer != game.getCurrentPlayer())
357                            // game.makeMove(new Move());
358                            // if (!game.getGameState().isLegal(move))
359                            // {
360                            // throw new RuntimeException("Illegal move " + move + " in load");
361                            // }
362                            // game.setTime(movingPlayer, m.getTime().intValue());
363                            // game.makeMove(move);
364                    }
365                    game.gameState = new GameState(prev.otherColor(), moves, board);
366                    game.setTime(player1, t.getCurrentWhite().intValue());
367                    game.setTime(player2, t.getCurrentBlack().intValue());
368    
369                    // for(Piece p : game.gameState.getPieces())
370                    // Debug.print(p.getLastMove());
371    
372                    // USELESS BECAUSE OF SCHEMA ISSUES
373                    // // Checks consistency of saved board and move info
374                    // if (!board.toString().equals(game.getGameState().boardString()))
375                    // throw new GameLoadException(
376                    // "Error processing game file. (board and move information
377                    // inconsistent)");
378                    //
379                    // // Checks consistency of game over message info
380                    noNamespace.GameDocument.Game.GameOver go = g.getGameOver();
381                    if (go != null)
382                    {
383                            // if (go.getDescription() == noNamespace.GameEnd.CHECKMATE
384                            // && game.gameState.checkmater() == Color.NONE)
385                            // throw new GameLoadException(
386                            // "Error processing game file. (game and winner information
387                            // inconsistent - checkmate)");
388                            // if (go.getDescription() == noNamespace.GameEnd.PIECES_LOST
389                            // && game.gameState.piecesWinner() == Color.NONE)
390                            // throw new GameLoadException(
391                            // "Error processing game file. (game and winner information
392                            // inconsistent - pieces lost)");
393                            //
394                            // else
395                            if (go.getDescription() == noNamespace.GameEnd.TIME_EXPIRED)
396                            {
397                                    game.timeWinner = Color.valueOf(go.getWinner().toString()
398                                                    .toUpperCase());
399    
400                                    if (game.getTimeLeftForPlayer(game
401                                                    .getPlayerByColor(game.timeWinner.otherColor())) > 0)
402                                            throw new GameLoadException(
403                                                            "Error processing game file. (game and winner information inconsistent - time expired)");
404                            }
405                    }
406                    return game;
407            }
408    
409            /**
410             * Takes an int and returns a BigInteger
411             * 
412             * @param i
413             *            an int
414             * 
415             * @returns the corresponding BigInteger
416             */
417            private BigInteger toBigInt(int i)
418            {
419                    return new BigInteger(String.valueOf(i));
420            }
421    
422            /**
423             * Returns the XML for the game as a string. See
424             * http://web.mit.edu/6.170/www/assignments/antichess/antichess.html#game-file-format
425             * for the format.
426             * 
427             * @return the xml string
428             */
429            public String getXML()
430            {
431                    assert (checkRep());
432                    // // Coding Conventions:
433                    // // Fully qualified names for xml types, and abbreviations for their
434                    // // instances.
435                    // // Relative names for actual types, and names for their instances.
436                    Player whitePlayer = getPlayerByColor(Color.WHITE);
437                    Player blackPlayer = getPlayerByColor(Color.BLACK);
438    
439                    noNamespace.GameDocument gd = noNamespace.GameDocument.Factory
440                                    .newInstance();
441    
442                    noNamespace.GameDocument.Game g = gd.addNewGame();
443                    g.setRuleset("6170-fall-2007");
444    
445                    noNamespace.GameDocument.Game.Time t = g.addNewTime();
446                    t.setTimed(timed);
447                    t.setInitWhite(toBigInt(startTime.get(whitePlayer)));
448                    t.setInitBlack(toBigInt(startTime.get(blackPlayer)));
449                    t.setCurrentBlack(toBigInt(time.get(blackPlayer)));
450                    t.setCurrentWhite(toBigInt(time.get(whitePlayer)));
451    
452                    noNamespace.GameDocument.Game.MoveHistory mh = g.addNewMoveHistory();
453                    noNamespace.GameDocument.Game.MoveHistory.Move m;
454                    for (HistoryElement historyElement : moveHistory)
455                    {
456                            if (historyElement.move.isPass())
457                            {
458                                    continue;
459                            }
460    
461                            m = mh.addNewMove();
462                            m.setSide(noNamespace.Side.Enum.forString(historyElement.side
463                                            .toString()));
464                            m.setValue(historyElement.move.toString());
465                            m.setTime(toBigInt(historyElement.time));
466                    }
467    
468                    noNamespace.GameDocument.Game.Pieces p = g.addNewPieces();
469                    noNamespace.GameDocument.Game.Pieces.Square s;
470                    for (Entry<Position, ? extends Piece> entry : gameState.getPieceMap()
471                                    .entrySet())
472                    {
473                            if (entry.getValue() instanceof Empty)
474                                    continue;
475                            s = p.addNewSquare();
476                            s.setId(entry.getKey().toString());
477                            s.setSide(noNamespace.Side.Enum.forString(entry.getValue()
478                                            .getColor().toString()));
479                            s.setPiece(noNamespace.Piece.Enum.forString(entry.getValue()
480                                            .pieceString()));
481                    }
482    
483                    if (getWinner() != Color.NONE)
484                    {
485                            noNamespace.GameDocument.Game.GameOver go = g.addNewGameOver();
486                            go.setWinner(noNamespace.Side.Enum
487                                            .forString(getWinner().toString()));
488                            go.setDescription(noNamespace.GameEnd.Enum.forString(getWinReason()
489                                            .toString()));
490                    }
491    
492                    assert (gd.validate());
493                    return "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"
494                                    + gd.xmlText();
495            }
496    
497            /**
498             * A helper datastructure for move history
499             * 
500             * @specfield side : color // the color of the moving player
501             * @specfield move : move // the move itself
502             * @specfield time : int // the time of the player after the move
503             * 
504             * Abstract Invariant: No elements are null
505             */
506            class HistoryElement
507            {
508                    final Color side;
509                    final Move move;
510                    final int time;
511    
512                    public HistoryElement(Color s, Move m, int t)
513                    {
514                            side = s;
515                            move = m;
516                            time = t;
517                    }
518            }
519    
520            /**
521             * Makes a move (if it is valid), also saving it the move history (if it is
522             * not a pass). <br>
523             * Note that you need to decrement the time separately (beforehand), in
524             * order to work with the tui spec.
525             * 
526             * @param move
527             *            the move to make
528             * 
529             * @requires move not null, time for the player is decremented beforehand,
530             *           the move is valid
531             * 
532             * @modifies game_state, current_player, xml + possibly: move_history,
533             *           winner, win_reason
534             * 
535             * @effects makes a move, updating all relavent fields
536             * 
537             * @throws RuntimeException
538             *             if the move is invalid
539             */
540            public void makeMove(Move move)
541            {
542                    assert (checkRep());
543                    if (getWinner() != Color.NONE)
544                    {
545                            throw new RuntimeException(
546                                            "Trying to make a move on a game that has ended.");
547                    }
548    
549                    moveHistory.push(new HistoryElement(gameState.getCurrentColor(), move,
550                                    time.get(players.get(gameState.getCurrentColor()))));
551                    gameState.makeMove(move);
552                    assert (checkRep());
553            }
554    
555            /**
556             * Returns a mutable copy of the gamestate (changing it will change the
557             * game).
558             * 
559             * @returns the current gamestate (not immutable)
560             */
561            public GameState getGameState()
562            {
563                    return gameState;
564            }
565    
566            /**
567             * History of moves that have been made so far, from first to last.
568             * 
569             * @returns the list if moves
570             */
571            public List<Move> getMoveHistory()
572            {
573                    List<Move> list = new LinkedList<Move>();
574                    for (HistoryElement h : moveHistory)
575                            list.add(h.move);
576                    return list;
577            }
578    
579            /**
580             * Gives the winner if there is one.
581             * 
582             * @returns the color (enum) of the winner (NONE if no winner)
583             */
584            public Color getWinner()
585            {
586                    if (gameState.getWinner() != Color.NONE)
587                            return gameState.getWinner();
588                    else
589                            return timeWinner;
590            }
591    
592            /**
593             * Gives the reason for winning if there is one.
594             * 
595             * @returns the reason for winning (as a GameOver enum), or NONE if no one
596             *          has won.
597             */
598            public GameOver getWinReason()
599            {
600                    if (gameState.checkmater() != Color.NONE)
601                            return GameOver.CHECKMATE;
602                    else if (gameState.piecesWinner() != Color.NONE)
603                            return GameOver.PIECES_LOST;
604                    else if (timeWinner != Color.NONE)
605                            return GameOver.TIME_EXPIRED;
606                    else
607                            return GameOver.NONE;
608            }
609    
610            /**
611             * Sets the time for a player. If the game isn't timed, does nothing. If the
612             * game is timed and the player runs out of time, sets the other player as
613             * the winner.
614             * 
615             * @param player
616             *            The player whose time to change
617             * @param t
618             *            The new time
619             * 
620             * @requires final time is > 0 or == -1, player is one of the player
621             * 
622             * @modifies time_for_players, winner, win_reason, xml
623             * 
624             * @effects sets the new time for the player and sets the winner if there is
625             *          one.
626             */
627            public void setTime(Player player, int t)
628            {
629                    assert (checkRep());
630                    assert (players.containsValue(player));
631                    if (timed)
632                    {
633                            if (time.get(player) == -1)
634                            { // Player is untimed
635                            } else if (t > 0)
636                            {
637                                    time.put(player, t);
638                            } else
639                            {
640                                    time.put(player, 0);
641                                    timeWinner = player.getColor().otherColor();
642                            }
643                    }
644                    assert (time.get(player) >= 0 || time.get(player) == -1);
645                    assert (checkRep());
646            }
647    
648            /**
649             * Subtracts amount from current time of the player (calls setTime, so
650             * updated winner if needed).
651             * 
652             * @param player
653             *            The player whose time to change
654             * @param amount
655             *            The new time
656             * 
657             * @requires amount >= 0, player is one of the player
658             * 
659             * @modifies time_for_players, winner, win_reason, xml
660             * 
661             * @effects sets the decreased time for the player and sets the winner if
662             *          there is one.
663             */
664            public void subtractTimeLeftForPlayer(Player player, int amount)
665            {
666                    assert (amount >= 0);
667                    setTime(player, time.get(player) - amount);
668            }
669    
670            /**
671             * A mapping from a player to the time left for that player
672             * 
673             * @param player
674             *            player to get the time of
675             * 
676             * @requires player not null
677             * 
678             * @returns int time for that player
679             */
680            public int getTimeLeftForPlayer(Player player)
681            {
682                    return time.get(player);
683            }
684    
685            /**
686             * A mapping from colors to players
687             * 
688             * @param color
689             *            the color of the player to get
690             * 
691             * @requires color not null or NONE
692             * 
693             * @returns the player of that color.
694             */
695            public Player getPlayerByColor(Color color)
696            {
697                    assert (color != Color.NONE);
698                    return players.get(color);
699            }
700    
701            /**
702             * Gives an immutable list of the players, with first player first
703             * 
704             * @returns list of players
705             */
706            public List<Player> getPlayers()
707            {
708                    return Arrays.asList(getPlayerByColor(Color.WHITE),
709                                    getPlayerByColor(Color.BLACK));
710            }
711    
712            /**
713             * Gives the current player to move. (or if the game is over, the player
714             * that would move otherwise).
715             * 
716             * @returns player to move
717             */
718            public Player getCurrentPlayer()
719            {
720                    return getPlayerByColor(gameState.getCurrentColor());
721            }
722    }