This is a website I created where users can play single-player and multi-player games. Users can become friends with each other and play private games with their friends, or play against random opponents. They can also view their game history.
The website is hosted on Render at https://gamingwebsite.onrender.com/.
I built this project using MongoDB, Express, React, Node, TypeScript, and Socket.IO. The back end is a server with an Express Rest API and a socket server. The front end is a React app. The front end is built into static files, which the back end then serves on the front end URL paths (e.g. /
, /login
, /past-games
), and the front end uses React Router to route pages. The back end also serves its Rest API on paths within /api
.
- Configure environment variables in a
.env
file within the project's root directory. See.env.example
for the necessary variables and example values. - Move to front end directory:
cd client
- Install front end dependencies:
npm install
- Build:
npm run build
- Move back to project's root directory:
cd ..
- Install back end dependencies:
npm install
- Start the server:
npm run start
The db
directory contains code for interfacing with my MongoDB database using Mongoose. The db/models
directory contains definitions for my database models. For each model, I define an interface (for Typescript typing) and a schema, then a model.
In db/models/document.ts
, the IDocument
interface, which the model interfaces extend, contains the _id
property since all MongoDB documents contain the _id
field. Wherever this documentation says "ID", it refers to the _id
field of a document.
In db/models/friend.ts
, there are models for a friendship and a friend request. A friendship stores the IDs of the users that are friends, while a friend request stores which user is requesting the friendship and which user they are requesting to be their friend.
In db/models/gameType.ts
, I define the game type model. I used "game type" to mean a game that can be played, such as Tic-Tac-Toe, Dots and Boxes, or Tetris. The game type document would contain data about how that game works. By contrast, I use "game" to mean a specific game played by 2 users of a certain type, so a game document would store information about what users are playing and the game result. The game type model includes fields for the name of the game type, the namespace used when playing that game with sockets and for routing to that game type, a description of the game type, and how many players the game type is for.
In db/models/game.ts
, I define the game model. This model contains fields for the type of the game, what user(s) is/are playing it and their usernames, whether the game is complete, who won (if the game is multiplayer) and the score (if the game is singleplayer).
In db/models/user.ts
, I define the user model. This model simply stores a user's username and password (in reality, a hash of their password). This model is used to store users and data about their games by referring to a user by their ID.
server/index.ts
is the main file that runs the server. The server is an Express app that serves API routes I've programmed, but also serves the client code on the website. The main components of the server are the API routes (in the routes
directory) and the socket event handlers, in the gameHandlers
directory.
On the auth
route, there are endpoints to authenticate users. I used Passport and Bcrypt for user authentication, using local authentication. I used session authentication, so that once a user is authenticated, their future requests are also authenticated until they log out.
-
POST
requests toauth/register
register a new user. The user must provide a username and password, and confirm their password. -
POST
requests toauth/login
log in a user. The user must provide their username and password. -
POST
requests toauth/logout
log out a user. -
GET
requests toauth/user
return the user's username and (hashed) password. This endpoint, as well as several others such as for friends and games, use theensureAuthenticated
middleware, which checks that a user is authenticated before allowing them to access the route. -
GET
requests toauth/invalid
signify that a user is not authenticated. Passport redirects requests to this endpoint if the authentication credentials are invalid. -
DELETE
requests toauth/delete
will delete a user's account.
The friends
route contains endpoints to read and change a user's friends and friend requests.
-
GET
requests tofriends/all
will return all a user's friends. -
GET
requests tofriends/search
search for a user by a search string, which is a query parameter. This route not only retrieves users whose usernames match the query, but also checks whether each matching user is already a friend of the requesting user, or whether a friend request has been made between them. -
POST
requests tofriends/request
create a new friend request to another user, whose ID is stored in the request's body asuserId
. -
GET
requests tofriends/request/incoming
will return all incoming friend requests to a user (i.e. all users who have sent friend requests to this user). -
GET
requests tofriends/request/outgoing
will return all outgoing friend requests a user has made to other users. -
POST
requests tofriends/accept
will accept a friend request from another user, whose ID is stored in theuserID
field of the request's body. -
POST
requests tofriends/decline
will decline a friend request from another user, whose ID is stored in theuserID
field of the request's body. -
POST
requests tofriends/cancel
will cancel a friend request the user has sent to another user, whose ID is stored in theuserID
field of the request's body. -
POST
requests tofriends/remove
will remove a certain friend of the user, with the friend's ID stored in theuserID
field of the request's body.
The game-type
route contains endpoints to get data about game types.
-
GET
requests togame-type/all
will return all existing game types. -
GET
requests togame-type/requests
will return all game requests other users have made to this user. Game requests are how users ask their friends to play private games. -
GET
requests togame-type/:gameType
will return data about a certain game type, with the parameter being the namespace of that game type.
The game
route contains endpoints to get view a user's past games and create new games.
-
GET
requests togame/all
will return all games a user has played. -
GET
requests togame/dots-and-boxes/colors
will return all available colors for playing Dots and Boxes. -
GET
requests togame/:gameTypeId
will return all games a user has played of a certain game type by specifying the ID of that game type. -
POST
requests togame/add
will create a new game that the user has finished. This endpoint is only used for single-player games, because multi-player games automatically save to the database once they are finished as part of the server's socket event handling, while single-player games take place entirely in the client and are only saved to the database once they are finished.
The server/gameHandlers
directory contains code for managing sockets for multiplayer games. In server/index.ts
, globalWaitingRandomUsers
stores users that are waiting to play games against randomOpponents, globalWaitingPrivateUsers
stores users that are waiting to play games against friends they've challenged, and globalInProgressGames
stores games that are in progress. Each of these variables are maps, with keys being socket namespaces and values being the users/games that are of the game of that namespace. Each game type's handlers keep track of users and games by reading and writing to these variables.
server/gameHandlers/types
contains type definitions for socket events between the client and server, and for the parameters that are passed to game handlers.
server/gameHandlers/handleRooms
contains a handler for users to wait for opponents and join games. Each game type handler calls this handler since the joining process is the same for each game type; it's just the playing process that varies. Clients emit joinRandomGame
events to join a game against a random opponent. The handler checks if any other users are waiting for random opponents, and if they are, it starts a game between the users; if not, it stores the requesting client as a waiting user. Clients emit createPrivateGame
to create a private game, and the challenged opponent must emit joinPrivateGame
to join the game and begin playing. The server tells clients that their game is starting by emitting a joinedGame
event. When a user starts playing a game or disconnects, they are removed from the lists of waiting users.
server/gameHandlers/dotsAndBoxes
contains the handler for playing Dots and Boxes. The code handles the game logic, and instantiates a game when users join it. Clients can emit chooseColor
to choose their colors (the game stores each user's color, the color used to shade their claimed boxes). They can emit move
to make a move in the game; when a user makes a move, the server emits gameUpdate
with the updated game to tell the clients to re-render the game. The move handler also checks whether the game is over, in which case it updates the game in the database to be complete and marks who the winner is.
server/gameHandlers/ticTacToe.ts
contains the handler for playing Tic-Tac-Toe. The events are identical to the Dots and Boxes handler, except a chooseColor
event is not needed. The only real difference is that the logic of the games is different.
The tests
directory contains unit tests of my functions and API routes. I used MongoUnit to create a separate database for testing.
You can run the tests with npm run test
.