A collection of games that you can play in Discord. Basically GamePigeon for Discord.
Go to https://discord.dev and create an application. Then go to the bot section and create a bot for the application. Record the bot's client ID, token, and client secret.
Then go to https://mongodb.com and register an account and create a free database. Follow their steps to get your login URI and record the URI.
Finally, create a file called .env in the root folder and paste this in. Make sure to replace the with the actual values you recorded.
NODE_ENV=development
VITE_GAME_SERVER_URL=http://localhost:5000
GAME_SERVER_URL_INTERNAL=http://localhost:5000
GAME_SERVER_TOKEN=j7n0zLeXx1TxLpw7VHcZoYTgTnL6FDX38hHV!FTEQQ3eotRU4
REDIS_URL=redis://redis-12093.c245.us-east-1-3.ec2.cloud.redislabs.com:12093
REDIS_PASSWORD=7Lb9qdija1EoDDOqWRSzHZHORZrg8lJ6
BOT_IPC_TOKEN=JNA2oS7?dzoCtUBxN5Q55VHr6A!pdEESzRY2K5YRvbu2VKMYQ
BOT_IPC_URL=http://localhost:2000
DISCORD_SERVER_INVITE=https://discord.gg/C2Wm3pkcsz
APPINSIGHTS_CONNECTIONSTRING=InstrumentationKey=54ef4b41-2e52-4be2-bf3f-02471829b486;IngestionEndpoint=https://eastus-1.in.applicationinsights.azure.com
GAME_SERVER_HOST_PORT=5000
WEB_SERVER_URL=http://localhost:3000
PORT=3000
WEB_HOST_ID=0
WEB_HOST_NAME=mango
TOTAL_SHARDS=2
SHARD_MANAGER_COUNT=2
SHARD_MANAGER_POD_ADDRESS=http://localhost:200%ID%
SHARD_MANAGER_POD_PREFIX=shardmanager-
GAME_SERVER_HOST_ID=mango
TOP_GG_URL=https://top.gg/bot/939355142556373043
BOT_CLIENT_ID=<PASTE YOUR BOT'S CLIENT ID HERE>
BOT_CLIENT_SECRET=<PASTE YOUR BOT'S CLIENT SECRET HERE>
BOT_TOKEN=<PASTE YOUR BOT'S TOKEN HERE>
MONGODB_URI=<PASTE YOUR MONGODB LOGIN URI HERE>
Use this commandin your terminal to run Arcadecord:
npm run start
If you're having any trouble getting Arcadecord to work, feel free to let a core contributor know and we can help!
TODO: Create npm script for starting on Windows. For now the start script uses Bash syntax.
https://github.com/notken12/arcadecord/issues
- Player
- Discord bot
/bot
- Game server
/server
- Website
/server
,/server/src/components/games/*
,/server/public
,/server/src
- Database
/db
- The player can create a game with a Discord slash command.
- The Discord bot listens for the slash command and tells the game server, which creates a game.
- The Discord bot sends the link to play the game that was created.
- The player clicks the link and is brought to the website to play the game.
- Players log in with their Discord accounts.
- The website broadcasts the player's actions to the server so that gameplay is recorded.
- The server notifies the website when other players finish their turns and gives an updated state of the game.
- The Discord bot will send messages about events such as players finishing turns and when the game is over.
For the bot we are using Discord.js. The Discord application credentials (bot token, client secret, client id) are stored in the /.env
file. The files are private and will need to be shared with you before you can run the program.
The main script for the bot is /bot/bot.js
. Each interaction (commands, buttons, select menus) has its own script within the /bot
folder.
Games are stored in a MongoDB database in the cloud. The game state is represented using JSON and any time the game is interacted with, the server gets the game from the database, performs the actions, and saves the updated state.
The base class for all games. Contains logic for event handlers and action models. All game types extend this class.
Gameplay is turn-based. Each player takes a turn during the game. Each turn is made up of actions.
- Game
- Turns
- Actions
- Turns
Games automatically start when the first action is taken. Actions can only be taken before the game has ended. The first player who joins a game (besides the game creator) will be the first allowed to take an action. The logic used for an action (we will call this an action model) can decide whether the action ends the turn or even ends the game, and how to manipulate the game data.
id
: Unique IDplayers
: User[], snapshot of players profilesturn
: Number, index of the player in theplayers
array whos turn it issockets
: Object, dict of socket.io sockets, key is user id. Used to send turn and action data to the website client.hasStarted
: boolhasEnded
: boollastTurnInvite
: Discord message ID, last message saying whos turn it is, it can be deleted and replacedstartMessage
: Discord message ID, start message, it can be deletedwinner
: Number or null, index of the winner or -1 if it's a drawturns
: Turn[], the turns that happened over the gamedata
: Object, data about the game state. Ex: state of chess board.
setGuild(String guildId)
setChannel(String channelId)
setActionModel(String action, Function model, ?String side)
action
: Action typemodel
: Action model function, will be explained later.side
: optional, either'client'
or'server'
. Specifies if it will be client or server only. By default it will be common.
on(String event, Function callback)
: add event handler, ex: console.log when game starts. There are provided handlers that send fancy Discord messages when players take turns.onAction(String action, Function callback)
: add action listener that fires after actionasync addPlayer(String id)
: add player with user id, emits'join'
eventemit(String event, ...args)
: emit an event, ex: 'init'. Used internally.init()
: adds the game intogameManager
's store of games, emits'init'
event. Used internally.getURL()
: get the URL to play the gameasync doesUserHavePermission(String id)
: does user have perms to join game? (slash command perms in game's channel)async canUserJoin(String id)
: can user join game?async canUserSocketConnect(id)
: can the user's socket.io socket connect?getDataForClient(String userId)
: gets the data to be sent to the client via socket. Hides user ids which can be used to join as the player. Later user ids will be made available after we switch to using private keys
Some of the functions intended for internal use aren't listed here. See /server/src/games/Game.js
.
Methods to control game flow.
end(Game game, Object result)
: ends the game with result, {winner: player index or -1 for draw}, emits'end'
event, broadcasts'end'
to all socketsstart(Game game)
: starts the game, emits'start'
event, broadcasts'start'
to all sockets. Used internally.endTurn(Game game)
: ends the current turn, next players turn.
Each different game has a folder in /server/src/games/types
. The folder name is the game type's ID. The folder contains 3 files:
common.spec.js
main.js
common.js
The test file for the game. Tests are crucial for ensuring that game logic is implemented correctly. The recommended development workflow is to write the game behavior in English, and then to write the tests for those behaviors, and then finally write the code in main.js
and common.js
to make those tests pass.
The main script for the game.
Exports:
options
Game
Object that contains info about the game.
typeId
: The ID of the game type. This will be used as the name of all folders related to the game type.name
: Displayname of the gamedescription
: Description of the gamealiases
: String[], game aliases, may be used later for searching for gamesminPlayers
: Number, minimum players required to play the gamemaxPlayers
: Number, maximum playersemoji
: String, emoji used to represent the game. Can be normal emoji or custom discord emojis. Discord bots can use custom Discord emojis as if they were Nitro users.data
: Object, default data of the game. Game data is all the data about the game state. Ex: chess board state.
This object is important because it is read to display the game in the Discord commands and make it available without the need to hard-code it in. See /server/src/games/game-types.js
.
A class that extends the class Game
from /server/src/games/Game.js
. Will be called to create a new game. In the constructor, a new Game
will be created with super(options);
and then action models and event handlers are assigned.
Optionally, a getThumbnail
function can be defined which is used to generate a thumbnail for the Discord messages.
A file that contains data and functions that are used in both the website client and the game server. main.js
imports common.js
. Common action models must be functions from common.js
. Examples of data stored in this file are:
- Numbers for tile states on a checker board, ex.
TILE_EMPTY = 0
,TILE_WHITE = 1
,TILE_BLACK = 2
- Utility functions for the game
- Action models - mandatory. These will be explained later.
- Any variables that needs to be consistent between the client and the server.
The website is served from an express server in server.js
. All files from the /server/src/public
folder are served at /
. Files from /server/src
are compiled and served at /
.
When accessing a game, the website will check if the user has permission to join the game. If so, it will render the Vue component /server/src/components/games/{GAME_ID}/App.vue
.
We will be using vue.js to create the UI (settings button, game manual). A template of the basic UI and useful logic has been built to make writing games easier.
Gameplay pattern:
- Show replay of last player's turn
- Player takes actions and finishes their turn
- Waiting for opponent
Vue is used for UI. Learn Vue first if you haven't at https://vuejs.org. Get started by creating at /server/src/components/games/{GAME_ID}/App.vue
. That will be the main Vue component for the game.
Use the <game-view>
component to add the basic UI in and write your game UI inside of it.
You can optionally use the <score-view>
component to build score displays for players in the game. Use a template inside of it with v-slot="scoreView"
and you'll be able to define a score display and get the player index with scoreView.playerindex
.
<template>
<game-view>
<scores-view>
<template v-slot="scoreView">
<div>Score: {{ game.data.scores[scoreView.playerindex] }}</div>
</template>
</scores-view>
<div class="middle">
<!-- Put your main game UI here! -->
</div>
</game-view>
</template>
Access to and control of the game is provided by useFacade()
. Import and use it like this:
<script setup>
import { useFacade } from '@app/components/base-ui/facade';
const {
game,
$runAction,
$endAnimation,
$replayTurn,
$endReplay,
previousTurn,
} = useFacade();
// Make function to say hi to the game
const sayHi = () => {
alert(`Hi ${game.value.name}!`);
// Notice how you have to use game.value
// This is because the data is made reactive by using
// Vue refs. See https://vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-variables-with-ref
};
</script>
<template>
<game-view>
<div class="middle">
<!-- Put your main game UI here! -->
<button @click="sayHi">Say hi to {{ game.name }}</button>
<!-- In the template, you use game instead of game.value -->
</div>
</game-view>
</template>
The recommended design philosophy is to directly represent game data in the template as much as possible, and when it isn't (i.e. when using canvas) use Vue watchers to watch the game state and call the functions to update the UI. Because the data is driving the view and not the other way around, it's easy to implement a replay system as the template and watchers will be able to handle the changing game state no matter if it's live or replayed.
Facade will take care of managing game state and showing replays when needed, and you tell it how replays will be run and, optionally define special UI behavior when replaying like preventing the user from interacting with the game.
<script setup>
import { replayAction, utils } from '@app/js/client-framework';
import { onMounted } from 'vue';
import { useFacade } from '@app/components/base-ui/facade';
const {
game,
$runAction,
$endAnimation,
$replayTurn,
$endReplay,
previousTurn,
} = useFacade();
const ANIMATION_DURATION = 1000; // ms
// Start the replay once the component is displayed
onMounted(() => {
$replayTurn(async () => {
for (let action of previousTurn.value.actions) {
// Replay the action
replayAction(game.value, action);
// Wait for the animations to finish up before doing the next action
await utils.wait(ANIMATION_DURATION);
}
// All done
// You can delay ending the replay too
$endReplay(300); // ms
});
});
</script>
This contains the low level functions needed to interact with the server.
socket
: socket.io socket used to communicate with the serverutils
: Object, contains utility functionssetUpGame(game)
: sets up the game, called when the socket connection is made. Attaches functions to the game, which can't be sent over socket.io. Used internally.updateGame(gameToUpdate, newGame)
: updates data of the old game to the new game. Use it whenever you receive turn data from the server.
emitAction(game, actionType, actionData, actionCallback)
: Function, emits an action to the server. Used internally.runAction(game, type, data, callback, ?clone)
: Function, runs an action and emits it to the server. Call this whenever the user does an action.game
: Game, the game to run the action ontype
: String, the type of the actiondata
: Object, the data of the actioncallback
: Function, callback function to be called when the action acknowledgement is received from the server.clone
: Boolean, whether to clone the game before running the action. Used when you want to see what would happen if the action was run, for example when you want to run animations and don't want the UI to be affected.
connect(gameId, callback)
: Function, connects to the server. Call this when the page loads and set up the game UI in the callback.replayAction(game, action)
: Function, replays an action. This is the same as runAction but it doesn't emit it to the server and it doesn't need to be your turn.