Nuggets
This is my team’s final project for COSC 50 Software Design and Implementation. Our team consisted of Sunbir Chawla, Isaiah Martin, Austin Zhang, and myself. This is a “Nuggets” game, in which players explore a set of rooms and passageways in search of gold nuggets. The rooms and passages are defined by a map loaded by the server at the start of the game. The gold nuggets are randomly distributed in piles within the rooms. Up to 26 players, and one spectator, may play a given game. Each player is randomly dropped into a room when joining the game. Players move about, collecting nuggets when they move onto a pile. When all gold nuggets are collected, the game ends and a summary is printed.
GitHub
You can find all of our code right here.
Requirements Spec
A multi-player exploration game, Nuggets, in which a game server maintains all game state, and one or more game clients display the game to a player. The object of the game is to collect more gold nuggets than any other player. The game ends when all gold nuggets have been collected by some player.
- Game play occurs in a set of interconnected rooms and passages, as defined by a map.
- At game start time,
GoldTotal
nuggets are randomly dropped in a random number of random-sized piles, at some spot in a room. Gold nuggets are indistinguishable; a pile contains at least one nugget. - There are zero to
MaxPlayers
players, and zero or one spectators. - A new player is dropped into a random empty spot within a room.
- A new player initially has 0 nuggets in its purse.
- A player can see only those spots that are visible from its current location.
- A player can know the boundaries of all rooms and passages it has seen since the player began playing.
- The spectator immediately knows and always sees all gridpoints.
- The display is an ASCII screen large enough to represent the entire grid.
- At any given time, a player’s display illustrates all known boundaries and all visible spots; the spectator’s display illustrates all boundaries and spots.
- A player moving into a spot containing a pile of gold collects that gold, adding all the pile’s nuggets to the player’s purse. The pile is then gone.
- A player moving into a spot occupied by another player causes the two players to switch places.
- The game ends when all gold nuggets have been collected.
- At end, the game announces to all players (and spectator, if any) the size of each players’ purse; the player(s), spectator, and server then quit.
Constants
The game has several parameters; although other values are reasonable, we specify the following.
static const int MaxBytes = 65507; // max number of bytes in a message
static const int MaxNameLength = 50; // max number of chars in playerName
static const int MaxPlayers = 26; // maximum number of players
static const int GoldTotal = 250; // amount of gold in the game
static const int GoldMinNumPiles = 10; // minimum number of gold piles
static const int GoldMaxNumPiles = 30; // maximum number of gold piles
Server
./server map.txt [seed]
The server shall
- Start from the commandline of the form above; thus the first argument is the pathname for a map file and the second argument is an optional integer providing a seed for the random-number generator.
- Verify its arguments; if error, provide a useful error messages and exit non-zero.
- If the optional seed is provided, the server shall pass it to
srandom(seed)
. If no seed is provided, the server shall usesrandom(time(NULL))
to produce random behavior. - Load the designated map file; the server may assume it is valid.
- Initialize the game by dropping at least
GoldMinNumPiles
and at mostGoldMaxNumPiles
gold piles on random empty spots; each pile shall have a random number of nuggets. - Initialize the network and announce the port number.
- Wait for messages from clients.
- Accept up to
MaxPlayers
players; if a player exits or quits the game, it can neither rejoin nor be replaced. - Accept up to 1 spectator; if a new spectator joins while one is active, the server shall tell the current spectator to quit, and the server shall then forget that current spectator.
- React to each type of inbound message as described in the protocol below.
- Update all clients whenever any player moves or gold is collected.
- Monitor the number of gold nuggets remaining; when it reaches zero, the server shall send a
GAMEOVER
message to all clients, print the Game-over summary, and exit.
The server should log useful information that can be saved in a logfile; a typical approach would be to log to stderr and thus usage could be:
./server 2>server.log map.txt
Player
./player hostname port [playername]
The player shall
- start from the commandline of the form above; thus the first argument is the name or IP address where the server is running, and the second argument is the port number on which the server expects messages; the third (optional) argument determines whether to join as a player or spectator.
- Verify its arguments; if error, provide a useful error messages and exit non-zero.
- If the
playername
argument is provided, the player shall truncate it (if necessary) to limit it toMaxNameLength
characters. - If the
playername
argument is provided, the user joins as a player and can interactively play the game. - If the
playername
argument is not provided, the user joins as a view-only spectator. - Initialize ncurses.
- Initialize the network and join the game with a
PLAY
orSPECTATE
message accordingly. - Upon receipt of a
GRID
message, ensure the window is large enough for the grid (it should be NR+1 x NC+1 for best results). - Display a status line on the first line of the display, in the protocol below.
- Display the game grid on the subsequent lines of the display, as noted in the protocol below.
- Update the display any time new information arrives from the server.
- Quit when told to do so by the server, as noted in the protocol below.
- Display a brief note on the status line if an unknown or malformed message arrives from the server.
- Quit the game if reaching EOF on stdin.
- Print a Game-over summary and exit, as noted in the protocol below.
The player should log useful information that can be saved in a logfile; a typical approach would be to log to stderr and thus usage could be:
./player 2>player.log hostname port playername
./player 2>spectator.log hostname port
Player interface
The display shall consist of NR+1 rows and NC columns. The player program shall complain if the window is too small, and wait for the user to enlarge the window.
The top line shall provide game status; for a player, it should look like this:
Player A has 39 nuggets (211 nuggets unclaimed).
If other information needs to be displayed briefly, it is placed on the right:
Player A has 39 nuggets (211 nuggets unclaimed). GOLD received: 39
Player A has 39 nuggets (211 nuggets unclaimed). unknown keystroke
The spectator’s status line should look like this:
Spectator: 211 nuggets unclaimed.
Grid display: The remaining NR lines present the grid using map characters:
- ` ` solid rock - interstitial space outside rooms
-
a horizontal boundary|
a vertical boundary+
a corner boundary.
an empty spot#
a passage spot
or occupant characters:
@
the playerA
-Z
another player*
a pile of gold
Spectator keystrokes: The spectator can type
Q
quit the game.
Player keystrokes: The player can type
Q
quit the game.h
move left, if possiblel
move right, if possiblej
move down, if possiblek
move up , if possibley
move diagonally up and left, if possibleu
move diagonally up and right, if possibleb
move diagonally down and left, if possible-
n
move diagonally down and right, if possible - where possible means the adjacent gridpoint in the given direction is an empty spot, a pile of gold, or another player.
- for each move key, the corresponding Capitalized character will move as far as possible automatically and repeatedly in that direction, until it is no longer possible.
Maps
A map defines the set of rooms and passages in which the game is played.
Valid maps
- The map is laid out on a grid.
- The grid is NR rows by NC columns; thus there are NR x NC gridpoints.
- The grid will fit in a
DISPLAY
message; thus, NR x NC + 10 < MaxBytes. - The grid has enough spots to accommodate
MaxPlayers
players andGoldMaxNumPiles
gold piles. - A room is a simple rectilinear polygon.
- A spot is a gridpoint in the interior of a room or along a passage.
- A room is defined by its boundaries.
- A horizontal boundary always meets a vertical boundary at a corner boundary.
- Thus, the boundaries of rooms are not spots, nor are gridpoints outside rooms and passages.
- A passage is one-spot wide and connects rooms to other rooms and passages. Passages are rectilinear but may not be straight, that is, they may have 90-degree turns.
- A passage interrupts a room’s vertical or horizontal boundary; a passage never meets a room at a corner.
-
The map is one connected component; thus, one can reach any spot from any other spot by moving in some sequence of up,down,left,right.
- Every gridpoint is one of these characters:
- ` ` solid rock - interstitial space outside rooms
-
a horizontal boundary|
a vertical boundary+
a corner boundary.
an empty spot#
a passage spot
Map files
A map file is a text file with exactly NC lines and in which every line has exactly NR characters.
Example map
The following is a 21x79 map. The dots represent empty spots; gold pieces and players may occupy these. The hashes represent passageways; players may occupy these. One room is non-convex. Some passageways bend, and some fork. Some rooms have multiple entrances. The room at upper-left shows a passage to nowhere.
+----------+
|..........| +---------+
|..........#### |.........| +-------+
|..........| +-----#---+ |.......|
+---------#+ # #######.......|
# # # +---#---+
# +-----------+ # +--------#+ #
####...........############## |.........| #
|...........| # |.........| #
+-----------+ ####.........| #
+----#----+ #
# +--------#--+
+---------------------------------+ # |...........|
|.................................| ######...........|
|.................................| # |...........|
|......+---------------+..........| # |...........|
|......| |..........########## +-----------+
|......| |..........|
|......| |..........|
|......| |..........|
+------+ +----------+
Visibility
Assume a player is at gridpoint (pr,pc). Another gridpoint (r,c) is “visible” from point (pr,pc) by reviewing the map. (Only the base map affects visibility; occupants do not affect visibility.)
- A map gridpoint that is blank (a space) is never visible.
- A map gridpoint on the same row or column as (pr,pc) is visible if all intervening map gridpoints are ‘spots’.
- Otherwise, we compute the mathematical line segment from (pr,pc) through (r,c). Considering each row in the range (pr…r) and each column in the range (pc…c), imagine the line segment passing between pairs of map gridpoints as it travels from (pr,pc) to (r,c); if both map gridpoints of any such pair are not ‘empty spots’, then they block our vision and we conclude (r,c) is not visible. Only if there are no such blocking pairs do we conclude that point (r,c) is visible.
- Note that gridpoint (r,c) itself may be an empty spot, passage spot, or boundary.
Examples, from the above map. At game start:
---------------+
..............|
............|
+..........|
|......*...#
|..........|
|....@.....|
|*.........|
+----------+
Consider some of the line segments that may be drawn toward the upper wall:
After moving up a few spots, the passageway became visible as we passed by, and (because it is now “known”) remains displayed. The wall to the left is not quite visible, but the gold and spots above have become visible:
+---------------------------------+
|...*.............................|
|.........*...*....*..............|
+....@.....|
|......*...##########
|..........|
|..........|
|*.........|
+----------+
Going up one and moving left to collect gold, the other part of the room comes into view, and the gold piles in the lower-right disappear from view:
+---------------------------------+
|...*.............................|
|.........@.......................|
|......+---------------+..........|
|... |..........##########
| |..........|
|..........|
|..........|
+----------+
Backtracking, and going down the passage to the corner, we can see straight up the passage and across the room beyond; the gold in the room behind us is no longer visible:
-
.
.
.
#
#
+---------------------------------+ #
|.................................| #
|.................................| #
|......+---------------+..........| #
|... |..........#########@
| |..........|
|..........|
|..........|
+----------+
Network protocol
The network protocol connects zero or more game clients (players and spectator) with one game server. The server maintains all game state; the clients display the game state to a user, and sends their keystrokes to the server; the server sends back updated displays.
The protocol runs over UDP/IP, that is, the user datagram protocol over the Internet Protocol. UDP/IP and TCP/IP form the core of the Internet. In either protocol, communication occurs between two endpoints; the address of an endpoint is a pair host IP address, port number. UDP carries datagrams from one port on one host to another port on another host (well, they could be the same host). A datagram can hold zero to 65,507 bytes.
Our game sends one message in each datagram.
Each message is an ASCII string (text).
Most messages are on one line and have no terminating newline.
Some messages are multiple lines long.
Newlines are shown explicitly as \n
in the specs below.
The first word of the message indicates the type of message.
(The first word begins at the start of the datagram and is terminated by a space, a newline, or the end of the message.)
Message types are in ALL CAPS.
When the server starts, it shall open a new endpoint and announce its port. When the client starts, it shall send a message to the hostname (or address) and port number where the server awaits. There are two types of client: players and spectators; let’s look at the messages each can send, then at the messages a server can send.
Player to server
When a player client starts, it shall send a message to the server:
PLAY realname
Everything after the PLAY
and one space is captured as the player’s “real name” (free text).
The player shall ensure realname
is less than MaxNameLength
characters long.
If there are already MaxPlayers
clients, the game server shall respond with
NO
Otherwise, the server shall respond with
OK L
where L
is this player’s letter in the set {A
, B
, … Z
}.
The server shall then immediately send GRID
and GOLD
messages as described below.
The client sends, at any time,
KEY k
where k is the single-character keystroke typed by the user.
When the player’s keystroke causes them to collect gold, the server shall inform all clients using a GOLD
message as described below.
When the player’s keystroke causes them to move to a new spot, the server shall inform all clients of a change in the game grid.
Spectator to server
When a spectator client starts, it shall send a message to the server:
SPECTATE
to join as a spectator.
If there is already a spectator, this spectator takes its place
(the server sends a QUIT
message to the prior spectator, then forgets it).
Thus, the server tracks only one spectator at a time.
The server shall respond with a GRID
message as described below.
The server shall then immediately send a GOLD
message as described below.
The spectator is not assigned a letter and is not represented on the map.
Subsequent DISPLAY
messages will include a complete view, as if this client knows all and sees all.
Server to clients
The server shall send immediately to new clients,
GRID nrows ncols
where nrows
and ncols
are positive integers describing the size of the grid.
This size will never change.
The server shall send immediately to new clients, and at any time to all clients,
DISPLAY\nstring
where the DISPLAY
is separated from the string
by a newline, and the string
is literally a multi-line textual representation of the grid as known/seen by this client.
(Indeed, if you were to just print the message string, it would be recognizable as the game map. That’s why DISPLAY ends with newline, and why the string contains an embedded newline after each row.)
More precisely, string
has nrows
lines, each of which has ncols
characters plus a newline.
Each client receives a different version, because (a) the spectator knows all and sees all, but is not itself represented on the map, (b) players’ displays show only the boundaries they know and the spots visible from their current position, and (c) the player’s own position is represented by @
.
Note it is entirely the server’s responsibility to produce these display strings.
The server shall send immediately to new clients, and at any time to all clients,
GOLD n p r
where n
, p
, and r
are positive integers,
to inform the player it has just collected n
gold nuggets, its purse now has p
gold nuggets, and there remain r
nuggets left to be found.
The value of n
may be zero.
The value of p
shall be initially zero, but will increase when gold is found.
The spectator shall always receive n=0
, p=0
.
The server sends, at any time,
QUIT
upon which the client should not send any more messages and shall exit.
The server sends, at any time,
GAMEOVER\nsummary
where summary
shall be a printable, multi-line string summarizing the purses of the game.
The summary shall include one line per player, with player Letter, purse (gold nugget count), and player real name, in tabular form.
After receiving a GAMEOVER
message the client shall print the summary and shall exit.
The server may send, in response to the client,
NO ...
to indicate it was unable to understand or handle the client’s prior message. The remainder of the line, if present, provides a short explanatory text. The client shall present this text to its user on the display’s status line.
Inspiration
This project was inspired by a classic game, Rogue.
Why do we use the H-J-K-L keys? Because the original ADM3a terminal had arrows on them.
Design spec
Components
Player interface
The player interface is defined within the requirements spec.
Inputs and outputs
The program takes in keystrokes from the user to represent commands. The program outputs ascii text to the terminal to show game information and status.
Functional decomposition into modules
-
server module
-
grid module
-
client module
Major data structures
Data structures for server:
- static struct game
- gold remaining
- struct grid
- set of player structs
- MinGoldPiles
- MaxGoldPiles
- struct grid
- NR (number of rows)
- NC (number of cols)
- 2D array of struct cell
- struct cell
- contents (empty or amount of gold)
- known bag/set (list of all the players to whom this cell is known)
- character associated (to be displayed)
- is walkable boolean
- struct player
- player name
- player tag
- gold obtained
- (row, col) position in grid
- connection for communicating with associated client
- Set of known vertices
- array of player structs
Data structures for client:
- char array to display map
Pseudo code for logic/algorithmic flow
Server module
Server validates input parameters goldtotal and maxplayers. Server reads in the board and intializes the game structure. The server listens to connections and accepts players into the game as they connect. The server receives commands from the players, updating their positions, if valid, and global gold count. The server sends the display of the board to the clients. When there is no longer any gold on the map, the server sends the final scores to each of the players. The server then closes the connections to the players.
Client module
Client reads and validates the path to logfile, hostname, port and name. Client connects to the hostname and port sending a message with the player’s name. Client listens for a char array from the server representing the display of the board. Client writes this display to the terminal window. Client listens for key inputs by the player, which it then validates and sends to the server if it is a valid command. If the player sends an EOF, the client exits. The client keeps updating until the server sends an end of game command. The client then listens for the scores of each players and displays them to the player. The client then exits.
Grid module
Defines grid structure with number of rows, number of columns, and 2D array of cells as parameters. Initializes grid cells at beginning of game. Updates grid cells during game runtime from given player moves.
Dataflow through modules
The server module reads in the map information from the file which it passes to the grid module for board creation. The server module passes its grid instance and player moves to the grid module for validation and the grid is updated on success. The server module asks the grid module if there is any gold remaining on its grid instance. The server module communicates with the client module by sending the board and final player scores to display. The client module sends the server information about a player when they join and the player’s moves.
Testing plan
Unit testing
The board, player, and server modules will have individual functions unittested by passing in sample input and validating that they perform as expected. Some test cases that we will attempt:
- passing invalid port to server
- passing the server an unreadable map file
- validate server in general by using logs
- making sure the grid will not overlap players and gold
- have player join server and ensure both he/she and server are consistently up-to-date
- prevent player from accidentally leaving map by traveling through walls
- spamming player keyboard input to see whether it will cause unexpected player movement
Integration testing
The player and server modules will be playtested by humans to ensure that behavior matches that which is expected. If behavior is unusual, we will use further unit testing to determine the source of this issue.
Some functionality that we will specifically test for
- two players trying to move to the same square
- running into walls to make sure collision/barriers works
- making sure that players and gold are only seen when currently visible by the player. Board squares are seen if they’re known.
Implementation spec
About
The nugget server reads the board, initializes the game, and then allows players to connect and play. The dynamics and control behind the game happen on the server side, while the inputs are given by and grid output is displayed to the client. All game functionality runs through the server.
Components
Server
static struct game
- Holds a
struct grid
for mapping purposes. - Holds
int num_players
which tracks current number of players.
- Holds a
struct grid
- Holds
int gold_remaining
which tracks remianing gold nuggets. - Contains mapping for the game struct with
int num_rows
andint num_cols
storing number of rows and columns in map respectively. - Holds a 2D array of containing
cell
structures. int MaxPlayers
is the maximum number of players in the gameplayer_t** players
Holds an array of non-spectatorplayer
structures.player *spectator*
holds a spectatorplayer
structure.
- Holds
struct cell
- Contains properties of a cell object within the grid mapping of the game.
char tag
, which holds the tag of the player occupying the cell. If no player is there, it holds'\0'
.int gold
, which is the amount of gold in that cell. 0 if no gold.- The default character to display when no character or gold is occcupying a cell is
default_char
. - Holds boolean indicating whether cell
is_walkable
. int row
is the row of th cell.int col
is the column.
struct player
(each held within an array)- Contains
char* player_name
which is the specific player’s name. - Holds
char player_tag
which is the label displayed on screen representing this player. - Holds
int gold_obtained
which indicates number of gold that has been obtained. - Contains
int row
which indicates the player’s row position in the grid. - Contains
int col
which indicates the player’s column position in the grid. addr_t* addr
is the address of connection to player. Themessage.c
module from the support library will be used to communicate with the client.- Holds
char *display
which is the string which the player needs to output for the grid. - Holds a 2D array
int **known
, whereknown[row][col] == 1
if the cell is known to the player, andknown[row][col] == 0
if it is not known. - Contains
bool player_quit
which tracks whether the player has disconnected.
- Contains
Psuedocode
Client module
Reads and validates the path to logfile, hostname, port, and name. Client connects to the hostname and port, sending a message with the player’s name. Client listens for a char array from the server representing the display of the board. Client writes this display to the terminal window if window is large. Client listens for key inputs by the player, sends it to the server, and then handles the server's responses. If the player sends an EOF, the client exits. The client keeps updating until the server sends an end-of-game command. The client then listens for the scores of each player and displays them to the user. Finally, the client exits.
main
- Attempts to initialize log file and message module.
- Validates usage, namely hostname, port, and playername (if given).
- If given playername and it exceeds maximum length, truncate automatically.
-
Attempts to set address to given hostname and port.
- Initializes ncurses display.
- Depending on whether client is player or spectator, sends appropriate join message to server.
- Begins message loop by passing
handle_stdin
andhandle_message
methods. - Shut down message module and closes log module
- Exits with status code 0 if message loop ends due to handler return true; otherwise, exit with status code 4 due to fatal error for which message loop could not keep looping.
bool handle_stdin(void *arg)
- If argument, ie. address to correspondent, is null, return true to break.
- If address is valid, upon key press, sends message containing key to server.
- Otherwise, if address is invalid, notifies user.
- Return false to exit.
bool handle_message(void *arg)
- If argument, ie. address to correspondent, is null, return true to break.
- If message begins with “OK “, return
handle_accept(message)
. - Otherwise, if message begins with “NO “, return
handle_reject(message)
. - Otherwise, if message begins with “GRID “, return
handle_grid(message)
. - Otherwise, if message begins with “DISPLAY”, return
handle_display(message)
. - Otherwise, if message begins with “GOLD “, return
handle_gold(message)
. - Otherwise, if message begins with “QUIT”, return
handle_quit(message)
. - Otherwise, if message begins with “GAMEOVER”, return
handle_gameover(message)
. - Otherwise, return false
- Helper Modules
void initialize_curses()
- Initializes ncurses screen, accepts keyboard input, sets colors of background and characters on screen.
- If client is spectator, hides cursor.
void update_statusline(const char *message)
- Stores current location of cursor.
- If client is player,
- Updates status line detailing status of the player, including player tag, nuggets claimed, and total nuggets unclaimed in map.
- If nuggets were recently picked up by player, displays amount on status line.
- Otherwise, if client is spectator, updates status line detailing nuggets unclaimed in map.
- If error message from server is received,
- Appends status line with error message.
- Returns cursor to original location.
void update_display
- For each character in map string,
- Displays character on screen in appropriate location.
- If character is client’s tag (@), saves its specific location on map.
- Moves cursor to hover over client’s tag.
- For each character in map string,
bool handle_accept(const char *message)
- Scans acceptance message for character indicating player’s tag on map.
- Return false.
bool handle_reject(const char *message)
- If rejection message is exactly “NO “
- Closes display.
- Notifies client that unable to join becase too many players are on server.
- Return true to break.
- Otherwise, uses
update_statusline
to notify client with rejection message. Returns false.
- If rejection message is exactly “NO “
bool handle_grid(const char *message)
- Extracts numbers of rows and columns in map from message.
- If terminal window fits is too small for said dimensions,
- Notifies user to resize window.
- Waits until ENTER is pressed and window has been resized appropriately.
- Begin listening for future window resizes (not allowed).
- Return false.
bool handle_display(const char *message)
- Stores map as string received as message.
- Uses
update_statusline
andupdate_display
to refresh screen. - Return false.
bool handle_gold(const char *message)
- Extracts and stores nuggets claimed, nuggets unclaimed, and nuggets recieved from message.
- Return false.
bool handle_quit()
- Closes display.
- Return true to break.
bool handle_gameover(const char *message)
- Closes display.
- Displays game over message, ie. summary of how much gold each player in game collected.
- Return true to break.
Server module
The server module, establishes a grid, and handles communication between the grid and players via a message module.
main
- Call
validate_params(const int argc, const char* argv[])
which validates that the map loaded is readable and the seed is valid (if any) - Once validated, create a new grid based on the input with a random int as seed if not specified by user
- If the grid is NULL return with non-zero exit status.
- If the number of columns * the number of rows + 10 is greater than or equal to
- Initialize the message module
- Loop through the message module with
handle_message
- Free the grid and all of the memory used by it once message module handling is done
- Call
bool handle_message(void *arg, const addr_t from, const char *message)
- If the message equals “PLAY”, pass that message onto
add_player
with address parameterfrom
- Otherwise If the message equals “KEY”, pass that message onto
process_keystroke
with address parameterfrom
- Otherwise If the message equals “SPECTATE”, pass that message onto
add_spectator
with addressfrom
- Otherwise, return false
- If the message equals “PLAY”, pass that message onto
void add_player(addr_t from, char* name)
- If the number of players is equal to maximum number players or the
from
address is already associated with a player stored in the array, send “NO” tofrom
address and break to reject join request - Malloc and create a new player with a tag and name corresponding to their connection, along with a
gold_obtained
value of 0 - malloc a display array for player, and
player_quit
property to false - send message to player client “OK
" to signify acceptance - put the player at a random empty room spot on the grid with
grid_add_player
- send gold information to the new player
- Increment the number of players by 1.
- If the number of players is equal to maximum number players or the
void process_keystroke(addr_t from, char keystroke)
- Check if there is currently a spectator and the message is from the spectator
- If so, quit the spectator, free its memory, and set it equal to null
- get the current player using its unique address
- for error handling purposes, validate that the player is not null
- initialize
gold_collected
to zero - map each key to the movement
- if
key
is:h
, move left usinggrid_move(grid, player, row, col)
j
, move down usinggrid_move(grid, player, row, col)
k
, move up usinggrid_move(grid, player, row, col)
l
, move right usinggrid_move(grid, player, row, col)
y
, move diagonally up and left using usinggrid_move(grid, player, row, col)
u
, move diagonally up and right using usinggrid_move(grid, player, row, col)
b
, move diagonally down and left usinggrid_move(grid, player, row, col)
n
, move diagonally down and right usinggrid_move(grid, player, row, col)
H
, move left usinggrid_move_to_end(grid, player,row, col)
J
, move down usinggrid_move_to_end(grid, player,row, col)
K
, move up usinggrid_move_to_end(grid, player,row, col)
L
, move right usinggrid_move_to_end(grid, player,row, col)
Y
, move diagonally up and left usinggrid_move_to_end(grid, player,row, col)
U
, move diagonally up and right usinggrid_move_to_end(grid, player,row, col)
B
, move diagonally down and left usinggrid_move_to_end(grid, player,row, col)
N
, move diagonally down and right usinggrid_move_to_end(grid, player,row, col)
Q
, remove player withplayer_remove
- otherwise, send “NO Invalid Key” as any other key is invalid
- if we collected gold during the move,
- send a gold message to the player who collected the gold
- send a message to the spectator with the updated gold count
- send a message to the rest of the players with an updated gold count
void add_spectator(addr_t from)
- If there is already a current spectator, boot that spectator from the game and free them
- malloc a new spectator and set it to the
from
address. - malloc the
display
andplayer_name
properties of the spectator - set its location and
gold_obtained
properties to zero - set its known property to NULL
- set the grid’s spectator to this one
- send grid and gold messages to spectator
- “GRID
" - “GOLD 0 0
"
- “GRID
void game_over()
- Send GAMEOVER summary to each player and spectator.
- intialize a
strsize
int andprint_size
int array - set first element in print_size to 9, as that is the size of “GAMEOVER”
- for
i
in the size of players- get current player
- add size of playername, gold obtained, and player tag + 3 for spaces and newline to
print_size[i+1]
- add size of
print_size[i+1]
tostrsize
- create string of adequate size
summary
- add “GAMEOVER” command to
summary
- initialize a pointer
idx
with the first element ofprint_size
- for each player summary print the summary to the string
- print to the next position after the previous print to
summary
- increment the start pointer after the message just printed; sum the holder
idx
with the next element ofprint_size
- print to the next position after the previous print to
- send the summary and quit command to each of the players still connected
- print the
summary
to the server screen - send the
summary
and “QUIT” message to the spectator if any
void send_board()
- sends the display to each of the players and spectator
- display the grid board with
grid_display_board
- for each player still connected, send the board they would see
- if the player’s
player_quit
property is false- send a message of display + “DISPLAY\n” to player address
- if the player’s
- If there’s a spectator send them the display
void player_remove()
- remove the player from the game board
- set the player’s
player_quit
property to true - send a quit message to the player client
int validate_params(const int argc, const char *argv[])
- check if correct number of arguments
- check if file exists
- check if the second argument is an integer
- return 0 if no error
player_t get_player_from_addr(const addr_t from)
- loop through all players until the player address property matches the address parameter and return that player, if no match return NULL
void free_player(player_t* player)
- free the player name and display
- if the
known
property is not null- free each individual row within
known
- free the struct itself
- free each individual row within
- free the player struct
void free_grid()
- if the spectator is not currently null, free it
- for all the players in the game free them
- call
grid_delete
which handles freeing for the grid
static bool str2int(const char string[], int *number)
- Convert a string to an integer, returning that integer.
- Returns true if successful, or false if any error.
- It is an error if there is any additional character beyond the integer.
- Assumes number is a valid pointer.
Grid module
Defines grid structure with number of rows, number of columns, and 2D array of cells as parameters. Initializes grid cells at beginning of game. Updates grid cells during game runtime using given player moves.
grid_t *grid_new(char* filename, int seed, int min_gold_piles, int max_gold_piles, int total_gold, int MaxPlayers)
- Allocate memory for grid
- Assume that server is doing this error checking
- hold the rows and cols of the map
- initialize gold remaining
- Allocate the array of cells
- Populate the cell data structures
- Along with
MaxPlayers
andspectator
- Allocate memory for the array of players
- check if there are enough spots to fit MaxPlayers and MaxGoldPiles
- if not, free the current grid and return NULL
- put gold in various piles in the grid with
grid_populate_gold
- close the map, and return the grid
static void generate_cells(cell_t **cells, FILE* map)
- set file pointer to beginning of map file
- initialize row and column
- loop through map
- clear the standard output and move it to console
- if the current character is a new line
- increment row, and reset col
- otherwise
- create a new cell at the location with the current character
- increment col
- reset the file pointer back to beginning of map
static cell_t cell_new(char default_char, int row, int col)
- initialize cell object with the default character
- if the default character is a # or a period
- set the
is_walkable
property of the cell to true
- set the
- otherwise, set the property to false
- update the cell row and col to the parameters
- initialize the gold count to 0
- initialize tag to null
- return the cell
static int calculate_rows(FILE* fp)
- If the file is null return 0
- reset file pointer back to beginning of file
- initialize a count for rows and character holder to iterate the file
- loop through the file
- if a newline, increment the row count
- reset file pointer back to beginning of file
- return the row count
static int calculate_cols(FILE* fp)
- if the file is null return 0
- reset file pointer back to beginning of file
- initialize a count for columns and character holder to iterate the file
- loop through the file
- if not a new line, increment the column count
- reset file pointer back to beginning of file
- return column count
static int calculate_dots(grid_t *grid)
- initialize a count for dots
- loop through the number of rows
- loop through the number of cols
- if the current cell is a dot, increment dot count
- loop through the number of cols
- return count for dots
static cell_t* get_cell(grid_t *grid, int dot_number)
- initialize a count for dots
- loop through the number of rows
- loop through the number of cols
- if the cell is a dot and the dot number refers to that dot, return it’s cell address
- increment dot count if cell is a dot
- loop through the number of cols
- return null of no dots were found
static void grid_populate_gold(grid_t* grid, int min_gold_piles, int max_gold_piles, int total_gold)
- set holder for number of dots in grid
- set the number of gold piles in grid to random number based on max/min params
- initialize int array for all the gold piles
- set every element in int array to 1
- set remaining gold to the difference of total amount of gold and the number of gold piles
- randomly select and increment gold piles by the amount of remaining gold
- for the number of gold piles
- select a random cell in the grid
- if the gold property in that cell is equal to 0
- set the gold property in that cell to the count in a gold pile
- move to another gold pile
void grid_remove_player(grid_t* grid, player_t* player)
- set the player tag to null
int grid_move(grid_t *grid, player_t *player, int row, int col)
- if the player would not be in bounds after move, don’t make the move and return 0
- hold the address of the cell the player would potentially move to
- check if the move_to cell is not walkable. If not, exit by returning 0
- hold the current cell the player is in
- update the player location to the move location
- save the gold amount of the move to cell, and then set the gold amount at that cell to 0
- if the player would potentially move to empty square, set the tag at the move_to cell to the player_tag and remove the player tag at the current cell
- otherwise, move to the cell that the other player is occupying, and swap locations and player tags
- update remaining gold count in grid
- update gold obtained by player
- return the player’s gold amount
int grid_move_to_end(grid_t *grid, player_t *player, int row, int col)
- hold current row and column of player
- increment gold count by the movement of player with
grid_move
- loop through rows and cols in grid
- if the current cell is visible
- set the cell to known for the player
- if the current cell is visible
- return gold count if at player location
- return 0 if at end of method
int int_len(int i)
- if the int is zero, return 1
- return the largest integer value less than or equal to the float (log of the positive value of the int param)
static bool grid_isVisible(grid_t *grid, int x1, int y1, int x2, int y2 )
- if the cell is empty return false
- if in the same column
- if starting row is above current row
- move down vertically and check cells until at current cell
- if the cell is not walkable, return false
- move down vertically and check cells until at current cell
- return true if all cells in between are walkable
- if starting row is above current row
- otherwise
- move up vertically and check for cells until at current cell
- if the cell is not walkable, return false
- return true if all cells in between are walkable
- move up vertically and check for cells until at current cell
- if in the same row
- if starting col is to the left of current col
- move to the right until at current cell
- if the cell is not walkable, return false
- move to the right until at current cell
- return true if none of the cells are not walkable
- if starting col is to the left of current col
- otherwise
- move to the left until at the current cell
- if the cell is not walkable, return false
- return true if all cells in between are walkable
- move to the left until at the current cell
- calculate the slope between the starting location and current location
- if starting col is to the left of current col
- move to the right until at current cell
- calculate row based on starting row, col, and slope
- check if there is a vertical wall at the current column and calculated row
- if so, return false
- check if there is a vertical wall at the current column and calculated row
- calculate row based on starting row, col, and slope
- move to the right until at current cell
- otherwise
- move to the right until at current cell
- calculate row based on starting row, col, and slope
- check if there is a vertical wall at the current column and calculated row
- if so, return false
- move to the right until at current cell
- if starting row is above the current col
- move down vertically and check cells until at current cell
- calculate col based on starting col, row, and slope
- check if there is a horizontal wall at the current row and calculated column
- if so, return false
- move down vertically and check cells until at current cell
- otherwise
- move up vertically and check cells until at current cell
- calculate col based on starting col, row, and slope
- check if there is a horizontal wall at the current row and calculated column
- if so, return false
- move up vertically and check cells until at current cell
- return true if all cells in between do not conflict with walls
static bool is_vertical_wall(grid_t *grid, int x, double y)
- if the largest integer value
j
is equal to the float value of the row * return true, if the spot is a point in the grid - otherwise, the largest integer value is less than the float value of the row
* if
j
is equal to the last col * return true, if the spot is a point in the grid - otherwise
- return true, if the spot and the spot right below it are grid points
- return false, if no grid point in grid found
- if the largest integer value
static bool is_horizontal_wall(grid_t *grid, double x, int y)
- if the largest integer value
j
is equal to the float value of the col * return true, if the spot is a point in the grid - otherwise, the largest integer value is less than the float value of the col
* if
j
is equal to the last row * return true, if the spot is a point in the grid - otherwise
- return true, if the spot and the spot to the right of it are points in the grid
- return false, if no grid point in grid found
- if the largest integer value
void grid_display_board(grid_t *grid)
- initialize player holder and spectator validator boolean
- loop through max amount of players in grid
- if at max players
- set that player to spectator and set spectator validator to true
- otherwise hold value of current player
- if that player is null, continue iteration
- hold the location of player and initialize display pointer
- loop through grid
- for each current cell
- if that cell is equal to the player’s location and is not a spectator
- set that cell to known
- use “@” sign to represent the current player
- otherwise if a spectator or that cell is visible in grid for player
- if the cell is just visible, set it to known for player
- if that cell has gold, use “*” to represent it
- if the cell is not empty
- just display the tag at the cell
- if the cell is empty, use default character at cell
- otherwise if the cell is already known to player
- just display the default character at that cell
- if just a spectator, display empty space
- increment display pointer
- if at max players
- display a newline and increment display pointer
void grid_delete(grid_t* grid)
- Free each row of the cells 2D array
- Free the
cells
andplayers
property - Free the grid
void grid_add_player(grid_t* grid, player_t* player)
- get a random empty cell from grid
- set the tag in that cell to the player’s tag and set the player’s location to that cell’s location
static int calculate_empty_spots(grid_t *grid)
- loop through grid
- if default char at cell is ‘.’ and it has no tag or gold
- increment number of empty spots
- if default char at cell is ‘.’ and it has no tag or gold
- return number of empty spots
- loop through grid
static cell_t* get_empty_cell(grid_t *grid, int dot_number)
- loop through grid
- if default char at cell is ‘.’ and it has no tag or gold
- if it’s dot number matches the current empty spot count
- return that spot
- increment empty spot count
- if it’s dot number matches the current empty spot count
- if default char at cell is ‘.’ and it has no tag or gold
- return NULL if not found
- loop through grid
static bool grid_in_bounds(grid_t *grid, int row, int col)
- return whether the location is not in bounds
Security, error handling, and recovery
- return whether the location is not in bounds
Arguments are validated for the server and client. Specifically, the server validates that there is a correct number of arguments and that there is a readable file for the map filename passed. It also validates that the seed is an integer if provided one. The client reads and validates the path to logfile, hostname, port and name. The client sends all inputs from the player to the server–validation is done on the server side. We will conduct unit testing on server client interaction, the presentation of the game in the client module, the game control functionality from client to server, and the board display on the client end when players exit, the game ends, or players move as they obtain gold. Once all of those tests are passed, we can test the game as a whole.
Materials
Maps
The maps
directory contains maps for the Nuggets game.
to build, run make
to clean run make clean
Our Maps are the following:
-
customMap.txt
: our custom foobarbaz map -
main.txt
: an interesting map for playing and testing. -
fewspots.txt
: a map with too-few empty spots to support a game with 26 players and many gold piles. -
small.txt
: a simple, small map; also too-few empty spots. -
hole.txt
: a map similar tomain.txt
, but with a hole in the room
Support Library
This library contains two modules useful in support of the CS50 final project.
‘log’ module
This module provides a simple way to log information to an output file.
See log.h
for interface details, and message.c
for some usage examples.
‘message’ module
Provides a message-passing abstraction among Internet hosts.
See message.h
for interface details, and the UNIT_TEST
at the bottom of message.c
for a usage example.
Messages are sent via UDP and are thus limited to UDP packet size, may be lost, and may be reordered, but require no connection setup or teardown. Within the Dartmouth campus network it is unlikely for messages to be lost or reordered; we will use this module as if neither will happen.
compiling
To compile,
make support.a
To clean,
make clean
using
In a typical use, assume this library is a subdirectory named support
, within a directory where some main program is located.
The Makefile of that main directory might then include content like this:
S = support
CFLAGS = ... -I$S
LLIBS = $S/support.a
...
program.o: ... $S/message.h $S/log.h
...
$S/support.a:
make -C $S support.a
clean:
make -C $S clean
...
This approach allows the main program to be built (or cleaned) while automatically building (cleaning) the support library as needed.
testing
The ‘message’ module has a built-in unit test, enabling it to be compiled stand-alone for testing.
See the Makefile
for the compilation.
To compile,
make messagetest
To run, you need two windows. In the first window,
./messagetest 2>first.log
In the second window,
./messagetest 2>second.log localhost 12345
where 12345
is the port number printed by the first program.
Then you should be able to type a line in either window and, after pressing Return, see that message printed on the other.
The above example assumes both windows are on the same computer, which is known to itself as localhost
.
Each window could be logged into a different computer, in which case the second above should provide the hostname or IP address of the first.
If the first is on the CS server known as “flume”, the second would run
./messagetest 2>second.log flume.cs.dartmouth.edu 12345
On a private network, such as inside a home, you might only have an IP address of the first:
./messagetest 2>second.log 10.0.1.13 12345
In all examples above notice we redirect the stderr (file number 2) to a log file, and we use different files for each instance… otherwise, if they are sharing a directory (as they would, on localhost), the log entries will overwrite each other.