// This is the CPP file you will edit and turn in. // Also remove these comments here and add your own. // TODO: remove this comment header #include "grid.h" #include "lifeutil.h" #include "unistd.h" #include #include #include const char *const FORMAT_UNDERSCORE = "\033[4m"; const char *const END_FORMATTING = "\033[0m"; const int ANIMATION_SLEEP_MS = 100, ANIMATION_N_GENERATIONS = 20; void printWelcome() { std::cout << "Welcome to the TDDD86 Game of Life,\n" << "a simulation of the lifecycle of a bacteria colony.\n" << "Cells (X) live and die by the following rules:\n" << " - A cell with 1 or fewer neighbours dies.\n" << " - Locations with 2 neighbours remain stable.\n" << " - Locations with 3 neighbours will create life.\n" << " - A cell with 4 or more neighbours dies." << std::endl; } std::string input(const char *message) { if (message != nullptr) std::cout << message; std::cout << FORMAT_UNDERSCORE; std::flush(std::cout); std::string ret; std::getline(std::cin, ret); std::cout << END_FORMATTING; return ret; } bool fileExists(const char *fileName) { return access(fileName, 0) == 0; } std::string fileRead(const std::string &filePath) { std::ifstream file(filePath, std::ios::binary); if (!file.is_open()) throw std::runtime_error("File couldn't be found"); std::string content; const int BUF_LENGTH = 1024; char buf[BUF_LENGTH]; while (file.read(buf, BUF_LENGTH)) content.append(buf, file.gcount()); content.append(buf, file.gcount()); if (!file.eof() && file.fail()) throw std::runtime_error("Error: Failed to read the file '" + filePath + "'"); return content; } enum MenuAction { ANIMATE, TICK, QUIT }; MenuAction askMenu() { while (true) { std::string userInput = input("a)nimate, t)ick, q)uit? "); if (userInput == "a") return MenuAction::ANIMATE; else if (userInput == "t") return MenuAction::TICK; else if (userInput == "q") return MenuAction::QUIT; std::cout << "The input '" << userInput << "' is an invalid option!\n"; } } std::vector split(const std::string &s, const char at) { std::vector ret; size_t last = 0, idx; while ((idx = s.find(at, last)) != std::string::npos) { std::string sub = s.substr(last, idx - last); ret.push_back(sub); last = idx + 1; } ret.push_back(s.substr(last)); return ret; } enum Cell { ALIVE, DEAD }; void gridPrint(Grid &grid) { std::stringstream ss; for (int row = 0; row < grid.numRows(); row++) { for (int col = 0; col < grid.numCols(); col++) { char sign = grid.get(row, col) == Cell::ALIVE ? 'X' : '-'; ss << sign; } if (row == grid.numRows() - 1) ss << std::endl; else ss << '\n'; } std::cout << ss.str(); } int gridAliveNeighbors(Grid &grid, int row, int col) { int alive = 0; for (int rowDelta = -1; rowDelta <= 1; rowDelta++) { int checkRow = row + rowDelta; // Skip out of bounds if (checkRow < 0 || checkRow >= grid.numRows()) continue; for (int colDelta = -1; colDelta <= 1; colDelta++) { // Don't check the original piece if (rowDelta == 0 && colDelta == 0) continue; int checkCol = col + colDelta; // Skip out of bounds if (checkCol < 0 || checkCol >= grid.numCols()) continue; if (grid.get(checkCol, checkRow) == Cell::ALIVE) alive++; } } return alive; } /** * Run one Game of Life tick. * Cells (X) live and die by the following rules: * - A cell with 1 or fewer neighbours dies. * - Locations with 2 neighbours remain stable. * - Locations with 3 neighbours will create life. * - A cell with 4 or more neighbours dies. */ void gridTick(Grid &grid) { Grid nextGrid = grid; for (int row = 0; row < grid.numRows(); row++) { for (int col = 0; col < grid.numCols(); col++) { int alive = gridAliveNeighbors(grid, row, col); // A cell with 1 or fewer neighbours dies. if (alive == 1) nextGrid.set(row, col, Cell::DEAD); // Locations with 2 neighbours remain stable. else if (alive == 2) nextGrid.set(row, col, grid.get(row, col)); // Locations with 3 neighbours will create life. else if (alive == 3) nextGrid.set(row, col, Cell::ALIVE); // A cell with 4 or more neighbours dies. else if (alive == 4) nextGrid.set(row, col, Cell::DEAD); } } grid = nextGrid; } int main() { printWelcome(); std::string inputFile; while (true) { inputFile = input("Grid input file name? "); if (fileExists(inputFile.c_str())) break; std::cout << "File '" << inputFile << "' doesn't exist!" << std::endl; } std::string fileContents; try { fileContents = fileRead(inputFile); } catch (std::exception &e) { std::cerr << "Failed to read file: " << e.what() << std::endl; return 1; } std::vector lines = split(fileContents, '\n'); if (lines.size() <= 2) { std::cerr << "No column, row header, in the file!" << std::endl; return 1; } int headerRows, headerColumns; try { headerRows = std::stoi(lines[0]); headerColumns = std::stoi(lines[1]); } catch (const std::invalid_argument &e) { std::cerr << "Column or row wasn't a number" << std::endl; return 1; } catch (const std::out_of_range &e) { std::cerr << "Column or row was too large" << std::endl; return 1; } int possibleRows = lines.size() - 2; if (possibleRows < headerRows) { std::cerr << "There's less rows (" << possibleRows << ") than described in the header! (" << headerRows << ')' << std::endl; return 1; } Grid grid(headerColumns, headerRows); for (int x = 0; x < headerRows; x++) { std::string &row = lines[x + 2]; if (headerColumns > row.size()) { std::cerr << "There's less columns (" << row.size() << "), than described in the header! (" << headerRows << ')' << std::endl; return 1; } for (int y = 0; y < headerColumns; y++) { char sign = row[y]; switch (sign) { case '-': grid.set(x, y, Cell::DEAD); break; case 'X': grid.set(x, y, Cell::ALIVE); break; default: std::cerr << "Invalid option '" << sign << "' in grid description!" << std::endl; return 1; } } } gridPrint(grid); MenuAction action; while ((action = askMenu()) != MenuAction::QUIT) { // We don't have to handle QUIT, since we do in the while check switch (action) { case MenuAction::TICK: gridTick(grid); gridPrint(grid); break; case MenuAction::ANIMATE: for (int i = 0; i < ANIMATION_N_GENERATIONS; i++) { gridTick(grid); clearConsole(); gridPrint(grid); pause(ANIMATION_SLEEP_MS); } break; } } std::cout << "Have a nice Life!" << std::endl; return 0; }