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 }