This is the code behind http://oneword.games, a group of online games inspired by party games like Just One and Decrypto.
- Install Node.js and npm
$ npm install --global yarn
to install Yarn$ yarn
to install JS dependencies
$ yarn dev
to spin up a local server at http://localhost:3000- Start editing code! See our recommended tools.
Merging code directly into master
is okay, but if some work you're doing is:
- Risky (could break existing gameplay)
- Uncertain (not sure if we want this)
- Touches code that others are also modifying
... then consider opening up a Pull Request instead!
Every pull request will automatically get a separate preview URL; perfect for getting quick feedback.
If you write some complicated logic, consider adding unit tests! Our testing framework combines @web/test-runner, this Vite plugin, and Chai.
$ yarn test
to run all tests- See
src/utils.test.js
for an example of how to add tests
Just push to master
, and the site will update automatically.
We use Capacitor to package our web apps into mobile apps.
- Install Android Studio
- Set up an Android emulator, or connect your phone in developer mode
$ yarn build
to build Vue app for distribution$ npx cap run --list android
to check that your emulator/phone is connected$ yarn android
to run on Android- Or
$ npx cap open android
to edit project in Android Studio
- Or
OR for live reloading from your local server:
$ yarn dev
to spin up a local Vue server- Copy the network url (e.g.
http://192.168.1.5:3001
) - Paste it into
capacitor.config.json
'sserver.url
field. $ yarn android
to run on Android
(TODO: would be cool to have $ yarn android-dev
do all of the above)
See also this guide to Play Store deployment.
- Get the keystore file (
oneword.jks
) and passwords from Austin $ npx cap open android
to open Android Studio- Build > Generate Signed APK
- Find the generated app in
oneword/android/app/release/app-release.aab
(TODO: Try out Appflow for automatic app deploys)
$ yarn resources
to generate app icons- For Android, we use adaptive icons
- To crack the keystore password, see this
- To use Java JDK after Android Studio is installed:
export JAVA_HOME=/Applications/Android\ Studio.app/Contents/jre/jdk/Contents/Home
- To use Java JDK after Android Studio is installed:
You'll need:
- A Mac to develop on, with:
- Xcode
- CocoaPods (
sudo gem install cocoapods
to install)
$ yarn build
to build Vue app for distribution$ yarn ios
to run on iOS- Or
$ npx cap open ios
to edit project in Xcode
- Or
- While setting up, needed
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
(see link) - OAuth client ID:
340753176141-k38315g3fgbnfq3avasgq05dmg9evjj2.apps.googleusercontent.com
(see reference)
Vue i18n is a library that lets our UI change between English and other languages.
Localazy is a website that allows people to add translations without needing to code.
- In
src/locales/en.json
, add a key and its English value - In the codebase, replace the English value with
{{ $t('key') }}
- Run
$ yarn localazy upload
to tell Localazy about the new key
- In
src/locales/{LANG}.json
, add the translation fo the English value - Run
$ yarn localazy upload existing
to upload the translation
-
Add the new language from the Localazy UI
-
On the main translations page, go to "Start Translating" from the dropdown
-
Translate the words using the UI
-
Click the green Publish button:
-
Run
yarn localazy download
to get the translated JSON file
One Word is built on top of:
- VueJS on the frontend
- Bulma for CSS styling
- Netlify for hosting
- Firestore for the database
- Firebase Auth for login
- Stripe for payments
- Mailjet for marketing & transactional emails
- Vue i18n and Localazy for localization
One Word code is all on the client (aka JAMStack). We start by defining the game's data: a single JS object to represent one game room. For example:
Room: {
name: apple,
players: ['alice', 'bob', 'carol'],
currentRound: {
state: 'clueing' // or 'guessing', or 'done'
guesser: 'alice',
word: 'company'
clues: {
alice: 'corporation',
bob: 'collected'
carol: 'collected'
}
}
history: [{round1}, ...]
}
This has all the info needed to represent the entire state of the game, at any point in time. It should be complete (no info missing), but also minimum (no additional information).
Based on what's in the room object, we then want to show the right HTML and CSS to users. The VueJS framework helps us express our JS object as HTML to show the user, and also handles user inputs.
Here's a simple example of Vue code:
<template>
<!-- JS expressions inside {{ these braces }} get rendered -->
<p v-for="player in room.players">- {{ player }}</p>
<!-- newPlayer automatically syncs when this input is edited -->
<input v-model="newPlayer" />
<!-- Call some Javascript code using @event notation -->
<button @click="addPlayer">Add a player</button>
</template>
<script>
export default {
data() {
return {
room: { players: ['Alice', 'Bob', 'Charlie'] },
newPlayer: 'Eve',
}
},
methods: {
addPlayer() {
this.room.players.push(this.newPlayer)
},
},
}
</script>
Which produces:
The amazing thing is that Vue binds the HTML elements and forms to our JS data. When we update the JS data, the HTML elements will re-render; and when the user clicks on or types in an HTML form, the underlying JS data stays in sync!
Now, how does one player's changes get sent to everyone else? A: Firestore. Each different game (One Word, Incrypt) has a different Firestore table, containing every room ever created and keyed by the room's name.
We use some Firestore logic to keep everyone's clients sync'd to the latest state. So your code can act as though its room is always up-to-date; you only think about when you need to push a change to the Firestore database. Super convenient!
To prevent race conditions (one client overwriting the changes of another), try to scope down each change to be very narrow. Instead of pushing the entire room object each time, just push the path of the object that updated.
Continuing the example above:
async addPlayer() {
this.room.players.push(this.newPlayer)
// Only update the 'players' field of the room
await saveRoom(room, 'players')
},
And... that's all you really need to get started with making your own game!
-
Get the Vue3 devtools to easily debug and inspect your Vue logic
-
We highly, highly, highly recommend VSCode, along with these extensions:
-
Vetur
for Vue syntax highlighting -
Prettier
for format-on-save -
i18n Ally
for text/translation display- Then open VSCode
Preferences: Open Settings (JSON)
, and add
"editor.formatOnSave": true, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "i18n-ally.sortKeys": true, "i18n-ally.keepFulfilled": true,
- Then open VSCode
-
Optional
Debugger for Chrome
for breakpoint debugging from inside VSCode- Then from the menu,
Run
>Add configuration
>Chrome (preview)
- In-depth setup instructions
- Then from the menu,
-
-
You can instantly join as guest by adding
?player=Holo
at the end of a room url. Useful for keeping 3 tabs open, for testing multiplayer interactions!
- Most important: keep your Firestore data structure simple and elegant. App logic and visuals are easy to change; backfilling data is annoying.
- Prefer local computed properties over putting more things into the Firestore room.
- Declarative code (the WHAT) is better than imperative code (the HOW); for example, prefer array.map() over for loops.
- When possible, your Firestore pushes should to be idempotent (ie can be called multiple times with the same effect), and free of race conditions (ie one client should not overwrite another's changes).
- Instead of pushing an array to Firestore, consider pushing an object.
(Then make a computed array, eg with
Object.keys(foo)
). This prevents race conditions if multiple clients update the same array
- Constantly invest in faster dev velocity!
- Build mod tools for yourself
- If anything in your iteration cycle seems slow, bother Austin about it
- Code should be as readable as possible
- Self-documenting if possible, then comments
- It's easier to read less code!
This is useful for testing the site for performance, in Chrome Devtool's Lighthouse.
$ yarn build
$ yarn serve
Then go to http://localhost:5000 and run Lighthouse!