The individual and team project for this class are designed to mirror the
experiences of a software engineer joining a new development team: you will be
“onboarded” to our codebase, make several individual contributions, and then
form a team to propose, develop and implement new features. The codebase that
we'll be developing is GameNite, a website that answers the question "what if
Twitch, but for correspondence chess?" hello
You will get an opportunity to work with the starter code which provides basic skeleton for the app and then additional features will be proposed and implemented by you! All implementation will take place in the TypeScript programming language, using React for the user interface.
Run npm install in the root directory to install all dependencies for the
client, server, and shared folders.
While you're working on the application, it's useful to run it in "development mode" locally. Development mode watches files for changes and updates the application when changes happen.
To run gamenite locally in development mode, do one of the following:
- Run
npm run devin the top-level directory - Open two terminal windows
- In the first, navigate to the
serverdirectory and runnpm run dev - In the second, navigate to the
clientdirectory and also runnpm run dev
- In the first, navigate to the
The second terminal window, the one in the client directory, shows a URL
that you should go to to preview the application, probably
http://localhost:4530/. You can use the default username/password
combinations user0/pwd0000, user1/pwd1111, user2/pwd2222, and user3/pwd3333 to
log in.
Checks can be run on every part of the application at once by running the following commands from the repository root:
npm run check- Checks all three projects with TypeScriptnpm run lint- Checks all three projects with ESLintnpm run test- Runs Vitest tests on all three projects and end-to-end Playwright tests
If you want to deploy the application or build it in production mode, running
npm run build -w=client in the root of the repository will create the
production build of the client. Then, the server can be started in production
mode by running npm start -w=server and accessed by going to
http://localhost:8000/.
client: Contains the frontend application code, responsible for the user interface and interacting with the backend. This directory includes all React components and related assets.server: Contains the backend application code, handling the logic, APIs, and database interactions. It serves requests from the client and processes data accordingly.shared: Contains all shared type definitions that are used by both the client and server. This helps maintain consistency and reduces duplication of code between the two folders.
The server provides the following REST endpoints: requests are routed to these
endpoints in server/src/app.ts.
| Endpoint | Method | Description |
|---|---|---|
/create |
POST | Create new game |
/list |
GET | List all games |
/:id |
GET | Get information about a specific game |
| Endpoint | Method | Description |
|---|---|---|
/create |
POST | Create new forum post |
/list |
GET | List all forum posts |
/:id |
GET | Get information about a form post |
/:id/comment |
POST | Add a comment to a forum post |
| Endpoint | Method | Description |
|---|---|---|
/list |
POST | Get details of a list of users |
/login |
POST | Validate username/password entry |
/signup |
POST | Create a new user |
/:username |
POST | Update user's displayname or password |
/:username |
GET | Get information about a user |
| Method | Route | Purpose |
|---|---|---|
| POST | /request |
Send a friend request ({ auth, payload: {toUsername} }) |
| POST | /respond |
Accept/reject a request ({ auth, payload: {requestId, action} }) |
| POST | /remove |
Remove a friend ({ auth, payload: {friendUsername} }) |
| GET | /list/:username |
Get user's accepted friends list |
| GET | /requests/:username |
Get pending incoming/outgoing requests |
| Method | Route | Purpose |
|---|---|---|
| GET | /list/:username |
Get all DM conversations for a user |
| GET | /:id |
Get a specific DM conversation (with messages) |
/list/:username needs "x-password: [insert user password]" as a header in
the request to authenticate themselves /:id needs both "x-username" and
"x-password"
| Method | Route | Purpose |
|---|---|---|
| POST | /block |
Block a user ({ auth, payload: {targetUsername} }) |
| POST | /unblock |
Unblock a user ({ auth, payload: {targetUsername} }) |
| GET | /list/:username |
Get user's blocked list |
/list/:username needs "x-password: [insert user password]" as a header in
the request to authenticate themselves
- Blocking a user should cascade: remove existing friendship, cancel pending friend requests, and remove DMs between both users.
- Blocking is one-directional (A blocks B does not mean B blocks A).
sendFriendRequestand DM messaging should check the block list and reject if either user has blocked the other.
| Method | Route | Description |
|---|---|---|
| POST | /create |
Create a new lobby (public/private toggle, game type, settings) |
| GET | /list |
List all public lobbies that haven't started yet |
| POST | /invited |
Get all lobbies where the authenticated user has a pending invite |
| GET | /:id |
Get information about a specific lobby |
| POST | /:id/invite |
Invite a player to the lobby (host only) |
| POST | /:id/join |
Join a public lobby or accept a pending invite |
| POST | /join-by-code |
Join a lobby using a unique lobby code |
| POST | /:id/leave |
Leave a lobby before the game starts |
| POST | /:id/decline |
Decline a pending lobby invitation |
| POST | /:id/remove |
Remove a player from the lobby (host only) |
| POST | /:id/settings |
Update lobby settings — mode, difficulty, timer (host only) |
| POST | /:id/start |
Start the game from the lobby (host only, min players required) |
Most lobby endpoints require authentication via
{ auth: { username, password } } in the request body. The lobby creator is
the host and has exclusive access to invite, remove, settings, and start
actions.
| Method | Route | Description |
|---|---|---|
| GET | /player/:username |
Get aggregated stats (wins, losses, win rate, badges) |
| GET | /history/:username |
Get paginated match history with optional filters |
| GET | /leaderboard |
Get global leaderboard ranked by win rate |
| POST | /leaderboard-opt-out |
Toggle leaderboard opt-out for the authenticated user |
| GET | /leaderboard-opt-out/:username |
Check if a player has opted out of the leaderboard |
Query parameters for /history/:username:
page(default: 1),limit(default: 10, max: 50)gameType— filter by game type (nim/guess)opponent— filter by opponent usernamedateFrom,dateTo— ISO date strings for date range filtering
Query parameters for /leaderboard:
page(default: 1),limit(default: 10, max: 50)period— time filter:week,month, orall(default)
The Socket.io API for event-driven communication between clients and the
server is detailed in shared/src/socket.types.ts.
This web application stores information about users, forum posts, games, lobbies, friends, direct messages, and player stats. The structure of the data can be described by this diagram:
erDiagram
Auth {
string username "unique key"
RecordId userId "unique"
string password ""
}
User {
RecordId userId "generated key"
string username "unique"
string display ""
DateISO createdAt ""
Record friends "userId set"
Record friendInReqs "userId to requestId"
Record friendOutReqs "userId to requestId"
Record directChats "userId to chatId"
Record blocked "userId set"
}
User ||--|| Auth: "Auth.userId"
Thread {
RecordId threadId "generated key"
string title ""
string text ""
DateISO createdAt ""
RecordId createdBy ""
RecordId[] comments ""
}
Thread }o--|| User: "Thread.createdBy"
Thread ||--o{ Comment: "Thread.comments"
Comment {
RecordId commentId "generated key"
string text ""
RecordId createdBy ""
DateISO createdAt ""
DateISO editedAt "optional"
}
Comment }o--|| User: "Comment.createdBy"
Game {
RecordId gameId "generated key"
GameKey type "nim or guess"
unknown state "optional"
boolean done ""
boolean isPrivate ""
RecordId chat ""
RecordId[] players ""
number timerDurationSeconds "optional"
DateISO timerEndsAt "optional"
DateISO createdAt ""
RecordId createdBy ""
}
Game ||--|| Chat: "Game.chat"
Game }o--|| User: "Game.createdBy"
Game }o--o{ User: "Game.players"
GameHistory {
RecordId gameHistoryId "generated key"
GameKey type "nim or guess"
unknown state "final state"
RecordId[] players ""
DateISO endedAt ""
boolean endedByTimer "optional"
}
GameHistory }o--o{ User: "GameHistory.players"
Chat {
RecordId chatId "generated key"
RecordId[] messages ""
MoveLogEntry[] moveLog ""
DateISO createdAt ""
}
Chat ||--o{ Message: "Chat.messages"
Message {
RecordId messageId "generated key"
string text ""
RecordId createdBy ""
DateISO createdAt ""
}
Message }o--|| User: "Message.createdBy"
Lobby {
RecordId lobbyId "generated key"
GameKey type "nim or guess"
boolean isPrivate ""
string code "unique join code"
RecordId createdBy ""
LobbyPlayer[] players ""
LobbySettings settings ""
RecordId chatId ""
RecordId startedGameId "optional"
DateISO createdAt ""
}
Lobby }o--|| User: "Lobby.createdBy"
Lobby ||--|| Chat: "Lobby.chatId"
Lobby |o--o| Game: "Lobby.startedGameId"
FriendRequest {
RecordId id "generated key"
RecordId fromUser ""
RecordId toUser ""
string status "pending, accepted, or rejected"
DateISO createdAt ""
DateISO respondedAt "optional"
}
FriendRequest }o--|| User: "FriendRequest.fromUser"
FriendRequest }o--|| User: "FriendRequest.toUser"
DirectChat {
RecordId id "generated key"
RecordId[] participants "two user IDs"
RecordId[] messages ""
DateISO createdAt ""
Record lastReadAt "userId to DateISO"
}
DirectChat }o--o{ User: "DirectChat.participants"
DirectChat ||--o{ Message: "DirectChat.messages"
PlayerStats {
RecordId playerStatsId "generated key"
RecordId userId ""
string username ""
number wins ""
number losses ""
number gamesPlayed ""
number winRate "0 to 1"
string[] badges ""
number currentWinStreak ""
number bestWinStreak ""
boolean leaderboardOptOut ""
DateISO lastPlayedAt ""
}
PlayerStats ||--|| User: "PlayerStats.userId"
To create a new game example, you need to take the following steps:
- In a new file
shared/src/games/example.types.ts, define the game's state: what gets stored on the server as anExampleState, what gets sent to players as anExampleView, and what players send as moves as anExampleMove. - In the existing file
shared/src/game.types.ts:- The
ExampleViewneeds to be imported fromshared/src/games/example.types.ts. - Everything in
shared/src/games/example.tsfile needs be exported (so it can be used in other files that importgame.types.ts). - The GameKey
exampleneeds to be added tozGameKeyand{ type: 'example'; view: ExampleView }needs to be added toTaggedGameView.
- The
- In a new file
server/src/games/example.ts, the rules of the game, which are evaluated in the backend server, need to be added. This file should exportexampleLogicandexampleGameService. - In the existing file
server/src/services/game.service.ts, the mapping fromexampletoexampleGameServiceneeds to be added togameServices. - In a new file
client/src/games/ExampleGame.tsx, a React componentExampleGameneeds to be defined, which takesGameProps<ExampleView, ExampleMove>as its props. - In the existing file
client/src/games/GameDispatch.tsx, a case statement for'example'needs to be added. - In the existing file
client/src/util/consts.ts, a mapping fromexampleto the user-facing name for the game needs to be added.