# CS50 Nuggets ## Implementation Spec ### Team XVI, Spring 2024 According to the [Requirements Spec](REQUIREMENTS.md), the Nuggets game requires two standalone programs: a client and a server. Our design also includes player, map, gridpoint, and visualization modules. We describe each program and module separately. We do not describe the `support` library nor the modules that enable features that go beyond the spec. We avoid repeating information that is provided in the requirements spec. ## Separation into Modules ### Programs - [Client](#Client-Module) - [Server](#Server-Module) ### Game-Logic Library - [Player](#Player-Module) - [Gridpoint](#Gridpoint-Module) - [Map](#Map-Module) - [Visibility](#Visibility-Module) ## Plan for division of labor - Client Program - Zach - Server Program - Michael - Visibility - Will - Map - Dylan ## Client Module ### Data structures The client also heavily utilizes a module, `message`. More detail on the `message_` module can be found in its associated header file. The message module initializes communication to a server. On the client's end: connect to server message_loop recieve keystrokes and send messages PLAY, SPECTATE, KEY The `message_loop` takes two functions to handle `input` and `messages`. Both functions will take a `void* arg` parameter. We will need to pass several client variables into these functions, so it is cleanest to store them all in a struct `user_t`: ```c typedef struct user{ addr_t server; char* playername; char letter; int goldStatus[3]; char explanation[100]; const char* quitExplanation; }user_t; ``` * The `server` of type `addr_t` is a module off of `message`, and holds various _sin_ and _IP_ info. * The `playername` should be `NULL` if the user is a _spectator_ otherwise it should be the provided playername. * `letter` Is assigned to the user from an `OK L` message, where `L` is the user's spot in an alphabetical array. * `goldStatus` is a 3-int array which stores `n`, `p`, & `r`: _gold recieved on newest move_, _total gold in purse_, & _remaining gold in game_ respectively * `explanation` is allocated on the stack, and is the right-most information on the status line * `quitExplanation` is recieved after inputting `Q` as a client. It prints after closing the game ### Definition of function prototypes Validate and assign command-line arguments. ```c static void parseArgs(const int argc, char* argv[], char** serverHost, char** serverPort, user_t* user); ``` Initialize display and begin communication with server. ```c static void initializeClient(user_t* user); ``` Validate and process user keystrokes. ```c static bool handleInput(void* arg); ``` Validate and process messages received from server. ```c static bool handleMessage(void* arg, const addr_t from, const char* message); ``` Print out the visible map recieved from the server. ```c static void displayGrid(const char* grid); ``` Print out game updates recieved from the server. ```c static void displayStatusLine(user_t* user); ``` Helper functions to break up message handling. ```c static void recieveGRID(const char* info); static void recieveOK(const char* info, user_t* user); static void recieveGOLD(const char* info, user_t* user); static void recieveQUIT(const char* info, user_t* user); static void recieveERROR(const char* info, user_t* user); ``` ### Detailed pseudo code #### `main`: The main function should parse user arguments with `parseArgs`, set up client-side messaging with `initializeClient`, call a `message_loop`, and exit depending on the success of the loop: parseArgs and validate user parameters initializeClient and send initial messages to server bool success mapped to message_loop(server, handleInput, handleMessage) end ncurses print any closing messages return success casted as an interger #### `parseArgs`: To validate all the user parameters passed into the command line, a function`parseArgs` is helpful to handle all non-zero exit statements. `parseArgs` should check that there is a correct count of parameters, initialize pointers from `argv[]`, validate those parameters, and exit non-zero at any point of failure: if (parameters == 3 || 4) initialize message module on failure exit non-zero print assigned port number if (3 parameters) declare client as a spectator if (4 parameters) declare client as a player set user->playername else exit non-zero #### `initializeClient`: The `initializeClient` function should set up `ncurse` within the terminal for individual character parsing and a refreshing display. On success, the function sends an initial `PLAY` or `SPECTATE` message to the server: validate message addr_t initialize curses if (user->playername == NULL) send "SPECTATE" to server else copy "PLAY " to message copy playername to message send message to server #### `handleInput`: The `handleInput` function is called from `message_loop` to continually get keystrokes and send them to the server: create addr_t from arg* if (addr == NULL) return true and break _loop if (addr != valid) return true and break _loop current char keystroke bool valid keystroke if getch() != ERR char[] of possible lowercase keystrokes iterate through [] if tolower(keystroke) == char[i] AND playername != NULL valid is true if keystroke == 'Q' valid is true else return true if valid send "KEY " + keystroke return false #### `handleMessage`: The `handleMessage` will recieve messages from the server. The client should only be recieving `OK`, `GRID`, `GOLD`, `DISPLAY`, `QUIT`, and `ERROR` messages. For clarity sake, the `recieve` functions have been integrated into the switch. The logic should be separated: So validate as such and display the grid: set user->addr_t to new from addr_t tokenize first word of message into char* phrase tokenize rest of message into char* info switch phrase case "OK" update user->letter to info case "GRID" scan rows and columns into ints getmaxyx() for resolution of screen while screen resolution < grid resolution display warning in status line case "GOLD" scan n, p, r into goldStatus[3] update explanation to display gold recieved case "DISPLAY" mvprintw infor on line below status (1,0) case "QUIT" copy quit explanation into user case "ERROR" update explanation to display error #### `displayStatusLine`: `displayStatusLine` is called through the message handler, and deals strictly with printing the `statusLine`(s) recieved from the server: if user->playername != NULL mvprintw player letter and their gold, gold remaining, and most recent status update else mvprintw gold remaining, and most recent status update refresh() ncurses --- ## Server Module ```c // Global constants: static int MaxNameLength = 50; // max number of chars in playerName static int MaxPlayers = 26; // maximum number of players ``` ### Data structures ### Definition of function prototypes A function to parse and validate the command-line arguments. ```c void parseArgs(int argc, char* argv[], char** mapText, int* seed); ``` A function to stop the game and free all memory. ```c void gameOver(addr_t* playerAddresses, char* playerNames); ``` Called by the message loop each time a message is received; correctly responds to each message ```c static bool handleMessage(void* arg, const addr_t from, const char* message); ``` send a GOLD message to all the players, specifically telling `player` how much new gold they aquired. ```c static void sendGoldUpdates(addr_t* addresses, char player, int amt); ``` send a DISPLAY message to all the players with the current state of the map for their view. ```c static void sendDisplayUpdates(addr_t* addresses); ``` get the player character corresponding to a given address ```c static char getCharFromAddress(addr_t* playerAddresses, addr_t* address); ``` ### Detailed pseudo code #### `main`: call parseArgs call map_init with the map text filename playerAddresses <-- an array of MaxPlayers+1 blank addr_t*'s playerNames <-- an empty array of strings numPlayers <-- 0 set up and start messaging loop - log to stderr - give handleMessage a copy of the playerAddresses and numPlayers call gameOver // clean up free all memory associated with playerAddresses and playerNames arrays close down map module close down message module #### `parseArgs`: validate num of args verify map file can be opened for reading if seed provided verify it is a valid seed number seed the random-number generator with that seed else seed the random-number generator with getpid() #### `gameOver`: intialize game over message for char c in A..Z: get final info about player c add info to game over message for address in playerAddresses: send game over message to address #### `handleMessage`: these cases will likely be separated into their own functions. unpack arg into playerAdresses array and numPlayers char player <-- getCharFromAddress switch on first word of message: case PLAY: if numPlayers == maxPlayers: send quit message else if no name: send quit message for no name else: tell map to add a new character if unsuccessful, tell client to quit else: send the OK message to client normalize name add player address and name to approriate arrays send GRID message send individual GOLD message sendDisplayUpdates return false case SPECTATE: if there is a spectator: tell spectator to quit set client address to start of playerAddresses send GRID message send individual GOLD message send individual DISPLAY message return false case KEY: // right now this just skips an invalid key if player is 0: tell client to quit and that it needs to start the game as a player or spectator if key is 'q' or 'Q': send QUIT message to client remove player from map remove player from server (free and null-out associated data) else if key is a movement key: if player is '@': tell client ERROR: spectators cannot move tell map to move character if successful: sendDisplayUpdates if gold was collected: sendGoldUpdates(player, amt) if no more gold in the game: return true return false default: send a message-type error return false #### `sendGoldUpdates`: t <-- get total remaining gold for char c in @,A..Z: p <-- get c's purse if c is the player: n <-- amt else: n <-- 0 send "GOLD n p t" to c's address #### `sendDisplayUpdates`: for char c in @,A..Z: mapString <-- get c's map string send "DISPLAY\nmapString" to c's address #### `getCharFromAddress`: for char c in @,A..Z: if playerAdresses[c] matches given address: return c return 0 --- ## Gridpoint Module ### Data structures We implement a new data structure `gridpoint` that stores information about a location in the map. The gridpoint will store the default character of the map, the gold amount, a set of other gridpoints visible from each point, and the player at the gridpoint. The gridpoint should be defined as follows: ```c typedef struct gridpoint{ int x; int y; char mapChar; char player; int gold; bool visible[MaxPlayers+1]; hashtable_t* visiblePoints; } gridpoint_t*; ``` Where: * *x* and *y* represent the (row,col) coordinates of the gridpoint * *mapChar* represents the background character \{ + , - , | , # , • , etc. } * *player* represents the letter occupying a space * *gold* represents the quantity of gold occupying the space * *visible* is an array of players to whom the gridpoint is visible. The 0-index represents the spectator, who can see everything; as such, every entry is always true. * i.e., a gridpoint visible to players 'A' and the spectator would contain: ```c [true, true, false, false ... false]; ``` The module also makes use of our previously developed `hashtable` data structure to store calculated visibility from a certain gridpoint. This tactic allows us to perform the expensive player visibility computations only once per gridpoint and only when necessary. ### Definition of function prototypes Set the visibility of a player at a certain gridpoint ```c gridpoint_t* gridpoint_new(char mapChar); ``` Get the default map character at a certain gridpoint ```c char gridpoint_getChar(gridpoint_t* gp); ``` Get the hashtable of gridpoints that can be seen from a certain gridpoint ```c hashtable_t* gridpoint_getVisibility(gridpoint_t* gp); ``` Get the gold at a certain gridpoint ```c int gridpoint_getGold(gridpoint_t* gp); ``` Get the player at a certain gridpoint, if any ```c char gridpoint_getPlayer(gridpoint_t* gp); ``` Set the hashtable of gridpoints that can be seen from a certain gridpoint ```c bool gridpoint_setVisibility(gridpoint_t* gp, hashtable_t* visible); ``` Set the gold at a certain gridpoint ```c bool* gridpoint_setGold(gridpoint_t* gp, int gold); ``` Set the player at a certain gridpoint ```c bool* gridpoint_setPlayer(gridpoint_t* gp, char player); ``` Print a gridpoint for debugging ```c bool* gridpoint_print(gridpoint_t* gp); ``` ### Detailed pseudo code The functions within the gridpoint module are trivial (simply getting and setting the variables within a gridpoint), so we omit pseudocode for this module. --- ## Player Module This module provides a player data structure to represent a single player of the game. ### Data Structures ```c typedef struct player { bool active; int gold; int xPos; int yPos; hashtable_t* seen; } player_t*; ``` ### Definition of Function Prototypes Add a new player to the game: ```c player_t* player_new(); ``` Remove a player from the game: ```c void player_delete(player_t* player); ``` Getters and Setters: ```c void player_setGold(player_t* player, int gold); int player_getGold(player_t* player); void player_setPos(player_t* player, int x, int y); int player_getX(player_t* player); int player_getY(player_t* player); ``` --- ## Map Module ```c static int GoldTotal = 250; // amount of gold in the game static int GoldMinNumPiles = 10; // minimum number of gold piles static int GoldMaxNumPiles = 30; // maximum number of gold piles ``` ### Data structures We implement a new datastructure called `map`, which holds information about the game status. This includes an array of gridpoints, redefined as a `grid`, the total gold left in the game, the players in the game, and the number of players who have joined the game. *Note: num_players is non-decreasing, a player leaving the game will not affect this value* ```c typedef gridpoint_t*** grid_t; struct map { grid_t grid; int totalGold; int remainingGold; player_t** players; int num_players; int width; int height; } map_t* ``` This map is initialized as a global variable, and all functions reference this global map. ### Definition of function prototypes Initialize the map global variable from a provided text file ```c bool map_init(fileName); ``` Distribute gold across the map in between minPiles and maxPiles piles ```c static bool map_distributeGold(); ``` Gets the remaining gold amount ```c int map_getRemainingGold(): ``` Get the amount of money in a player's purse ```c int map_getPurse(char player); ``` Gets the player with certain char id ```c static player_t* map_getPlayer(char player); ``` Randomly place character in map, create player struct ```c char map_addPlayer(); ``` Removes a player from the game ```c void map_removePlayer(char player); ``` Returns the gridpoint at a certain location ```c gridpoint_t* map_getLocation(int x int y); ``` Cleans up the map ```c void map_delete(); ``` Moves player in certain direction (key input) ```c int map_movePlayer(char player, char direction) ``` Moves player exactly once, if possible. ```c static int map_movePlayerOnce(char player, int dy, int dx); ``` ```c int map_getWidth(); int map_getHeight(); ``` ### Detailed pseudo code Pseudocode for `map_init`: ```c setWidth = false; file = fopen(fileName); while((c = getc(file)) != EOF): if(c == '\n'): setWidth = true; map.height++; elif(setWidth == false): map.width++; rewind(file); map.grid = malloc(sizeof(gridpoint_t**)*height); for(row in map.grid): row = malloc(sizeof(gridpoint_t*)*width); int x = 0; int y = 0; gridpoint_t* gp; while((c = getc(file)) != EOF): gp = gridpoint_new(c); map.grid[y][x] = gp; ``` Pseudocode for `map_distributeGold()` ```c int numPiles = rand()%(maxPiles - minPiles); numPiles+=minPiles; ``` Pseudocode for `map_getPlayer()` ```c return map.players[player-'A'] ``` Pseudocode for `map_addPlayer()` ```c if we don't have room for a new character: return 0 else: char player = numPlayers + '@'; increment number of players placed = false; while(!placed): x = rand()%width y = rand()%height if(gridpoint_getChar(map.grid[y][x]) == '.' && gridpoint_getPlayer(map.grid[y][x]) == '\0' && gridpoint_getGold((map.grid[y][x])) == 0): new_player = player_new(player); if (new_player == NULL) return 0; gridpoint_setPlayer(map.grid[y][x], player); player_setPos(new_player, x, y); add player to list of players; placed = true; return player ``` Pseudocode for `map_removePlayer()` ``` if player doesn't exist, return free player struct set pointer in players array to NULL remove character from the grid ``` Pseudocode for `map_getLocation()` ```c return map.grid[y][x] ``` Pseudocode for `map_movePlayer()` ```c ignore invalid direction if(isUpper(dir)): move player until hitting a wall or swapping with a player keep track of total gold collected else: move player once in direction if possible record gold collected if movement was unsuccessful, return -1 else return amt of gold collected ``` Pseudocode for `map_delete()` is omitted for the implementation spec, as it's relatively trivial --- ## Visibility Module ### Control Flow Visibility functions are called only by the map module. visibility_set is called whenever a player moves or is added to the game. visibility_clear is called whenever a player moves, *before they move*. ### Data structures Visibility relies on the `grid_t` 2D array of gridpoints contained within the global `map` structure. This module also needs access to the player structure. ### Definition of function prototypes Calculates which gridpoints should be visible to a particular player. ```c void visibility_set(map_t map, player_t* player); ``` resets the gridpoints that are visible while retaining those that have been seen ```c void visibility_clear(map_t map, player_t* player); ``` ### Detailed pseudocode visibility_set: if gridpoint at current location has a non-NULL hashtable this location has been calculated before for each gridpoint item in the hashtable: set visibility for this gridpoint and provided player to true otherwise, calculate all points that are visible px = player_getX py = player_getY mark map[px][py] as visible and seen for current player add map[px, py] to hashtable of visible gridpoints for this gridpoint iterate through each gridpt gx, gy in the map get the horizontal distance from player to gridpt get the vertical distance from player to gridpt dy/dx = vertical / horizontal dx/dy = horizontal / vertical float current x = px float current y = py bool checkRow = false bool checkCol = false Iterate through the rows, starting at py+1: while cy is less than gridpoint y current x += dx/dy current y += 1 if the current row is a gridpt (cx % 1 = 0) if the gridpt == {+,-,|,#} it blocks visibility. set visibility to false, just in case break out of while loop otherwise, round down column if both map[x][y] and map[x+1][y] == {+,-,|,#}: set visibility to false, just in case break out of while loop if no wall detected, checkRow = true if checkRow is false, break and move to next gridpt otherwise, reset cx and cy Iterate through the rows, starting at py+1: while cx < gridpoint x current x += 1 current y += dy/dx if the current col is a gridpt (cy % 1 = 0) if the gridpt == {+,-,|,#} it blocks visibility. set visibility to false, just in case break out of while loop otherwise, round down row if both map[x][y] and map[x][y+1] == {+,-,|,#}: set visibility to false, just in case break out of while loop if no wall detected, checkCol = true; if checkCol is false: break and move to next column otherwise: mark map[cx][cy] as visible to player add map[cx][cy] to visiblePoints for key "px,py" visibility_clear: for each row in the map: for each col in the map: gridpoint = map[x][y] if a gridpoint was visible to current player seen for current player is true visibility for current player is false ### Error Handling and Recovery Any error producing the visibility array would likely be fatal or result in unexpected behavior when displaying the map to a client. As such, the visibility functions do attempt to catch and respond to errors; this is left to the higher-level modules. Visibility functions assume a valid `map_t` map and an existing `player`, both of which should be validated prior to their inclusion as a parameter in a function call. The use of a `hashtable` to document other gridpoints visible from a given gridpoint is a memory-intensive process. While visibility_set does not allocate memory for new sets, it does add items to existing sets. So long as `set_delete()` is eventually called on each gridpoint, no memory issues should occur. --- ## Testing plan ### unit testing #### map and visibility We plan to manually create a few small environment maps in which to test that the visibility functions run as expected. This will be implemented in a visibilityTest module, complementary to the visibility module. * First, we will build a 4x4 room encased by walls to ensure that all gridpoints are visible to one or two players. For both scenarios, we will artificially 'move' the player by altering the contents of the player struct. To validate the resulting * As display of characters and gold is handled by a simple conditional within the server's display, testing of this component will simply consist of checking a gridpoint's visibility and the results of calling gridpoint_getGold and gridpoint_getPlayer. * Second, we will build a more complex corridor similar to the one in the demonstration video to test that hidden gridpoints remain hidden. Again, we will artificially 'move' players to check that the clearVisibility function accurately preserves seen gridpoints. ### integration testing #### server We plan to test the functional server with the provided dummy client scripts. We'll also make use of the simple stdin-based client to try out bad input. - trying to joing while already playing - spectating while already playing - sending movement strokes as a spectator #### client We plan to test the functional client with the provided `miniserver.c`. When both the server and client are functional, we plan to test the two together in a variety of _valid_ cases, including: * One player * One spectator * One player and one spectator * Several players * Several players and one spectator As well, we will test the client for _invalid edge_ and _base_ cases: * More than 1 spectator(s) * More than 26 players * Unopened server port These tests will probe only the ability of the server and client to process joining and quitting. Full-function testing is described below. ### system testing We plan to apply a provided bot testing script, complete with 26 players and a spectator. Each client sends random messages to the server. These messages are valid but may not code for valid movement. We'll also try having too many clients and clients that leave and rejoin a lot --- ## Limitations > To be updated as modules are implemented.