-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathServerGame.java
More file actions
396 lines (345 loc) · 12.8 KB
/
ServerGame.java
File metadata and controls
396 lines (345 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import java.util.Random;
class ServerGame {
GameState state = GameState.INITIALIZING;
char[] board = new char[9];
int turn = 0;
ServerClient playerX = null;
ServerClient playerO = null;
ServerClient lastWinner = null;
int streak = 0;
Random random = new Random();
Thread waitThread = null;
/**
* Sets the random number generator seed to the current time; see {@link Random#setSeed(long)} and
* {@link System#currentTimeMillis()}.
* <p>
* Begins waiting for players; see {@link #waitForPlayers()}.
*/
ServerGame() {
random.setSeed(System.currentTimeMillis());
waitForPlayers();
}
/**
* Starts a new thread that looks for players, and starts the game once it finds some; see {@link #findPlayers()}
* and {@link #startGame()}.
*/
void waitForPlayers() {
if (state == GameState.WAITING_FOR_PLAYERS) {
return;
}
playerX = null;
playerO = null;
state = GameState.WAITING_FOR_PLAYERS;
System.out.print("Waiting for players...\n");
waitThread = new Thread(() -> {
while (Server.serverSocket != null) {
if (findPlayers()) {
startGame();
break;
}
Thread.yield();
}
});
waitThread.start();
}
/**
* Grabs the first two clients from the {@link Server#clients} array (queue). The two clients will only be used
* if/once they are fully connected.
* <p>
* If there is only one client, sends the "waiting for another player" message to that client ('w'); this message
* is only sent to the client once.
* <p>
* Uses {@link Random#nextBoolean()} to determine which player will be X.
*
* @return Boolean indicating whether two players were found.
*/
boolean findPlayers() {
int clientCount = Server.clients.size();
ServerClient player1 = clientCount >= 1 ? Server.clients.get(0) : null;
if (player1 == null || player1.state != ClientState.CONNECTED) {
return false;
}
ServerClient player2 = clientCount >= 2 ? Server.clients.get(1) : null;
if (playerX != player1 && playerO != player1) {
if (random.nextBoolean()) {
playerX = player1;
} else {
playerO = player1;
}
if (player2 == null || player2.state != ClientState.CONNECTED) {
player1.sendMessage(new byte[]{'w'});
return false;
}
}
if (player2 == null || player2.state != ClientState.CONNECTED) {
return false;
}
if (playerX == null) {
playerX = player2;
} else {
playerO = player2;
}
return true;
}
/**
* @return The number of clients currently playing the game.
*/
int getPlayerCount() {
int players = 2;
if (playerX == null) {
players--;
}
if (playerO == null) {
players--;
}
return players;
}
/**
* Sends the position in the queue to every fully connected client, skipping (from the beginning of the array)
* the number of clients defined by {@code skipClients}.
*
* @param skipClients Number of clients (from the beginning of the array) to skip sending the message to.
*/
void sendQueueUpdates(int skipClients) {
int players = getPlayerCount();
for (int index = Server.clients.size() - 1; index >= skipClients; index--) {
ServerClient client = Server.clients.get(index);
if (client.state == ClientState.CONNECTED) {
client.sendMessage(new byte[]{'Q', (byte) (index - players)});
}
}
}
/**
* Populates the passed {@code byteArray} array with the board character bytes from {@link #board}.
*
* @param byteArray Byte array to populate.
* @param offset Integer to add to the byte array indices.
*/
void populateBoardBytes(byte[] byteArray, int offset) {
for (int square = 0; square < 9; square++) {
byteArray[offset + square] = (byte) board[square];
}
}
/**
* @return The current player whose turn it is based on {@link #turn}.
*/
ServerClient getTurnPlayer() {
return turn % 2 == 0 ? playerX : playerO;
}
/**
* Starts the game.
* <p>
* Sends queue updates to all clients except for the current players; see {@link #sendQueueUpdates(int)}.
* <p>
* Sends "game starting", board state and "indicate who plays next" messages to the current players.
*/
void startGame() {
sendQueueUpdates(2);
for (int square = 0; square < 9; square++) {
board[square] = ' ';
}
turn = 0;
state = GameState.PLAYING;
System.out.printf("Game started with %s and %s!\n", playerX, playerO);
ServerClient turnPlayer = getTurnPlayer();
ServerClient otherPlayer = turnPlayer == playerX ? playerO : playerX;
byte[] turnPlayerBytes = new byte[11];
turnPlayerBytes[0] = 'x';
populateBoardBytes(turnPlayerBytes, 1);
turnPlayerBytes[10] = 1;
turnPlayer.sendMessage(turnPlayerBytes);
byte[] otherPlayerBytes = new byte[11];
otherPlayerBytes[0] = 'o';
populateBoardBytes(otherPlayerBytes, 1);
otherPlayerBytes[10] = 0;
otherPlayer.sendMessage(otherPlayerBytes);
}
/**
* Checks the rows, columns and diagonals of the passed {@code square} for a win for the passed {@code role}.
*
* @param role The role to check for the win.
* @param square The square to check the rows, columns and diagonals of.
* @return Boolean indicating whether the passed {@code role} won.
*/
boolean checkWin(char role, int square) {
// check row
int rowStart = (square / 3) * 3;
if (board[rowStart] == role && board[rowStart + 1] == role && board[rowStart + 2] == role) {
return true;
}
// check column
int columnStart = square % 3;
if (board[columnStart] == role && board[columnStart + 3] == role && board[columnStart + 6] == role) {
return true;
}
if (square % 2 == 1) { // no need to check diagonals
return false;
}
// check main diagonal
if (square == 0 || square == 4 || square == 8) {
if (board[0] == role && board[4] == role && board[8] == role) {
return true;
}
}
// check anti diagonal
if (square == 2 || square == 4 || square == 6) {
if (board[2] == role && board[4] == role && board[6] == role) {
return true;
}
}
return false;
}
/**
* Checks if all board squares are filled, indicating a tie.
*
* @return Boolean indicating whether the board is completely full, indicating a tie.
*/
boolean checkTie() {
int filled = 0;
for (int square = 0; square < 9; square++) {
if (board[square] != ' ') {
filled++;
}
}
return filled == 9;
}
/**
* Attempts to play a turn.
* <p>
* If the move is invalid, sends the "incorrect move" message to the client.
* <p>
* Otherwise, makes the move and checks for a win and a tie; see {@link #checkWin(char, int)} and
* {@link #checkTie()}.
* <p>
* If the player won, calls {@link #endGame(ServerClient)}.
* <p>
* Otherwise, sends board state and "indicate who plays next" messages to the current players.
*
* @param player The client claiming to be a player and attempting to play a turn.
* @param square The square the player wants to play their turn on.
*/
void playTurn(ServerClient player, int square) {
if (state != GameState.PLAYING) {
return;
}
ServerClient turnPlayer = getTurnPlayer();
if (turnPlayer == null || turnPlayer.state != ClientState.CONNECTED || turnPlayer != player) {
return;
}
ServerClient otherPlayer = turnPlayer == playerX ? playerO : playerX;
if (otherPlayer == null || otherPlayer.state != ClientState.CONNECTED) {
return;
}
if (board[square] != ' ') {
turnPlayer.sendMessage(new byte[]{'I'});
return;
}
char role = turnPlayer == playerX ? 'X' : 'O';
board[square] = role;
turn++;
System.out.printf("%s played square %d.\n", player, square + 1);
boolean win = checkWin(role, square);
if (win || checkTie()) {
byte[] boardBytes = new byte[9];
populateBoardBytes(boardBytes, 0);
turnPlayer.sendMessage(boardBytes);
otherPlayer.sendMessage(boardBytes);
endGame(win ? turnPlayer : null);
return;
}
byte[] turnPlayerBytes = new byte[10];
populateBoardBytes(turnPlayerBytes, 0);
turnPlayerBytes[9] = 0;
turnPlayer.sendMessage(turnPlayerBytes);
byte[] otherPlayerBytes = new byte[10];
populateBoardBytes(otherPlayerBytes, 0);
otherPlayerBytes[9] = 1;
otherPlayer.sendMessage(otherPlayerBytes);
}
/**
* Ends the game.
* <p>
* If a {@code winner} was passed, increments their win streak, and sends them and the loser the win and loss
* messages. The loser will be sent to the end of the {@link Server#clients} array (queue), and the server will
* then wait for the passed {@code winner} to respond whether they want to play again.
* <p>
* If no {@code winner} was passed ({@code null}), sends the tie messages to both current players, moves them
* both to the end of the {@link Server#clients} array (queue) and begins waiting for players again; see
* {@link #waitForPlayers()}.
*
* @param winner The player who won, or {@code null} if it was a tie.
*/
void endGame(ServerClient winner) {
if (state != GameState.PLAYING) {
return;
}
if (winner != lastWinner) {
streak = 0;
}
lastWinner = winner;
if (winner != null && winner.state == ClientState.CONNECTED) {
System.out.printf("Game over, %s wins!\n", winner);
if (++streak > 1) {
System.out.printf("%s has won %d games in a row!\n", winner, streak);
}
if (winner == playerX) {
if (Server.clients.remove(playerO)) {
Server.clients.add(playerO);
}
} else {
if (Server.clients.remove(playerX)) {
Server.clients.add(playerX);
}
}
state = GameState.WAITING_ON_WINNER;
System.out.printf("Waiting on %s to respond...\n", winner);
winner.sendMessage(new byte[]{'W', (byte) streak});
ServerClient loser = winner == playerX ? playerO : playerX;
if (loser != null && loser.state == ClientState.CONNECTED) {
loser.sendMessage(new byte[]{'L'});
}
} else {
System.out.print("Game over, it's a tie!\n");
if (playerX != null && playerX.state == ClientState.CONNECTED) {
if (Server.clients.remove(playerX)) {
Server.clients.add(playerX);
}
playerX.sendMessage(new byte[]{'T'});
}
if (playerO != null && playerO.state == ClientState.CONNECTED) {
if (Server.clients.remove(playerO)) {
Server.clients.add(playerO);
}
playerO.sendMessage(new byte[]{'T'});
}
waitForPlayers();
}
}
/**
* Restarts the game.
* <p>
* If the winner ({@code player}) chooses not to play again, disconnects them from the server.
* <p>
* Begins waiting for players again; see {@link #waitForPlayers()}.
*
* @param player The client claiming to be the winner and attempting to restart the game.
* @param winnerPlaysAgain Whether the winner ({@code player}) wants to play again.
*/
void restartGame(ServerClient player, boolean winnerPlaysAgain) {
if (state != GameState.WAITING_ON_WINNER) {
return;
}
if (player != lastWinner) {
return;
}
if (!winnerPlaysAgain) {
if (player.state == ClientState.CONNECTED) {
System.out.printf("%s will not play again.\n", player);
player.disconnect();
}
} else {
System.out.printf("%s will play again!\n", player);
}
waitForPlayers();
}
}