-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.cpp
More file actions
520 lines (444 loc) · 14.8 KB
/
main.cpp
File metadata and controls
520 lines (444 loc) · 14.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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
//
// Main source file for Jamie's Scrabble solution
//
#include <fstream>
#include <string>
#include <iostream>
#include <iomanip>
#include <cmath>
#include "Board.h"
#include "Bag.h"
#include "Dictionary.h"
#include "ConsolePrinter.h"
#include "Player.h"
#include "ConfigFile.h"
#include "Exceptions.h"
#include "rang.h"
#include "CPU.h"
#include <typeinfo>
// Get a friendly string to print to the user in response to a move exception
std::string getFriendlyError(MoveException const & exception)
{
std::string exMessage(exception.what());
// first half of invalid word exception message
std::string invalidWordExPrefix("INVALIDWORD:");
if(exMessage == "EMPTY")
{
return "Move would not manipulate any tiles!";
}
else if(exMessage == "MALFORMED")
{
return "Format error in command arguments!";
}
else if(exMessage == "UNKNOWN")
{
return "Unknown command word";
}
else if(exMessage == "WRONGTILES")
{
return "You don't have all the tiles you'd need for this move!";
}
else if(exMessage == "OUTOFBOUNDS")
{
return "Would go out of bounds!";
}
else if(exMessage == "OCCUPIED")
{
return "Coordinates of first tile are occupied!";
}
else if(exMessage == "NOSTART")
{
return "This is the first move, you have to use the start square!";
}
else if(exMessage == "NOWORDS")
{
return "Would not create a sequence of at least 2 letters!";
}
else if(exMessage == "NONEIGHBOR")
{
return "No tile is adjacent to a previously placed tile!";
}
else if(exMessage.substr(0, invalidWordExPrefix.size()) == invalidWordExPrefix)
{
// get invalid word out of exception message
std::string invalidWord = exMessage.substr(invalidWordExPrefix.size(), std::string::npos);
return "Would form the letter sequence \'" + invalidWord + "\', which is not a word!";
}
else
{
return "Unknown MoveException: " + exMessage;
}
}
// Perform the final subtraction, whereby all players lose score equivalent to the
// values of the tiles they hold. If emptyHandPlayerIndex is not -1, this denotes the player
// who ran out of tiles, and they will receive the total of all deducted points.
unsigned int doFinalSubtraction(std::vector<Player *> & players, ssize_t emptyHandPlayerIndex)
{
unsigned int totalScore = 0;
for(std::vector<Player *>::iterator playersIter = players.begin(); playersIter != players.end(); ++playersIter)
{
unsigned int remainingPoints = (*playersIter)->remainingPoints();
totalScore += remainingPoints;
(*playersIter)->subtractPoints(remainingPoints);
}
if(emptyHandPlayerIndex != -1)
{
players[emptyHandPlayerIndex]->addPoints(totalScore);
}
return totalScore;
}
#define MOVE_PROMPT_COLOR rang::fgB::blue << rang::style::bold
#define PLAYER_NAME_COLOR rang::fgB::red << rang::style::bold
#define TILE_NAME_COLOR rang::fg::green << rang::style::bold
#define SCORE_COLOR rang::fgB::magenta << rang::style::bold
int main(int argc, char** argv)
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <configuration file>" << std::endl;
return 3;
}
// data for running game
// NOTE: must be pointers so that they can be initialized inside the try-catch block.
ConfigFile * configFile;
Board * board;
Bag * bag;
Dictionary * dictionary;
std::vector<Player *> players;
// attempt to read files and initialize game.
// -------------------------------------------------------------------------------
// NOTE: it's not required to be careful about missing files like this,
// but I like being paranoid.
try
{
configFile = new ConfigFile(argv[1]);
bag = new Bag(configFile->tilesetFile, configFile->randomSeed);
dictionary = new Dictionary(configFile->dictionaryFile);
board = new Board(configFile->boardFile, configFile->initFile);
}
catch(FileException & fileException)
{
if(fileException.what() == std::string("MISSINGCONFIG"))
{
std::cerr << "Error: config file " << argv[1] << " was not found!" << std::endl;
return 3;
}
else if(fileException.what() == std::string("BAG"))
{
std::cerr << "Error: tileset file " << configFile->tilesetFile << " could not be read!" << std::endl;
return 4;
}
else if(fileException.what() == std::string("BOARD"))
{
std::cerr << "Error: board file " << configFile->boardFile << " could not be read!" << std::endl;
return 5;
}
else if(fileException.what() == std::string("BOARDCONTENT"))
{
std::cerr << "Error: board file " << configFile->boardFile << " contained invalid content!" << std::endl;
return 5;
}
else if(fileException.what() == std::string("DICTIONARY"))
{
std::cerr << "Error: dictionary file " << configFile->dictionaryFile << " could not be read!" << std::endl;
return 6;
}
else if(fileException.what() == std::string("INIT"))
{
std::cerr << "Error: board init file " << configFile->initFile << " contained invalid content!" << std::endl;
return 5;
}
else
{
std::cerr << "Unexpected FileException: " << fileException.what() << std::endl;
return 7;
}
}
// question user about players to add
// -------------------------------------------------------------------------------
size_t numPlayers;
std::cout << "Please enter number of players: ";
std::cin >> numPlayers;
// this check is not strictly required but it is in the spec...
if(numPlayers < 1 || numPlayers > 8)
{
std::cerr << "Invalid number of players, must be between 1 and 8." << std::endl;
return 9;
}
std::cout << numPlayers << " players confirmed." << std::endl;
// read in player names
// -------------------------------------------------------------------------------
// clear remaining data on cin from the previous prompt
std::cin.ignore();
for(size_t playerNum = 1; playerNum <= numPlayers; ++playerNum)
{
std::cout << "Please enter name for player " << playerNum << ": ";
std::string playerName;
std::getline(std::cin, playerName);
std::string playerNameL = playerName;
std::transform(playerNameL.begin(), playerNameL.end(), playerNameL.begin(), ::tolower);
if (playerNameL == "cpul") {
std::cout << "CPUL Player " << playerNum << " has been added." << std::endl;
players.push_back(new CPUL(configFile->handSize));
} else if (playerNameL == "cpus") {
std::cout << "CPUS Player " << playerNum << " has been added." << std::endl;
players.push_back(new CPUS(configFile->handSize));
} else {
std::cout << "Player " << playerNum << ", named \"" << playerName << "\", has been added." << std::endl;
// create player with a full initial hand
players.push_back(new Player(playerName, configFile->handSize));
}
players[playerNum - 1]->addTiles(bag->drawTiles(configFile->handSize));
}
// run game loop
// -------------------------------------------------------------------------------
bool gameWon = false;
ssize_t emptyHandPlayerIndex;
size_t sequentialPasses = 0;
while(!gameWon)
{
for(size_t playerNum = 0; (!gameWon && playerNum < numPlayers); ++playerNum)
{
ConsolePrinter::printBoard(*board);
ConsolePrinter::printHand(*players[playerNum]);
Move * playerMove = nullptr;
bool correctMove = false;
while(!correctMove)
{
//check if it's CPU L's turn
if (dynamic_cast<CPUL*>(players[playerNum])) {
std::cout << "CPUL players turn" << std::endl;
players[playerNum]->performCPUMove(*board, *bag, *dictionary);
break;
}
//check if it's CPU S' turn
if (dynamic_cast<CPUS*>(players[playerNum])) {
std::cout << "CPUS players turn" << std::endl;
players[playerNum]->performCPUMove(*board, *bag, *dictionary);
break;
}
std::cout << std::endl;
std::cout << MOVE_PROMPT_COLOR << "Your move, " << PLAYER_NAME_COLOR << players[playerNum]->getName() << MOVE_PROMPT_COLOR << ": " << rang::style::reset;
std::string moveString;
std::getline(std::cin, moveString);
try
{
// first construct the move, which could throw an exception
playerMove = Move::parseMove(moveString, *players[playerNum]);
// now execute it, which could also throw exceptions
playerMove->execute(*board, *bag, *dictionary);
if(playerMove->isPass())
{
++sequentialPasses;
}
else
{
sequentialPasses = 0;
}
correctMove = true;
}
catch(MoveException & exception)
{
// print error message and reprompt the player
std::cout << "Error in move: " << getFriendlyError(exception) << std::endl;
}
}
// draw more tiles from the bag to bring the player up to a full hand
std::vector<Tile *> newTiles = bag->drawTiles(players[playerNum]->getMaxTiles() - players[playerNum]->getNumTiles());
players[playerNum]->addTiles(newTiles);
// tell the user about what new tiles they got
if(!newTiles.empty())
{
std::cout << "Picked up new tiles:" << TILE_NAME_COLOR;
for(std::vector<Tile *>::iterator newTilesIter = newTiles.begin(); newTilesIter != newTiles.end(); ++newTilesIter)
{
std::cout << ' ' << static_cast<char>(std::toupper((*newTilesIter)->getLetter()));
}
std::cout << rang::style::reset << std::endl;
}
std::cout << "Your current score: " << SCORE_COLOR << players[playerNum]->getPoints() << rang::style::reset << std::endl;
// wait for player confirmation
std::cout << std::endl << "Press [enter] to continue.";
std::cin.ignore();
// if all players pass for one whole loop, the game is over
if(sequentialPasses >= players.size())
{
gameWon = true;
// no one gets the bonus
emptyHandPlayerIndex = -1;
}
// if this player has run out of tiles, and no more could be drawn from the bag,
// the game is also over and they get a bonus
if(players[playerNum]->getNumTiles() == 0)
{
gameWon = true;
// this player gets the bonus
emptyHandPlayerIndex = playerNum;
}
delete playerMove;
}
}
// total up scores
// -------------------------------------------------------------------------------
doFinalSubtraction(players, emptyHandPlayerIndex);
// put all players into a multimap by score
std::multimap<unsigned int, Player *> playerScores;
for(std::vector<Player *>::iterator playersIter = players.begin(); playersIter != players.end(); ++playersIter)
{
playerScores.insert(std::make_pair((*playersIter)->getPoints(), *playersIter));
// ^ dis is weird
}
// get high score by looking at last (=largest) key in multimap
unsigned int highScore = (--playerScores.end())->first;
// print all players with the high score
std::pair<std::multimap<unsigned int, Player *>::iterator, std::multimap<unsigned int, Player *>::iterator> winningPlayersRange = playerScores.equal_range(highScore);
size_t numWinners = static_cast<size_t>(std::distance(winningPlayersRange.first, winningPlayersRange.second));
if(numWinners == 1)
{
std::cout << "Winner:";
}
else
{
std::cout << "Winners:";
}
for(; winningPlayersRange.first != winningPlayersRange.second; ++winningPlayersRange.first)
{
std::cout << ' ' << PLAYER_NAME_COLOR << winningPlayersRange.first->second->getName();
}
std::cout << rang::style::reset << std::endl;
// now print score table
std::cout << "Scores: " << std::endl;
std::cout << "---------------------------------" << std::endl;
// justify all integers printed to have the same amount of character as the high score, left-padding with spaces
std::cout << std::setw(static_cast<uint32_t>(floor(log10(highScore) + 1)));
for(std::multimap<unsigned int, Player *>::const_reverse_iterator playerScoresIter = playerScores.rbegin(); playerScoresIter != playerScores.rend(); ++playerScoresIter)
{
// note: we print the scores first since the names could be any width but the scores will be all the same width
std::cout << SCORE_COLOR << playerScoresIter->first << rang::style::reset << " | " << PLAYER_NAME_COLOR << playerScoresIter->second->getName() << rang::style::reset << std::endl;
}
// cleanup
// -------------------------------------------------------------------------------
for(size_t playerIndex = 0; playerIndex < players.size(); ++playerIndex)
{
delete players[playerIndex];
}
delete configFile;
delete board;
delete bag;
delete dictionary;
}
ConfigFile::ConfigFile(std::string const &configPath)
{
// This parser uses a state machine to process the file. It's a bit different from
// the more common line-based parsers, but a lot more robust.
enum class ParserState
{
LOOKING_FOR_KEY, // not inside any structures at the moment, waiting for a key or a comment character
IN_KEY, // inside key, waiting for equals sign or whitespace
LOOKING_FOR_VALUE, // just saw colon, now looking for start of value
IN_VALUE // inside value, waiting for newline to end it.
};
std::ifstream configFileStream(configPath);
if(!configFileStream)
{
throw FileException("MISSINGCONFIG");
}
std::string keyBuffer, valueBuffer;
char currChar;
ParserState state = ParserState::LOOKING_FOR_KEY;
ParserState nextState;
while(!configFileStream.eof() && configFileStream)
{
// read a single character
configFileStream.get(currChar);
if(configFileStream.eof())
{
break;
}
switch(state)
{
case ParserState::LOOKING_FOR_KEY:
if(currChar == ' ' || currChar == '\n')
{
// keep looking
nextState = ParserState::LOOKING_FOR_KEY;
}
else
{
// found something!
nextState = ParserState::IN_KEY;
keyBuffer = currChar;
}
break;
case ParserState::IN_KEY:
if(currChar == ':')
{
// last char of key
nextState = ParserState::LOOKING_FOR_VALUE;
}
else
{
// keep scanning key
nextState = ParserState::IN_KEY;
keyBuffer += currChar;
}
break;
case ParserState::LOOKING_FOR_VALUE:
if(currChar == ' ')
{
// keep looking
nextState = ParserState::LOOKING_FOR_VALUE;
}
else
{
//found start of value
valueBuffer = currChar;
nextState = ParserState::IN_VALUE;
}
break;
case ParserState::IN_VALUE:
if(currChar == '\n' || currChar == ' ')
{
//std::cout << "Parsed config value, key: " << keyBuffer << ", value: " << valueBuffer << std::endl;
// done with value, now set things appropriately
if(keyBuffer == "HANDSIZE")
{
handSize = static_cast<size_t>(std::stoi(valueBuffer));
}
else if(keyBuffer == "TILES")
{
tilesetFile = valueBuffer;
}
else if(keyBuffer == "DICTIONARY")
{
dictionaryFile = valueBuffer;
}
else if(keyBuffer == "SEED")
{
randomSeed = static_cast<uint32_t>(std::stoi(valueBuffer));
}
else if(keyBuffer == "BOARD")
{
boardFile = valueBuffer;
}
else if(keyBuffer == "INIT")
{
initFile = valueBuffer;
}
else
{
// unrecognized key, ignore
}
nextState = ParserState::LOOKING_FOR_KEY;
}
else
{
// keep scanning value
valueBuffer += currChar;
nextState = ParserState::IN_VALUE;
}
break;
}
state = nextState;
}
}