001 /**
002 * A GameThread is the main loop that drives a game of Antichess. The user
003 * interacts through this thread via a GraphicUI.
004 */
005
006 package ui;
007
008 import java.io.File;
009 import java.net.URL;
010
011 import javax.swing.ImageIcon;
012 import javax.swing.JOptionPane;
013
014 import core.Color;
015 import core.Game;
016 import core.GameLoadException;
017 import core.HumanPlayer;
018 import core.Move;
019 import core.Piece;
020 import core.Player;
021
022 import rules.King;
023 import ai.AIFactory;
024
025 public class GameThread extends Thread implements StopwatchListener {
026 private Game game;
027 private GraphicUI gui;
028 private TimerBox timerBox;
029 private AIFactory aiFactory;
030
031 boolean newGameRequested = false; // Has the user requested a new game?
032 File loadFile = null; // Null until the user tries to load a game
033 Move suggestedMove = null; // Move input from the user
034 Move nextAiMove = null; // Move input from the AI
035 Runnable aiRunnable; // The Runnable calculating the AI's next move
036 boolean waitingForAi = false;
037 boolean gameOverMessage = false; // Has the game-over prompt been displayed?
038
039 public GameThread(GraphicUI gui) {
040 game = null;
041 this.gui = gui;
042 aiFactory = new AIFactory();
043 }
044
045
046
047 /**
048 * Sets up a default game, and then runs the game loop
049 */
050 public synchronized void run() {
051 // Start a default game: human (white) vs. computer, 5min to a side
052 Player whitePlayer = new HumanPlayer(Color.WHITE);
053 int initialTime = 300000;
054 Player blackPlayer = aiFactory.createPlayer(false, initialTime,
055 initialTime, null);
056 timerBox = new TimerBox(this);
057 game = new Game(whitePlayer, initialTime, blackPlayer, initialTime);
058 setWatches();
059 gui.updateBoard();
060
061 try {
062 while (true) {
063 // The user asked for a new game
064 if (newGameRequested) {
065 startNewGame();
066 }
067
068 // The user tried to load a game
069 if (loadFile != null) {
070 loadGame();
071 loadFile = null;
072 }
073
074 // No game in progress; wait for user input
075 else if (game == null) {
076 this.wait();
077 }
078
079 // The current game is over; wait for user input
080 else if (game.getWinner() != Color.NONE) {
081 if (! gameOverMessage) {
082 timerBox.switchClockTo(Color.NONE);
083 ImageIcon icon;
084
085 String imgDir = SquarePanel.getArtDirectory();
086 Piece k = new King(game.getWinner(), 0);
087 String imgFile = k.toString() +
088 k.getColor().toString() + ".gif";
089 URL url = getClass().getResource("/"+imgDir + "/" +
090 imgFile);
091 if (url != null){ //run from JAR
092 icon = new ImageIcon(url);
093 } else {
094 icon = new ImageIcon(imgDir + File.separator
095 + imgFile);
096 }
097
098 JOptionPane.showMessageDialog(gui, "The " +
099 game.getWinner() + " player won " +
100 game.getWinReason().humanReadableString() +
101 "\n\nPlease start or load a new game.",
102 "Game Over", JOptionPane.PLAIN_MESSAGE, icon);
103 gameOverMessage = true;
104 wait();
105 }
106 }
107
108 // There is a game in progress, and it's the human's turn
109 else if (game.getCurrentPlayer() instanceof HumanPlayer) {
110 // The human has no legal moves and must pass
111 if (game.getGameState().isLegal(new Move())) {
112 gui.updateHistory(new Move(),
113 game.getCurrentPlayer().getColor().equals(
114 Color.WHITE));
115 game.makeMove(new Move());
116 gui.updateBoard();
117
118 }
119 // The human player hasn't entered a move via the GUI
120 else if (suggestedMove == null) {
121 this.wait();
122 }
123 else {
124 nextHumanMove();
125 }
126 }
127
128 // It's the computer's turn
129 // Get the next AI move
130 else if (nextAiMove == null) {
131 //Thread.sleep(200);
132 if (! waitingForAi) {
133 getNextAiMove();
134 }
135 wait();
136 }
137
138 else {
139 makeNextAiMove();
140 }
141
142 // Give GUI commands a chance to be recognized
143 this.wait(1);
144 }
145 }
146 catch (InterruptedException x) {}
147 }
148
149 /**
150 * @return the Game this thread is running
151 */
152 public Game getGame() {
153 return game;
154 }
155
156 /**
157 * This method is called by the GUI when the user requests a new game.
158 */
159 public synchronized void requestNewGame() {
160 newGameRequested = true;
161 this.notifyAll();
162 }
163
164 /**
165 * This method is called by the GUI when the user requests to load a game.
166 */
167 public synchronized void requestLoadGame(File file) {
168 loadFile = file;
169 this.notifyAll();
170 }
171
172 /**
173 * This method is called by the GUI when the user tries to make a move.
174 */
175 public synchronized void suggestMove(Move m) {
176 if (game.getCurrentPlayer() instanceof HumanPlayer) {
177 suggestedMove = m;
178 this.notifyAll();
179 }
180 }
181
182 /**
183 * Begins a new game of Antichess. The user chooses the settings for the
184 * game with a NewGameDialog.
185 */
186 private void startNewGame() {
187 GameSettings g = NewGameDialog.showDialog();
188
189 if (g == null) {
190 newGameRequested = false;
191 return;
192 }
193
194 Player whitePlayer;
195 Player blackPlayer;
196
197 if (g.isWhiteHuman()) {
198 whitePlayer = new HumanPlayer(core.Color.WHITE);
199 }
200 else {
201 whitePlayer = aiFactory.createPlayer(true, g.getWhiteTime(),
202 g.getBlackTime(), g.getPowerupString());
203 }
204
205 if (g.isBlackHuman()) {
206 blackPlayer = new HumanPlayer(core.Color.BLACK);
207 }
208 else {
209 blackPlayer = aiFactory.createPlayer(false, g.getWhiteTime(),
210 g.getBlackTime(), g.getPowerupString());
211 }
212
213 game = new Game(whitePlayer, g.getWhiteTime(),
214 blackPlayer, g.getBlackTime(), g.getPowerupString());
215
216 newGameRequested = false;
217 suggestedMove = null;
218 nextAiMove = null;
219 waitingForAi = false;
220 aiRunnable = null;
221 gameOverMessage = false;
222 gui.updateBoard();
223 gui.clearHistory();
224 setWatches();
225 }
226
227 /**
228 * Loads and starts a game from a savefile. The loaded game will have the
229 * same types of players (human/computer) as the current game.
230 */
231 private void loadGame() {
232 // Create the players
233 Player whitePlayer;
234 Player blackPlayer;
235
236 // No game in progress: Default to human-vs-human game
237 if (game == null) {
238 whitePlayer = new HumanPlayer(Color.WHITE);
239 blackPlayer = new HumanPlayer(Color.BLACK);
240 }
241 // Use player types of current game
242 else {
243 if (game.getPlayerByColor(Color.WHITE) instanceof HumanPlayer) {
244 whitePlayer = new HumanPlayer(Color.WHITE);
245 }
246 else {
247 whitePlayer = aiFactory.createPlayer(true, 0, 0, null);
248 }
249
250 if (game.getPlayerByColor(Color.BLACK) instanceof HumanPlayer) {
251 blackPlayer = new HumanPlayer(Color.BLACK);
252 }
253 else {
254 blackPlayer = aiFactory.createPlayer(false, 0, 0, null);
255 }
256 }
257
258 try {
259 game = Game.loadGame(whitePlayer, blackPlayer, loadFile);
260
261 // Update players
262 whitePlayer.update(game);
263 blackPlayer.update(game);
264
265 loadFile = null;
266 suggestedMove = null;
267 nextAiMove = null;
268 waitingForAi = false;
269 aiRunnable = null;
270 gameOverMessage = false;
271 gui.updateBoard();
272 gui.clearHistory();
273 int moveNum = 1;
274 for (Move m : game.getMoveHistory()) {
275 gui.updateHistory(m, moveNum % 2 != 0);
276 moveNum++;
277 }
278 setWatches();
279 }
280 catch (GameLoadException e) {
281 JOptionPane.showMessageDialog(gui, loadFile.getName() +
282 " could not be loaded.\n" + e.getMessage(), "Error",
283 JOptionPane.ERROR_MESSAGE);
284 e.printStackTrace();
285 }
286 }
287
288 /**
289 * Gets the AI player's next move, then updates the GUI and timers
290 * accordingly.
291 */
292 private synchronized void getNextAiMove() {
293 waitingForAi = true;
294
295 // find CPU player's next move
296 final Player cpu = game.getCurrentPlayer();
297
298 final Move lastMove;
299 if (game.getMoveHistory().size() == 0) {
300 lastMove = new Move();
301 }
302 else {
303 lastMove= game.getMoveHistory().get(game.getMoveHistory().size()
304 - 1);
305 }
306 final long thistime = game.getTimeLeftForPlayer(cpu);
307 //XXX UGLY!
308 final long othertime = game.getTimeLeftForPlayer(
309 game.getPlayerByColor(
310 game.getGameState().getCurrentColor().otherColor()));
311
312 aiRunnable = new Runnable() {
313 public synchronized void run() {
314 Move move = cpu.askForMove(lastMove, thistime, othertime);
315 //
316 if (aiRunnable == this) {
317 nextAiMove = move;
318 }
319 this.notifyAll();
320 }
321 };
322 Thread t = new Thread(aiRunnable);
323 t.start();
324 }
325
326 private void makeNextAiMove() {
327 if (!game.getGameState().isLegal(nextAiMove)){
328 throw new RuntimeException("AI returned invalid move");
329 }
330 gui.updateHistory(nextAiMove,
331 game.getCurrentPlayer().getColor() == Color.WHITE);
332 if (game.getWinner() != Color.NONE) {
333 return;
334 }
335 game.makeMove(nextAiMove);
336 gui.updateBoard();
337 timerBox.switchClockTo(game.getCurrentPlayer().getColor());
338 nextAiMove = null;
339 waitingForAi = false;
340 }
341
342 /**
343 * Uses the input from the GUI to make a move for a human player.
344 */
345 private synchronized void nextHumanMove() {
346 // The player tried to make an illegal move
347 if (suggestedMove == null) {
348 return;
349 }
350
351 if (!game.getGameState().isLegal(suggestedMove)){
352 throw new RuntimeException("Human returned invalid move");
353 }
354 gui.updateHistory(suggestedMove,
355 game.getCurrentPlayer().getColor() == Color.WHITE);
356 game.makeMove(suggestedMove);
357 gui.updateBoard();
358 timerBox.switchClockTo(game.getCurrentPlayer().getColor());
359
360 suggestedMove = null;
361 }
362
363 /**
364 * Triggered by Stopwatch events. Keeps the GameState and GUI updated as far
365 * as time left in the game.
366 */
367 public synchronized void timeChanged(long millis, Stopwatch s) {
368 //System.out.println("Time changed");
369 if (s == timerBox.getWhiteWatch()) {
370 game.setTime(game.getPlayerByColor(Color.WHITE), (int) millis);
371 gui.updateTime(millis, Color.WHITE);
372 }
373 else if (s == timerBox.getBlackWatch()) {
374 game.setTime(game.getPlayerByColor(Color.BLACK), (int) millis);
375 gui.updateTime(millis, Color.BLACK);
376 }
377 this.notifyAll();
378 }
379
380 /**
381 * Sets up the TimerBox for a new or newly loaded game.
382 */
383 private void setWatches() {
384 timerBox.setWatches(game.getTimeLeftForPlayer(
385 game.getPlayerByColor(Color.WHITE)),
386 game.getTimeLeftForPlayer(
387 game.getPlayerByColor(Color.BLACK)));
388
389 timerBox.switchClockTo(game.getCurrentPlayer().getColor());
390 }
391
392 }