53 Things I Learned Writing a Multiplayer Strategy Game in Javascript and Python
Tags: react, django, css, javascript
Published: 2019-08-20 23:57:47

I started working on Selfies 2020 in April 2019, wanting to recreate a 1999 game called [sissyfight 2000](http://sissyfight.com/) with a modern twist. It is a 'tongue-in-cheek', social media game theory multiplayer game where players strategize to get followers. The followers a player gets depends on what the other players did that round. I am the sole contributor. 5 months later I wasn't done and I had a moment of reflection...**why am I doing this?** It was to learn more programming concepts (although it would be great if people played), particularly **everything that goes into making an app production-ready from front to back.** I also wanted to learn tech I didn't have much familiarity with, like Django and React Hooks. My ultimate goal was to make a production-quality app. 

The project is not quite "production-quality", but I consider it mostly "done." If you want to support it, star it on [github](https://github.com/aduranil/selfies), [play](https://selfies-22b79.firebaseapp.com) it, and offer your feedback!

{% youtube xyoPOfHdvA4 %}

I learned, by category:

[Frontend: Design](#place-1)
[Frontend: CSS + HTML](#place-2)
[Frontend: Javascript](#place-3)
[Frontend: React/Redux](#place-4)
[Frontend: Tooling](#place-5)
[Backend: Django](#place-6)
[Backend: Python](#place-7)
[Backend: Infrastructure](#place-8)
[Tooling: Github](#place-9)
[Overall: Metalearnings](#place-10)

    
## Frontend: Design <a name="place-1"></a>   
**1) Sketch is awesome. I’m not a designer** but learned enough Sketch to improve my design. Importing google fonts, **exporting svg/png/css, and using the iPhone components that come free** with the 1000's of plugins greatly improved my design process. Plus you can **prototype quickly**: 

![](https://thepracticaldev.s3.amazonaws.com/i/gwxpxoqdrdedh2y1obch.png)

**2) [Coolers](https://coolors.co/) is useful for generating color schemes**

**3) Learn CSS rather than a component framework.** I started off trying to learn and decide on a component framework. Getting the library components to do what you want is time consuming. I was more productive/improved my skills by learning CSS fundamentals.     

**4) If you do use a component framework, [Grommet](https://v2.grommet.io/) is a nice one that looks modern.**    

**5) On the homepage, tell users what your webpage does. Make navigation very clear.** I neglected navigation before reading this [book](https://www.amazon.com/Dont-Make-Think-Revisited-Usability-ebook/dp/B00HJUBRPG/ref=sr_1_1?crid=BEQZ8PV26SB7&keywords=don%27t+make+me+think&qid=1565915646&s=gateway&sprefix=don%27t+make+m%2Caps%2C133&sr=8-1). My homepage said "Click to enter!" without other info. I fixed these issues which might be obvious to a designer but were not on my radar.   
 
## Frontend: CSS + HTML <a name="place-2"></a>    

**6) [Reset CSS](https://meyerweb.com/eric/tools/css/reset/) to reduce browser inconsistencies.** The link has CSS that standardizes how your site looks on different browsers. 

**7) Styling components inline is more manageable than using CSS in my opinion.** It’s uglier, but having the styling in one file is easier. I sometimes move it to CSS later. 

**8) A component should not know about its position on a page.** It is the container's job to position the component. For example, all of my buttons had `margin-right: 5px` on them which I removed because relative positioning is the container's job. This **improves the reusability factor of the component.** 

**9) How to use [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), and that it doesn't look the same in Safari (Safari requires special `-webkit` prefixes to display flexbox).** `flex-grow` is especially useful. 
with `flex-grow: 1;` ![](https://firebasestorage.googleapis.com/v0/b/selfies-22b79.appspot.com/o/Screen%20Shot%202019-08-19%20at%203.56.28%20PM.png?alt=media&token=bd085253-a716-4445-9f3d-38f0ea786534)

without: ![](https://firebasestorage.googleapis.com/v0/b/selfies-22b79.appspot.com/o/Screen%20Shot%202019-08-19%20at%203.56.57%20PM.png?alt=media&token=98889fa2-3aa9-427f-ab42-105dced32d26)

**10) Positioning a div in the middle of a screen isn't straightforward.** 
{% gist https://gist.github.com/aduranil/7870e95baf94497ea13c7dbe97ea6719 %}

**11) Set `outline: none;` when styling inputs and buttons** or they will get an outline like the below when clicked/interacted with:

![](https://firebasestorage.googleapis.com/v0/b/selfies-22b79.appspot.com/o/Screen%20Shot%202019-08-19%20at%203.58.18%20PM.png?alt=media&token=943b1465-f9e1-40a6-927e-5ec771d86148)

**12) How to style a scrollbar.** It's different in Firefox, which only added some [support](https://stackoverflow.com/questions/6165472/custom-css-scrollbar-for-firefox/54101063#54101063) with Firefox 64 in 2018. I didn't style my scrollbars to be compatible with Firefox. For Safari and Chrome here is my CSS:
```CSS
::-webkit-scrollbar {
  width: 0.5em;
  background: none;
}

::-webkit-scrollbar-thumb {
  background: black;
  outline: 1px solid slategrey;
}
```

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/hdaopzknpcbcelouwbba.png)
**13) Objects, imgs, svgs.** Initially, an svg I used on the landing page was 440KB and hard to resize dynamically relative to the pink div around it because it was basically a base64 embedded .png of an iPhone that I made into an svg (the wrong move). My bundle size was huge when just loading it directly and wrapping it in a React component. Some solutions I tried:

* **img** I used an img with the src being the relative path to my svg. This made my bundle much smaller and let me resize, but I **lost my Roboto font face. img tags default to system fonts.** 
* **object** I then embedded the svg in an `object`. This solved both the aforementioned problems, but **I wasn't able to [click](https://stackoverflow.com/questions/11374059/make-an-html-svg-object-also-a-clickable-link) the image anymore.** I had to solve that styling the link wrapping the object with `display: inline-block`. This didn't give me quite the styling I wanted.
* **png** The moral is **be sure to select the image format that is the best for that image type and use**, which in this case was a png. 

**14) There are many ways to add animations in React. CSS is the best IMO.** I say this based on two criteria: performance and simplicity. I looked at [React Transition Group](https://github.com/reactjs/react-transition-group) and some libraries. I spent a minute trying to learn transition group and was confused. Then I found [this library](https://daneden.github.io/animate.css/). It has a bunch of awesome [examples](https://daneden.github.io/animate.css/) and you can copy the CSS for the animations you want into your CSS file so you don't have to import the entire library. I like to learn by example, so seeing all of these CSS animations gave me a sense of how they work. 

**15) A little bit of SCSS.** I don't like adding packages, but this is a dev dependency that compiles to CSS. It has additive feature like letting you embed properties and `lighten()` or `darken()` by certain amounts. Here is an example of how I styled my button:

```CSS
button {
  border-radius: 20px;
  cursor: pointer;
  border: 3px solid darken(#44ffd1, 5%);
  background-color: #44ffd1;
  box-shadow: 0 1px 1px 0 rgba(0, 0, 0.5, 0.5);
  font-size: 14px;
  padding: 5px;
  outline: none;
  &:hover {
    background-color: lighten(#44ffd1, 10%);
  }
  &:disabled {
    opacity: 0.5;
    cursor: default;
  }
}
```

## Frontend: Javascript <a name="place-3"></a> 

**16) How to write vanilla JS websockets and integrate them with redux.** {% link https://dev.to/aduranil/how-to-use-websockets-with-redux-a-step-by-step-guide-to-writing-understanding-connecting-socket-middleware-to-your-project-km3 %} 

**17) The [`next()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next)function.** I dove under the hood of javascript to learn more about iterators and generators when writing my websocket middleware. 

**18) Error handling with fetch.** I wanted fetch to throw an error if the backend returned a 400+ response. To do that, you have to check the status of the response first. **A 400+ error will have a message in the response body, but that is not available until the promise has resolved `(res => res.json())`. But if you throw an error before the promise has resolved, then you don't have access to the response body yet.** To solve this, **I added an async/await so I could pass the response body to my catch statement.**  

```javascript
const status = async (res) => {
  if (!res.ok) {
    const response = await res.json();
    throw new Error(response);
  }
  return res;
};

export const getCurrentUser = () => dispatch => fetch(`${API_ROOT}/app/user/`, {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Token ${Cookies.get('token')}`,
  },
})
  .then(status)
  .then(res => res.json())
  .then((json) => {
    dispatch({ type: 'SET_CURRENT_USER', data: json });
  })
  .catch(e => dispatch({ type: 'SET_ERROR', data: e.message }));
```

## Frontend: React/Redux <a name="place-4"></a>

**19) You can’t use [redux devtools](https://stackoverflow.com/questions/53514758/redux-typeerror-cannot-read-property-apply-of-undefined) with websockets** without more advanced configurations.

**20) How to use `useRef` with React Hooks.** I needed to use this to scroll to the bottom of a div. **This project was the first time I ever used React Hooks**:

{% gist https://gist.github.com/aduranil/19bf8dfad757fee8f86a9c309ef948e8 %}

**21) How to use PropTypes.** I normally use `Flow.js` but tried PropTypes. PropTypes library has the benefit of telling you about type issues both in the code editor and the console.

```javascript
// example propTypes for my game object
Game.propTypes = {
  id: PropTypes.string,
  dispatch: PropTypes.func.isRequired,
  history: PropTypes.shape({
    push: PropTypes.func.isRequired,
  }).isRequired,
  game: PropTypes.shape({
    id: PropTypes.number.isRequired,
    game_status: PropTypes.string.isRequired,
    is_joinable: PropTypes.bool.isRequired,
    room_name: PropTypes.string.isRequired,
    round_started: PropTypes.bool.isRequired,
    users: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.number.isRequired,
        followers: PropTypes.number.isRequired,
        selfies: PropTypes.number.isRequired,
        username: PropTypes.string.isRequired,
        started: PropTypes.bool.isRequired,
      }),
    ),
  }),
  time: PropTypes.string,
};

Game.defaultProps = {
  id: PropTypes.string,
  game: PropTypes.null,
  time: PropTypes.null,
  currentPlayer: PropTypes.null,
};
```

**22) How to share logic between components with React Hooks.** I use a hook that determines the color of buttons in different components. {% link https://dev.to/aduranil/react-custom-hooks-sharing-logic-between-components-19o8 %}

**23) You need to return an empty array when using `useEffect` or your component may keep re-rendering.** If you used any props though, you’ll get an error `react-hooks/exhaustive-deps`. To fix this, pass the prop you used to the array.

**24) How to set [environment variables](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables).** `create-react-app` makes it simple and I never appreciated how much `create-react-app` does for you. Just define an `env.production` and `env.development` file, and start your variables with `REACT_APP_` 

```
# my env.development file
REACT_APP_WS_HOST=localhost:8000
REACT_APP_HOST=http://localhost:8000
REACT_APP_PREFIX=ws
```

```
# my env.production file
REACT_APP_WS_HOST=selfies-2020.herokuapp.com
REACT_APP_HOST=https://selfies-2020.herokuapp.com
REACT_APP_PREFIX=wss
``` 

```javascript
// using the environment variable
const HOST = process.env.REACT_APP_WS_HOST;
const PREFIX = process.env.REACT_APP_PREFIX;

const host = `${PREFIX}://${HOST}/ws/game/${id}?token=${Cookies.get('token')}`;
```

**25) How to create an [Error Boundary](https://reactjs.org/docs/error-boundaries.html).** My code is a copy/paste of the example minus logging errors, which I might do if my app gets users. 
```javascript
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error) {
    // Catch errors in any components below and re-render with error message
    this.setState({
      hasError: error,
    });
    // You can also log error messages to an error reporting service here
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <React.Fragment>
          <h1
            className="animated infinite bounce"
            style={{
              textAlign: 'center',
              margin: 'auto',
              position: 'absolute',
              height: '100px',
              width: '100px',
              top: '0px',
              bottom: '0px',
              left: '0px',
              right: '0px',
            }}
          >
            Something went wrong
          </h1>
        </React.Fragment>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;
```

## Frontend: Tooling <a name="place-5"></a>

**26) How to analyze bundle size.** The instructions are [here](https://create-react-app.dev/docs/analyzing-the-bundle-size). 

```
[13:45:24] (other-stuff) selfies-frontend
🙋 yarn run analyze
yarn run v1.9.4
$ source-map-explorer 'build/static/js/*.js'
build/static/js/2.e1a940a4.chunk.js
  Unable to map 130/173168 bytes (0.08%)
build/static/js/main.17442792.chunk.js
  Unable to map 159/29603 bytes (0.54%)
build/static/js/runtime~main.a8a9905a.js
  2. Unable to map 62/1501 bytes (4.13%)
✨  Done in 0.52s.
```

Sample output from one of my builds:
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/eunayehbi3xhy8ob9txy.png)

**27) Configure [eslint](https://eslint.org/docs/user-guide/getting-started)** enough to keep my code organized.

**28) Safari does not support localStorage in [private mode](https://jonathanmh.com/use-ios-safari-localstorage-sessionstorage-private-mode/).** I switched to storing the token with cookies. I wanted to support major browsers, which very much includes Safari for mobile. 

**29) The WS tab in the browser.** I never noticed it or had any use for it before.

## Backend: Django <a name="place-6"></a>  

**30) It's hard to change a model from a One-to-One relationship to a foreign key relationship in Django.** I had to do four migrations and delete all of the records from my database to make it work.

**31) Django `get` method returns an error if what you're getting isn't actually there.** A `get_or_none` class is useful to either get the object if it's there or return nothing if not:
```python
class GetOrNoneManager(models.Manager):
    """Adds get_or_none method to objects"""

    def get_or_none(self, **kwargs):
        try:
            return self.get(**kwargs)
        except self.model.DoesNotExist:
            return None
``` 

**32) Overriding objects in Django.** If I want to use the class defined above, I will need to override the `objects` [Manager](https://docs.djangoproject.com/en/1.8/topics/db/managers/) in Django. A Manager is how you perform database queries in Django (i.e. `Model.objects.get, Model.objects.save`, etc) and one named `objects` is added to every Django class by default. 

```python
class GamePlayer(models.Model):
    #...
    objects = GetOrNoneManager()
```

**33) What [on_delete](https://stackoverflow.com/questions/38388423/what-does-on-delete-do-on-django-models) does on Django models.** It's a SQL standard and I set it to `MODELS.CASCADE` so that if one item is deleted, the references to that item are also deleted.

For example, I have this definition in my models:
```python 
class Message(models.Model):
    game = models.ForeignKey(Game, related_name="messages", on_delete=models.CASCADE)
```
If I delete an instance of `Game`, all models that have a foreign key to that instance of `Game` will also be deleted:

```
[14:10:42] (other-stuff) selfies-frontend
🙋 docker exec -it e079c83c8e1c bash
root@e079c83c8e1c:/selfies# python manage.py shell
Python 3.7.4 (default, Jul 13 2019, 14:20:24) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from app.models import Game
>>> Game.objects.get(id=17).delete()
(5, {'app.Move': 1, 'app.Message': 1, 'app.GamePlayer': 1, 'app.Round': 1, 'app.Game': 1})
>>> 
```
**34) Django has a `User` model with built-in authentication, but you [can't save stuff](https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html) on it.** This wasn't a problem for me until I wanted a Leaderboard. I solved this by creating a new model called `Winner` with a One-to-One relationship to the `User` model.

**35) Custom error handling with [Django REST framework](https://www.django-rest-framework.org/).** 
I thought the way the error messages were constructed with Django Rest Framework was hard for the frontend to handle, and decided to override them. {% link https://dev.to/aduranil/creating-custom-exceptions-using-django-rest-framework-pl0 %}

**36) Using Django Channels with token authentication is not ideal, and I had to write [custom middleware](https://stackoverflow.com/questions/43392889/how-do-you-authenticate-a-websocket-with-token-authentication-on-django-channels).** I've attached the token to the request because you can't [headers](https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api) with websockets. 

**37) How to use [Django Channels](https://channels.readthedocs.io/en/latest/).** Working through the tutorial in the documentation and following [this](https://github.com/justdjango/justchat) tutorial gave me what I needed to start my project. This is a big topic I may write a separate post about.

## Backend: Python/Pytest <a name="place-7"></a> 

**38) [Set up pytest](https://docs.pytest.org/en/latest/getting-started.html) in a project.** It’s fairly simple and one of the few setups where there isn’t some gotcha or bug that I need to fix. 

```ini
; pytest.ini, create this file in your root folder

[pytest]
DJANGO_SETTINGS_MODULE = selfies.settings
addopts = -s --ignore integrations --ignore tests --ignore integration_tests
python_files = tests.py test_*.py *_tests.py
```
**39) Testing with [pytest-factoryboy](https://github.com/pytest-dev/pytest-factoryboy) and pytest.** {% link https://dev.to/aduranil/writing-tests-with-pytest-and-factory-k7 %}

**40) Threading.** I needed to send the updated time on the timer to the frontend while not blocking other data I had to send too, like messages. To achieve this, I put the timer in another `thread` that updated the timer and sent the right time to the frontend.

Here is the code for that within my websocket class:

```python
    def start_round(self, data=None):
        """Checks if the user has opted in to starting the game"""

        game_player = GamePlayer.objects.get(user=self.scope["user"], game=self.game)
        game_player.started = True
        game_player.save()
        self.send_update_game_players()
        if self.game.can_start_game():
            # start the timer in another thread
            Round.objects.create(game=self.game, started=True)
            # pass round so we can set it to false after the time is done
            self.start_round_and_timer()

    def start_round_and_timer(self):
        """start timer in a new thread, continue to send game actions"""
        
        threading.Thread(target=self.update_timer_data).start()
        self.send_update_game_players()

    def update_timer_data(self):
        """countdown the timer for the game"""
        
        i = 90
        while i > 0:
            time.sleep(1)
            self.send_time(str(i))
            i -= 1
            try:
                round = Round.objects.get_or_none(game=self.game, started=True)
            except Exception:
                round = Round.objects.filter(game=self.game, started=True).latest(
                    "created_at"
                )
            if round.everyone_moved():
                i = 0
                j = 10
                while j > 0:
                    time.sleep(1)
                    self.send_time(str(j))
                    j -= 1

        # reset timer back to null
        self.send_time(None)
        self.new_round_or_determine_winner()
```
## Backend: Infrastructure <a name="place-8"></a> 

**41) How to lint python with [black](https://black.readthedocs.io/en/stable/index.html).** You type `black` and then the folder you want to lint and get:
```
[15:27:16] (master) selfies
🙋 black app
reformatted /Users/lina.rudashevski/code/selfies/app/services/message_service.py
All done! ✨ 🍰 ✨
1 file reformatted, 54 files left unchanged.
```
**42) What a webserver is.** You [don't want to use `python manage.py runserver`](https://vsupalov.com/django-runserver-in-production/) to start your server in production.

**43) How to get Docker to work with Django Channels.** For a while, no matter what I did, my server would not hot reload. I finally realized that my `Dockerfile` was in the wrong order after looking at [this](https://github.com/andrewgodwin/channels-examples/tree/master/multichat) helpful example. 

**44) How to [deploy Django Channels](https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django) with Heroku.** I did not deploy my docker container, but used a `Procfile` to start up my server. 

**45) How to find environment variables in Heroku, and what to do if they're not there.** 

Creating the app from the site didn't generate the environment variables I needed: 
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/pm9w21ecxo624jiencry.png)

After creating the add-ons, I looked for my database url using `heroku config` but nothing appeared. What fixed it was deleting my add-ons from the browser, and then recreating them from the the [command line](https://devcenter.heroku.com/articles/heroku-postgresql)

## Tooling: Github <a name="place-9"></a>

**46) How to [.gitignore files](https://stackoverflow.com/questions/24290358/remove-a-folder-from-git-tracking) I've already committed.** This isn't something I needed to do before, and because of that, it's a basic command that I overlooked.

## Overall: Metalearnings <a name="place-10"></a>

**47) To finish anything, just commit to starting.** I didn’t want to work on the project a lot, especially when I was stuck. It’s important to say “I’ll make progress on one thing” today, and not worry about hitting a certain goal or spending X amount of time. By just starting most days, I’d sometimes work all day, or sometimes I’d do nothing.

**48) It's hard to know what "done" means.** Should I add a leaderboard? Does my game need to work on mobile? Do the rules of the game make for a "fun" experience? Ultimately I said no to all of these questions so I added the leaderboard, redid the CSS to look right on mobile, and completely rewrote the rules of the game. Now I think it's done but I have a lot more I want to add like:
* better images/dynamic images server from the server
* sound effects
* a Jenkinsfile and scripts that build and deploy the app, and run management commands to perform certain routine actions like deleting old games
* let users customize their iPhone
* significantly expanded user menu with a profile they can fill out, and where they can see their old games and messages 
* users can message other players directly

**49) Don't name commits "ok" out of laziness.** This bad habit made it hard to find info for this post when looking through my commit history. 

**50) I don't learn well by watching tutorials or reading because I get bored and want the relevant information.** I don't learn well with verbal explanations, especially with code, because **people are often coming from a place of either knowing their subject matter too well or not knowing it at all.** As a newbie you often can't tell the difference either. I learn best looking at code examples and sample projects. 

**51) Sometimes you cannot fix a bug for weeks.** If that happens, work around it and revisit later when you have the mental energy. My game used docker with django channels for websockets. I had an issue where my docker server wouldn't hot reload. I had this issue for a long time and finally gave up by running everything locally. This let me make progress, and eventually I fixed the issue by copying the docker settings in one of the django channels sample projects. 

**52) It is a battle between trying to finish and trying to learn.** I grew impatient wanting to finish my game. Still, I remembered my personal mission statement and that it didn't impact anyone but me if I finished or not. It's not like it's a useful package other people can use. I didn't have to refactor with React Hooks, configure linters, use propTypes, etc., but my goal was to try to make a production quality app and learn a lot of new concepts. **Stick to your personal mission statement when doing a big project!** 

**53) Backtracking is necessary sometimes.** I defined a one-to-one relationship between my User and GamePlayer models early on. This ended up being the wrong design because there was no reason players couldn't be in multiple games and I had to make sure they were exited from a game, or they couldn't join another game. This wasn't really feasible. I had to 
- redo the schema, 
- delete everything from my production db
- find out that changing a one-to-one relationship to a foreign key in django is really troublesome
I almost didn't do it, opting to think of every possible way to make sure users were in one game at a time (lots of event listeners). I am glad I did because my app is less buggy and it was probably less work than trying to work around my bad design. 

**I also completely redid the rules of my game.** I made them match the rules of the original game. This was a huge rewrite that ultimately made for a much better experience.