diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8b5e2..37c8a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,24 @@ * Player scores are calculated * Tile placement validation * Logging of games + +## Version 1.1 + +#### Breaking Changes +* The build system has been changed to Cmake + +#### Bug fixes +* Premium squares are applied only if tiles from the current play are placed on them +* Players prompted only if their racks are not empty, avoiding unplayable game state +* Infinite game loop fixed + +#### Feature Updates +* Perform input validation +* Show details of currently made play +* Play confirmation +* Added option to quit the game + +#### Minor changes +* Remove lingering debug messages +* Correct board layout + diff --git a/CMakeLists.txt b/CMakeLists.txt index 9152c1c..3598dfe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) if(CMAKE_BUILD_TYPE MATCHES Debug) add_definitions(-DLOG_PATH=\"$(HOME)/RPDATA/Repos/scrabble/logs/\") - add_definitions(-DDBG) + add_definitions(-DDEBUG) else() add_definitions(-DLOG_PATH=\"$(HOME)/.local/share/rp-scrabble/logs/\") endif() diff --git a/README.md b/README.md index 46de87f..4022810 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ This is a command-line Scrabble game written in C++ to explore object oriented programming. +# NOTICE for users of `v1.0` +* Users of `v1.0` **MUST** re-install the application as the update to `v1.1` contains breaking changes +* All future updates will conform to these changes + ## Build requirements +* `cmake` * `git` * `make` * `g++` @@ -19,8 +24,12 @@ programming. ``` * Build + + Run the install script with the appropriate argument- ```sh - $ make && make install + $ ./install.sh arch # for Arch based distributions (Manjaro, Void, etc) + $ ./install.sh debian # for Debian based distributions (Ubuntu, Pop_OS, etc) + $ ./install.sh custom # for other distributions ``` * Now, simply running `rp-scrabble` should launch the game. * If it doesn't launch, check the output of `$ echo $PATH`. If it does not contain `your-home-dir/.local/bin`, add it to your `PATH` like this- @@ -30,14 +39,14 @@ programming. ``` ## Other notes -* Keep the repo up to date by running `git pull` every once in a while in the project directory. Remember to rebuild again after pulling to bring any changes into effect. +* Keep the repo up to date by running `git pull` every once in a while in the project directory. Remember to rerun the install script after pulling to bring any changes into effect. * Log files are stored in ```sh $HOME/.local/share/rp-scrabble/logs ``` -* To uninstall, run this command in the project directory +* To uninstall, run the install script with the `uninstall` argument in the project directory ```sh - $ make uninstall + $ ./install.sh uninstall ``` **NOTE** This will also remove any log files created diff --git a/TODO.md b/TODO.md index a4b0894..a35acaf 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,9 @@ # TODO ## Bugs -* Premium squares to be applied only if tiles from the current play are placed on them ## Features -* Perform input validation -* Add option to quit gracefully -* Show details of currently made play * Add undo, fetch, skip functionality * Add dictionary for checking words ## Other -* Remove lingering debug messages diff --git a/assets/scrabble.png b/assets/scrabble.png deleted file mode 100644 index 6b79dbb..0000000 Binary files a/assets/scrabble.png and /dev/null differ diff --git a/include/play.h b/include/play.h index 8825150..c24954a 100644 --- a/include/play.h +++ b/include/play.h @@ -22,6 +22,7 @@ class Play { int pointsMade; Player* playMaker; std::string playStr; + std::vector> wordsInPlay; public: Play(Player* p); @@ -31,8 +32,11 @@ class Play { bool validate(std::string tileStr, Board* b, int r, int c, char dir); std::vector> getWords(std::vector tilesInStr, Board* b, int r, int c, char dir); std::vector getConnectedWord(Tile* t, char dir); - void calculatePoints(std::vector> words); + void calculatePoints(std::vector> words, std::vector tileStrVec); int getPointsMade(); + bool confirmPlay(); + void reset(); + void show(); }; #endif diff --git a/include/player.h b/include/player.h index dd7216c..658cb4d 100644 --- a/include/player.h +++ b/include/player.h @@ -23,12 +23,14 @@ class Player { void setName(std::string); void toggleTurn(); void updateScore(int points); + int getScore(); Tile* tileFromRack(int index); bool placeTile(Tile* t, Board* b, int r, int c); std::vector placeTileStr(std::string str, Board* b, int r, int c, char dir); void draw(int count, Bag* b); bool rackIsEmpty(); + void returnToRack(Tile* t, Board* b); void show(); }; diff --git a/include/utils.h b/include/utils.h index 48d1c37..0697e0a 100644 --- a/include/utils.h +++ b/include/utils.h @@ -8,10 +8,12 @@ class Tile; class Square; class Play; -#define DEBUG(x, y) std::cout << x << ":" << y << std::endl +#define DEBUG_PRINT(x, y) std::cout << x << ":" << y << std::endl #define NUM_ROWS 15 #define NUM_COLS 15 +const std::string alphabets = "abcdefghijklmnopqrstuvwxyz"; + /* * ANSI escape codes for colour * @@ -94,7 +96,7 @@ inline void RED_BG(std::string x) inline void PINK_BG(std::string x) { - std::cout << "\033[48;2;255;77;109m" + x + "\033[0m"; + std::cout << "\033[48;2;225;0;109m" + x + "\033[0m"; } inline void DARK_BLUE_BG(std::string x) @@ -117,12 +119,12 @@ inline void OFF_WHITE_BG(std::string x) */ inline void TILE_COLOURS(std::string x) { - std::cout << "\033[1;38;2;0;0;0;48;2;255;236;230m" + x + "\033[0m"; + std::cout << "\033[1;38;2;0;0;0;48;2;255;255;255m" + x + "\033[0m"; } inline void BOARD_COLOURS(std::string x) { - std::cout << "\033[1;38;2;232;232;232;48;2;104;0;13m" + x + "\033[0m"; + std::cout << "\033[1;38;2;232;232;232;48;2;77;0;9m" + x + "\033[0m"; } /* @@ -130,6 +132,7 @@ inline void BOARD_COLOURS(std::string x) */ bool squarePresent(std::vector s, Square* target); bool tilePresent(std::vector t, Tile* target); +bool charPresent(std::string str, char target); std::vector parsePlay(std::string in); void log(std::string logFilePath, std::string str); std::string RawTimeToString(const time_t& t); diff --git a/src/bag.cc b/src/bag.cc index 396a4c1..9f15dee 100644 --- a/src/bag.cc +++ b/src/bag.cc @@ -184,9 +184,7 @@ void Bag::show() for(Tile* i : bag) { cout << i->getLetter(); } - cout << "\n"; - BOLD(" " + to_string(bag.size())); - cout << " tiles remaining\n"; + BOLD(" (" + to_string(bag.size()) + " tiles remaining)\n"); } void Bag::shuffle() diff --git a/src/board.cc b/src/board.cc index 6314ea0..30b18d6 100644 --- a/src/board.cc +++ b/src/board.cc @@ -9,7 +9,7 @@ using namespace std; Board::Board() { enum_sqType quarterBoard[7][7] = { - { TWS, N , N , DWS, N , N , N }, + { TWS, N , N , DLS, N , N , N }, { N , DWS, N , N , N , TLS, N }, { N , N , DWS, N , N , N , DLS }, { DLS, N , N , DWS, N , N , N }, diff --git a/src/game.cc b/src/game.cc index 3a3e1e1..b38c11b 100644 --- a/src/game.cc +++ b/src/game.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include "game.h" #include "bag.h" #include "board.h" @@ -21,7 +22,7 @@ Game::Game() gameID = RawTimeToString(rawTime); logFilePath = LOG_PATH + gameID + string(filenameBuffer); - DEBUG(" logFilePath", logFilePath); + DEBUG_PRINT(" logFilePath", logFilePath); cout << endl; gameBoard = new Board; @@ -73,7 +74,7 @@ void Game::init() } catch(string err) { BOLD_RED_FG(" Unable to open log file\n"); - BOLD_RED_FG(" You can set the path in the Makefile\n"); + BOLD_RED_FG(" You can set the path in the CMakeLists.txt file\n"); BOLD_RED_FG(" Aborting\n"); exit(1); } @@ -88,7 +89,7 @@ void Game::init() cin >> i; if(i > 0 && i < 4) { for(j = 0; j < i; j++) { - cout << " Name of Player " + to_string(j + i + 1) + ": "; + cout << " Name of Player " + to_string(j + 2) + ": "; cin >> tempName; addPlayer(new Player(tempName)); try { @@ -113,6 +114,13 @@ void Game::init() } } } + else if(response == 'n') { + // do nothing, carry on + } + else { + BOLD_RED_FG(" Error: Invalid input\n"); + init(); + } cout << "\n"; } @@ -142,7 +150,7 @@ string Game::getInput() string input = ""; BOLD(" Enter the tiles you want to place "); - cout << "(? for help, . to show board) "; + cout << "(? for help, . to show board, - to quit) "; cin >> tempIn; if(tempIn == "?") { return "?"; @@ -150,12 +158,23 @@ string Game::getInput() else if(tempIn == ".") { return "."; } + else if(tempIn == "-") { + return "-"; + } + else if(tempIn == "!") { + return "!"; + } else { + for(char ch : tempIn) { + if(!charPresent(alphabets, ch)) { + throw(string("Invalid character input\n")); + } + } input.append(tempIn + "-"); } BOLD(" Enter the row where the first tile will go "); - cout << "(? for help, . to show board) "; + cout << "(? for help, . to show board, - to quit) "; cin >> tempIn; if(tempIn == "?") { return "?"; @@ -163,12 +182,24 @@ string Game::getInput() else if(tempIn == ".") { return "."; } + else if(tempIn == "-") { + return "-"; + } + else if(tempIn == "!") { + return "!"; + } else { + try { + stoi(tempIn); + } + catch(const invalid_argument& ia) { + throw(string("Invalid integer input\n")); + } input.append(tempIn + "-"); } BOLD(" Enter the column where the first tile will go "); - cout << "(? for help, . to show board) "; + cout << "(? for help, . to show board, - to quit) "; cin >> tempIn; if(tempIn == "?") { return "?"; @@ -176,12 +207,24 @@ string Game::getInput() else if(tempIn == ".") { return "."; } + else if(tempIn == "-") { + return "-"; + } + else if(tempIn == "!") { + return "!"; + } else { + try { + stoi(tempIn); + } + catch(const invalid_argument& ia) { + throw(string("Invalid integer input")); + } input.append(tempIn + "-"); } BOLD(" Enter the direction of placement "); - cout << "(? for help, . to show board) "; + cout << "(? for help, . to show board, - to quit) "; cin >> tempIn; if(tempIn == "?") { return "?"; @@ -189,7 +232,16 @@ string Game::getInput() else if(tempIn == ".") { return "."; } + else if(tempIn == "-") { + return "-"; + } + else if(tempIn == "!") { + return "!"; + } else { + if(tempIn.length() != 1 && (tempIn != "h" || tempIn != "v")) { + throw(string("Invalid direction\n")); + } input.append(tempIn); } @@ -229,6 +281,7 @@ void Game::run() bool endTurn; bool allEmpty = false; bool playValid; + bool firstTurn = true; string tileStr; string in = ""; string tempIn = ""; @@ -254,98 +307,133 @@ void Game::run() // Main game loop while(!allEmpty) { for(Player* currPlayer : players) { - plays.push_back(new Play(currPlayer)); - Play* currPlay = plays.back(); - row = col = 7; - endTurn = false; - tileStr = ""; - - currPlayer->toggleTurn(); // Turn begins - gameBoard->show(); - BOLD(" Bag: "); - gameBag->show(); - cout << "\n"; - for(Player* p : players) { - p->show(); - } - - while(!endTurn) { - try { - BOLD(" " + currPlayer->getName()); + if(!currPlayer->rackIsEmpty()) { + plays.push_back(new Play(currPlayer)); + Play* currPlay = plays.back(); + row = col = 7; + endTurn = false; + tileStr = ""; + + currPlayer->toggleTurn(); // Turn begins + gameBoard->show(); + BOLD(" Bag: "); + gameBag->show(); + cout << "\n"; + currPlayer->show(); + + while(!endTurn) { + try { + BOLD(" " + currPlayer->getName()); - in = getInput(); + in = getInput(); - if(in == "?") { - printHelp(); - } - else if(in == ".") { - gameBoard->show(); - BOLD(" Bag: "); - gameBag->show(); - cout << "\n"; - for(Player* p : players) { - p->show(); + if(in == "?") { + printHelp(); } - } - else { - vector> connnectedWords; - vector tileStrVec; - - try { - log(logFilePath, in); + else if(in == ".") { + gameBoard->show(); + BOLD(" Bag: "); + gameBag->show(); + cout << "\n"; + currPlayer->show(); } - catch(string err) { - BOLD_RED_FG(" Unable to open log file\n"); - BOLD_RED_FG(" You can set the path in the Makefile\n"); - BOLD_RED_FG(" Aborting\n"); - exit(1); + else if(in == "-") { + char c; + BOLD_RED_FG(" Are you sure you want to quit? (y/n) "); + cin >> c; + if(c == 'y') { + for(Player* p : players) { + log(logFilePath, p->getName() + ": " + to_string(p->getScore())); + } + exit(0); + } + else if(c == 'n') { + // Do nothing + } + else { + throw(string("Invalid input\n")); + } } - - parsed = parsePlay(in); - - tileStr = parsed[0]; - row = stoi(parsed[1]); - col = stoi(parsed[2]); - dir = parsed[3][0]; - playValid = currPlay->validate(tileStr, gameBoard, row, col, dir); - - if(plays.size() == 1) { - if(!firstTurnCheck(tileStr, row, col, dir)) { - BOLD_RED_FG(" This is the first turn of the game, please make sure the centre square is covered by your word\n"); + else if(in == "!") { + char c; + BOLD_RED_FG(" Skip turn? (y/n) "); + cin >> c; + if(c == 'y') { + currPlayer->toggleTurn(); + endTurn = !endTurn; + } + else if(c == 'n') { + // Do nothing } else { - playValid = true; + throw(string("Invalid input\n")); } } + else { + vector> connnectedWords; + vector tileStrVec; + + try { + log(logFilePath, in); + } + catch(string err) { + BOLD_RED_FG(" " + err); + BOLD_RED_FG(" You can set the path in the CMakeLists.txt file\n"); + BOLD_RED_FG(" Aborting\n"); + exit(1); + } + + parsed = parsePlay(in); - if(playValid) { - tileStrVec = currPlayer->placeTileStr(tileStr, gameBoard, row, col, dir); - connnectedWords = currPlay->getWords(tileStrVec, gameBoard, row, col, dir); - currPlay->calculatePoints(connnectedWords); + tileStr = parsed[0]; + row = stoi(parsed[1]); + col = stoi(parsed[2]); + dir = parsed[3][0]; + playValid = currPlay->validate(tileStr, gameBoard, row, col, dir); - for(vector vec : connnectedWords) { - for(Tile* t : vec) { - t->getSquare()->show(); + if(firstTurn) { + if(!firstTurnCheck(tileStr, row, col, dir)) { + firstTurn = true; + BOLD_RED_FG(" This is the first turn of the game, please make sure the centre square is covered by your word\n"); + } + else { + playValid = true; + firstTurn = false; } - cout << "\n"; } - currPlayer->updateScore(currPlay->getPointsMade()); + if(playValid) { + tileStrVec = currPlayer->placeTileStr(tileStr, gameBoard, row, col, dir); + connnectedWords = currPlay->getWords(tileStrVec, gameBoard, row, col, dir); + currPlay->calculatePoints(connnectedWords, tileStrVec); - currPlayer->draw(tileStr.length(), gameBag); - currPlayer->toggleTurn(); - endTurn = !endTurn; // Turn ends - } - else { - BOLD_RED_FG(" You can't place a word there!\n"); + currPlay->show(); + + if(currPlay->confirmPlay()) { + currPlayer->updateScore(currPlay->getPointsMade()); + currPlayer->draw(tileStr.length(), gameBag); + currPlayer->toggleTurn(); + endTurn = !endTurn; // Turn ends + } + else { + for(Tile* t : tileStrVec) { + currPlayer->returnToRack(t, gameBoard); + } + currPlay->reset(); + } + } + else { + BOLD_RED_FG(" You can't place a word there!\n"); + } } } - } - catch(string ex) { - BOLD_RED_FG(" Error: " + ex); + catch(string ex) { + BOLD_RED_FG(" Error: " + ex); + } } } - // Check whether all racks are empty + // Find out whether all racks are empty + allEmpty = players.front()->rackIsEmpty(); for(Player* p : players) { allEmpty = allEmpty && p->rackIsEmpty(); } @@ -354,6 +442,7 @@ void Game::run() BOLD(" You have placed all tiles!!! Final scores are-\n"); for(Player* p : players) { - p->show(); + log(logFilePath, p->getName() + ": " + to_string(p->getScore())); + BOLD_WHITE_FG(p->getName() + ": " + to_string(p->getScore()) + "\n"); } } diff --git a/src/main.cc b/src/main.cc index 80c8271..648f8b8 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,4 +1,4 @@ -#ifdef DBG +#ifdef DEBUG #include #include "game.h" @@ -10,7 +10,7 @@ void testBoard(); int main() { - testBoard(); + testGame(); return 0; } diff --git a/src/play.cc b/src/play.cc index ccf4feb..ab64270 100644 --- a/src/play.cc +++ b/src/play.cc @@ -22,6 +22,23 @@ Play::~Play() delete playMaker; } +void Play::show() +{ + string wordStr = ""; + for(vector word : wordsInPlay) { + for(Tile* t : word) { + wordStr.append(t->getLetterStr()); + } + wordStr.append(" + "); + } + if(!wordStr.empty()) { + wordStr.replace(wordStr.end()-3, wordStr.end(), ""); + } + + BOLD_WHITE_FG(" Words in play: " + wordStr + "\n"); + BOLD_WHITE_FG(" " + to_string(pointsMade) + " points\n"); +} + void Play::setPlayer(Player* p) { playMaker = p; @@ -100,7 +117,6 @@ bool Play::validate(string tileStr, Board* b, int r, int c, char dir) vector> Play::getWords(vector tilesInStr, Board* b, int r, int c, char dir) { - vector> words; vector placedTiles; int currRow = r; int currCol = c; @@ -118,13 +134,13 @@ vector> Play::getWords(vector tilesInStr, Board* b, int r, placedTiles.push_back(currSquare->getTile()); currSquare = currSquare->getRight(); } - words.push_back(placedTiles); + wordsInPlay.push_back(placedTiles); for(Tile* t : tilesInStr) { currSquare = t->getSquare(); if(( currSquare && currSquare->getAbove() && !currSquare->getAbove()->isEmpty() ) || ( currSquare && currSquare->getBelow() && !currSquare->getBelow()->isEmpty() )) { - words.push_back(getConnectedWord(t, 'v')); + wordsInPlay.push_back(getConnectedWord(t, 'v')); } } } @@ -145,13 +161,13 @@ vector> Play::getWords(vector tilesInStr, Board* b, int r, placedTiles.push_back(currSquare->getTile()); currSquare = currSquare->getBelow(); } - words.push_back(placedTiles); + wordsInPlay.push_back(placedTiles); for(Tile* t : tilesInStr) { currSquare = t->getSquare(); if(( currSquare && currSquare->getLeft() && !currSquare->getLeft()->isEmpty() ) || ( currSquare && currSquare->getRight() && !currSquare->getRight()->isEmpty() )) { - words.push_back(getConnectedWord(t, 'h')); + wordsInPlay.push_back(getConnectedWord(t, 'h')); } } } @@ -165,7 +181,7 @@ vector> Play::getWords(vector tilesInStr, Board* b, int r, throw; } - return words; + return wordsInPlay; } vector Play::getConnectedWord(Tile* t, char dir) @@ -202,7 +218,7 @@ vector Play::getConnectedWord(Tile* t, char dir) return connectedWord; } -void Play::calculatePoints(vector> words) +void Play::calculatePoints(vector> words, vector tileStrVec) { int multiplier = 1; @@ -211,36 +227,72 @@ void Play::calculatePoints(vector> words) */ for(vector word : words) { for(Tile* t : word) { - DEBUG("sqType", t->getSquare()->getType()); - DEBUG("getPoints", t->getPoints()); switch(t->getSquare()->getType()) { case N: pointsMade += t->getPoints(); break; case DLS: - pointsMade += 2*(t->getPoints()); + if(tilePresent(tileStrVec, t)) { + pointsMade += 2*(t->getPoints()); + } + else { + t->show(); + pointsMade += t->getPoints(); + } break; case TLS: - pointsMade += 2*(t->getPoints()); + if(tilePresent(tileStrVec, t)) { + pointsMade += 3*(t->getPoints()); + } + else { + pointsMade += t->getPoints(); + } break; case TWS: pointsMade += t->getPoints(); - multiplier *= 3; + if(tilePresent(tileStrVec, t)) { + multiplier *= 3; + } break; case DWS: pointsMade += t->getPoints(); - multiplier *= 2; + if(tilePresent(tileStrVec, t)) { + multiplier *= 2; + } break; } } - DEBUG("pointsMade", pointsMade); } - pointsMade *= multiplier; - DEBUG("Final pointsMade", pointsMade); } int Play::getPointsMade() { return pointsMade; } + +bool Play::confirmPlay() +{ + char ch; + PALE_GREEN_FG(" Confirm the play? (y/n) "); + cin >> ch; + + switch(ch) { + case 'y': + return true; + break; + case 'n': + return false; + break; + default: + return false; + break; + } +} + +void Play::reset() +{ + pointsMade = 0; + wordsInPlay.clear(); + playStr = ""; +} diff --git a/src/player.cc b/src/player.cc index 70a94fc..92184b2 100644 --- a/src/player.cc +++ b/src/player.cc @@ -24,10 +24,10 @@ Player::~Player() void Player::show() { if(turn) { - BOLD_BRIGHT_GREEN_FG(" " + playerName + ": " + to_string(score) + " points\n\n"); + BOLD_BRIGHT_GREEN_FG(" " + playerName + ": " + to_string(score) + " points\n"); } else { - BOLD(" " + playerName + ": " + to_string(score) + " points\n\n"); + BOLD(" " + playerName + ": " + to_string(score) + " points\n"); } rack->show(); @@ -39,6 +39,11 @@ string Player::getName() return playerName; } +int Player::getScore() +{ + return score; +} + void Player::setName(string name) { playerName = name; @@ -86,3 +91,10 @@ bool Player::rackIsEmpty() { return rack->isEmpty(); } + +void Player::returnToRack(Tile* t, Board* b) +{ + if(t) { + rack->addTile(b->retrieve(t->getSquare()->getRow(), t->getSquare()->getCol())); + } +} diff --git a/src/utils.cc b/src/utils.cc index 9a9196e..0a47941 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -26,6 +26,13 @@ bool tilePresent(std::vector t, Tile* target) return(it == t.end() ? false : true); } +bool charPresent(string str, char ch) +{ + auto it = str.end(); + it = std::find(str.begin(), str.end(), ch); + return(it == str.end() ? false : true); +} + std::vector parsePlay(std::string in) { vector parse;