This is the server side of the Codewords game. It manages game state and handles turn logic, however it does not have a built-in front-end. You will also need the Codewords UI available within this organization.
- Ruby 2.6.3
- Rails 5.2.3
- This server
Need to set up a Ruby environment? MacOS instructions within.
Note: Depending on your existing setup, you may be able to skip one or more of the steps below. If you're not sure whether you have something installed or not, go ahead and run the respective command, and the command-line tools we're using will let you know that you're good to go. First, we'll install a Ruby environment manager. rbenv gives you very straightforward tools to manage which version of Ruby you use for a particular project.
brew install rbenv
rbenv init
- This will instruct you to add content to your
~/.bash_profile
. Do so.
- This will instruct you to add content to your
source ~/.bash_profile
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash
- The above command should report green across the board. If you get anything else, you'll need to search for assistance on the internet.
Now that rbenv is installed, we need to install Ruby. This project uses Ruby 2.6.3, so we'll start there.
rbenv install 2.6.3
- If you have used
rbenv
to install Ruby before, you will need to set the correct Ruby version for this project. - Once you have cloned down this repository,
cd
into the project root. - Run
rbenv local 2.6.3
. (This step can be skipped ifrbenv global
reports you're already using2.6.3
) - With Ruby, the package manager we use is called Bundler. Let's be certain you have the right version.
- Run
gem install bundler -v 2.0.2
- If you have used
Next, we'll install the PostgreSQL database.
brew install postgresql
Finally, we'll install Redis, which is used to manage our Websockets broadcasts
brew install redis
- redis runs as a service in the background. It uses very little resources, so we'll go ahead and start it now.
brew services start redis
- It will need to be active anytime we're running the server, however you can stop it whenever you don't need it with
brew services stop redis
At this point, we have everything we need for our Rails environment. If you wish, you can also globally install Rails with gem install rails -v 5.2.3
however Bundler will ensure you have what you need for this project.
- Clone this repository and run
bundle install
. - Prepare database with
rails db:create
- Migrate with
rails db:migrate
- Start the server with
rails s
From Server | From Client |
---|---|
Player Joined | |
Game Started | |
Hint Provided | Hint Sent |
Board Update | Guess Sent |
Game Over | |
Illegal Action |
Request that the server create a new Game instance and attach the requesting user as a Player. Because we are not managing persistent users at this time, this endpoint also creates a User record for the requesting user.
POST /api/v1/games
{
"name": "Archer"
}
key | description |
---|---|
name |
String: The username that the requesting user would like to use during the game |
HTTP/1.1 201 Created
{
"invite_code": "L6J5suAqTsKUAjEXm5swoEUN",
"id": 0,
"name": "Archer",
"token": "uuxHQc7djqQuzWgJxAp5r1vy"
}
key | description |
---|---|
invite_code |
String: A code which can be shared with other players. They will use this code to join the game. |
id |
Integer: The unique id for the player. |
name |
String: A confirmation that the requested name was indeed assigned to the player. |
token |
String: A token unique to the current player, which can be used to identify them in future requests to the server. |
Failed Responses
This error occurs if the body of the request does not contain a name
, or if the name
is empty.
HTTP/1.1 401 Unauthorized
{
"error": "You must provide a username"
}
Request that the server create a new Player instance for the requesting user and attach it to the Game denoted by the invite code.
POST /api/v1/games/:invite_code/players
{
"name": "Lana"
}
key | description |
---|---|
:invite_code |
String: (Within URI) The invite code provided by the person inviting the requesting user to their existing game. |
name |
String: The username that the requesting user would like to use during the game |
HTTP/1.1 200 OK
{
"id": 0,
"name": "Lana",
"token": "uuxHQc7djqQuzWgJxAp5r1vy"
}
key | description |
---|---|
id |
Integer: The unique id for the player. |
name |
String: A confirmation that the requested name was indeed assigned to the player. |
token |
String: A token unique to the current player, which can be used to identify them in future requests to the server. |
Failed Responses
This error occurs if the body of the request does not contain a name
, or if the name
is empty.
HTTP/1.1 401 Unauthorized
{
"error": "You must provide a username"
}
This error occurs if the name
requested by the user is already in use by another Player in this game.
HTTP/1.1 401 Unauthorized
{
"error": "That username is already taken"
}
This error occurs if the invite code provided by the user does not match a current game.
HTTP/1.1 401 Unauthorized
{
"error": "That invite code is invalid"
}
This error occurs if the invite code provided for the user matches a game that does not have room for additional players.
HTTP/1.1 401 Unauthorized
{
"error": "That game is already full"
}
Request the Intel data for a game, allowing the player to see which cards belong to which team, which are bystanders, and which is the assassin. This endpoint requires a valid token belonging to a player with the intel role.
GET /api/v1/intel?token=<player_token>
key | description |
---|---|
token |
String: A valid token belonging to a Player with the Intel role. |
HTTP/1.1 200 OK
{
"cards": [
{
"id": 0,
"word": "<card word>",
"type": "red"
}, ...
]
}
key | description |
---|---|
cards |
Array: An ordered collection of card objects which are part of the game. These cards go onto the board left-to-right, top-to-bottom. |
-->card.id |
Integer: The unique identifier for the card. |
-->card.word |
String: The word for the card. |
-->card.type |
String: The type of card to render in the UI: "red", "blue", "bystander", or "assassin". |
Failed Responses
This error occurs if the querystring of the request does not contain a token
, or if the token
is empty.
HTTP/1.1 401 Unauthorized
{
"error": "You must provide your access token"
}
This error occurs if the provided token does not match a player.
HTTP/1.1 401 Unauthorized
{
"error": "Unable to find a user with that token"
}
This error occurs if the token matches a player who does not have the Intel role for the game.
HTTP/1.1 401 Unauthorized
{
"error": "You are not authorized for this game's secret intel"
}
This message is broadcast to the game channel whenever a player joins the game. It contains the name and ID of the player who joined, as well as a roster of all players currently in the game.
{
type: "player-joined",
data: {
id: 0,
name: "name",
playerRoster: [
{
id: 0,
name: "name"
},
...
]
}
}
key | Description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.id |
Integer: The unique id of the player who joined. |
data.name |
String: The name of the player who joined. |
data.playerRoster |
Array: A collection of player objects for all players currently in the game, ordered by the time they joined the lobby. |
-->player.id |
Integer: The unique id of the given player. |
-->player.name |
String: The name of the given player. |
This message is broadcast to the game channel once the final player has joined the lobby. It is broadcast after the player joined message generated by that player.
{
type: "game-setup",
data: {
cards: [
{
id: 0,
word: "someword"
},
...
],
players: [
{
id: 0,
name: "name",
isBlueTeam: true,
isIntel: true
},
...
],
firstTeam: "red"
}
}
key | description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.cards |
Array: An ordered collection of card objects that will be used in the game. These cards go onto the board left-to-right, top-to-bottom. |
-->card.id |
Integer: The unique id for the given card. |
-->card.word |
String: The word to display on a given card. |
data.players |
Array: A collection of player objects for all players in the game, ordered by the time they joined the lobby. |
-->player.id |
Integer: The unique id for the given player. |
-->player.name |
String: The name for the given player. |
-->player.isBlueTeam |
Boolean: true if the player is on the blue team, false if they're on the red team. |
-->player.isIntel |
Boolean: true if the player has the Intel role, false if they don't. |
data.firstTeam |
String: The team that will play first: "red" or "blue". |
This message is sent from the game client to the server by the current player. So long as the sender is the current player and has the Intel role, and the hint is a single-word string, this will generate a Hint Provided broadcast to all players. If any of these requirements are not met, the appropriate Illegal Action message will be broadcast instead.
cable.sendHint({
hintWord: "tree",
numCards: "3"
})
key | Description |
---|---|
hintWord |
String: A single word hint. |
numCards |
Integer: The number of cards to which the hint is related. |
This message is broadcast to all players after a valid Hint Sent action is performed by a player. After receiving this message, play will proceed to the Spy player on the same team. The Spy will be allowed one more guess than the number of related cards.
{
type: 'hint-provided',
data: {
isBlueTeam: true,
hintWord: "tree",
relatedCards: 3,
currentPlayerId: 1
}
}
key | Description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.isBlueTeam |
Boolean: Whether the hint belongs to the Blue team or not. |
data.hintWord |
String: The hint word provided by the player. |
data.relatedCards |
Integer: The number of cards this hint is related to. |
data.currentPlayerId |
Integer: The ID of the new current player. |
This message is sent from the game client to the server by the current player. So long as the sender is the current player and has the Spy role, and the provided ID matches a card in the current game, this will generate a Board Update broadcast to all players, unless the guess causes the game to end, in which case the Game Over message will be broadcast instead. If any of these requirements are not met, the appropriate Illegal Action message will be broadcast instead.
If the Spy elects not to use all their guesses, they may pass. To convey a pass to the server, this message should be sent with id: null
.
cable.sendGuess({
id: "1"
})
key | Description |
---|---|
id |
Integer: The ID of the guessed card, or null if player ends their guesses early. |
This message is broadcast to all players after a valid Guess Sent action is performed by a player, unless the guess causes the game to end (in which case, the Game Over message will be broadcast instead). After receiving this message, play will proceed to the currentPlayer
provided in the response.
In the event that a Spy player decides to end their turn without taking all allowed guesses, the returned card
will be null
instead of an Object.
{
type: 'board-update',
data: {
card: { // {} or null
id: 1,
flipped: true,
type: "red"
},
remainingAttempts: 2,
currentPlayerId: 1
}
}
key | Description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.card |
Object: Data about the card that was guessed, or null if the player has ended their turn without making all allowed guesses. |
-->card.id |
Integer: The ID of the guessed card. |
-->card.flipped |
Boolean: The flipped state of the card (always true ). |
-->card.type |
String: The type of card to render in the UI: "red", "blue", "bystander", or "assassin". |
data.remainingAttempts |
Integer: The number of guesses remaining. |
data.currentPlayerId |
Integer: The ID of the current player. |
This message is broadcast to all players after a valid Guess Sent action causes the game to end. After receiving this message, the game UI will render the end state.
{
type: 'game-over',
data: {
card: {
id: 1,
flipped: true,
type: "red"
},
winningTeam: "red",
nextGame: "<invite code>"
}
}
key | Description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.card |
Object: Data about the card that was guessed. |
-->card.id |
Integer: The ID of the guessed card. |
-->card.flipped |
Boolean: The flipped state of the card (always true ). |
-->card.type |
String: The type of card to render in the UI: "red", "blue", "bystander", or "assassin". |
data.winningTeam |
String: The team that won the game. |
data.nextGame |
String: An invite code for a new game. Provided automatically so all players can easily join the next game. |
This message is broadcast to all players after any illegal action is performed by a player in a Hint Sent or Game Sent action.
{
type: 'illegal-action',
data: {
error: "<descriptive message>",
byPlayerId: 1
}
}
key | Description |
---|---|
type |
String: The type of message being broadcast. |
data |
Object: The data payload of the message. |
data.error |
String: The descriptive error message. |
data.byPlayerId |
Integer: The ID of the player who performed the illegal action. |
The potential illegal actions that are anticipated and caught are:
- The player performing the action is not the current player
- "<player name> attempted to submit a (hint/guess) out of turn"
- The player performed an action during their turn, but does not have the correct role for that action
- "<player name> attempted to submit a (hint/guess), but does't have the (Intel/Spy) role"
- An Intel player submits a multi-word hint during their turn
- "<player name> attempted to submit an invalid hint"
- A Spy player submits a guess with a card ID not present in this game
- "<player name> attempted to submit a guess for a card not in this game"