loading...
Cover image for 53 Things I Learned Writing a Multiplayer Strategy Game in Javascript and Python

53 Things I Learned Writing a Multiplayer Strategy Game in Javascript and Python

aduranil profile image Lina Rudashevski ・15 min read

I started working on Selfies 2020 in April 2019, wanting to recreate a 1999 game called sissyfight 2000 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, play it, and offer your feedback!

I learned, by category:

Frontend: Design
Frontend: CSS + HTML
Frontend: Javascript
Frontend: React/Redux
Frontend: Tooling
Backend: Django
Backend: Python
Backend: Infrastructure
Tooling: Github
Overall: Metalearnings

Frontend: Design

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:

2) Coolers 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 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. 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

6) Reset CSS 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, 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;

without:

10) Positioning a div in the middle of a screen isn't straightforward.

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

12) How to style a scrollbar. It's different in Firefox, which only added some support with Firefox 64 in 2018. I didn't style my scrollbars to be compatible with Firefox. For Safari and Chrome here is my CSS:

::-webkit-scrollbar {
  width: 0.5em;
  background: none;
}

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

Alt Text
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 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 and some libraries. I spent a minute trying to learn transition group and was confused. Then I found this library. It has a bunch of awesome examples 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:

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

16) How to write vanilla JS websockets and integrate them with redux.

17) The 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.

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

19) You can’t use redux devtools 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:

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.

// 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.

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. 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
// 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. My code is a copy/paste of the example minus logging errors, which I might do if my app gets users.

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

26) How to analyze bundle size. The instructions are here.

[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

27) Configure eslint enough to keep my code organized.

28) Safari does not support localStorage in 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

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:

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 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.

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

33) What on_delete 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:

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 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.
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.

36) Using Django Channels with token authentication is not ideal, and I had to write custom middleware. I've attached the token to the request because you can't headers with websockets.

37) How to use Django Channels. Working through the tutorial in the documentation and following this 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

38) Set up pytest 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.

; 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 and pytest.

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:

    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

41) How to lint python with black. 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 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 helpful example.

44) How to deploy Django Channels 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

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

Tooling: Github

46) How to .gitignore files 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

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.

Discussion

pic
Editor guide
Collapse
tlannigan profile image
tlannigan

Great read, however I do have one qualm with you saying to remove outline for focused input elements. The outline property's specification doesn't call for need to follow border-radius (however Safari does). Check this out for more about it:

outlinenone.com/

Collapse
aduranil profile image
Lina Rudashevski Author

thanks for the feedback! I just meant I didn't like it from a styling perspective, I don't like how it looks. But the accessibility factor in the link you shared trumps my styling preference, and I didn't know that about it so thank you! The accessibility part of my lighthouse report did not catch this either

Collapse
aut0poietic profile image
Jer

The outline property itself isn't the critical bit. A distinct, accessible focus state is. So you could fake them:

input:focus {
  outline: 0;
  box-shadow: 0 0 0 3px #bfe7f3;
}

Only down-side to this technique is you generally need to then pay attention to the focus appearance for anything else in the app that receives focus so they all match.

Hope that's helpful and not just a noise-comment for what is a very awesome article! :)

Thread Thread
aduranil profile image
Lina Rudashevski Author

thanks for the tip!

Collapse
pitops profile image
Petros Kyriakou

Hello there Lina, nice job - getting your feet wet by diving into the project is the only way i know of that you can actually grow. I would like to point out a few somewhat wrong statements

7) Styling components inline is more manageable than using CSS in my opinion

In truth it seems more manageable because its easy but its not composable. You are redefining the same styles that you could reuse anywhere. I suggest you checkout styled-components which are the number one way to style stuff in react. Inline styles have their place but mostly for writing/overwriting specific css attributes.

10) Positioning a div in the middle of a screen isn't straightforward.

Aside fron the fact that you present a h1 tag :) - Its actually quite straight forward. You already mentioned flexbox so why not do this?

// html

<div>
  <h1>Loading</h1>
</div>

// css
div {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flexbox;
  align-items: center;
  justify-content: center;
}

14) There are many ways to add animations in React. CSS is the best IMO - performance and simplicity

This statement is half wrong. CSS animations are actually slower than Javascript animations. Yes they might feel more complex than CSS animations. But javascript animations (given a nice library) are more powerful and flexible.

This is a nice read if you want to get technical (css-tricks.com/myth-busting-css-an...). See a library like react-spring which again is a favorite among developers for animations in react.

Other than that, thats a lot of learning good job :)

Collapse
aduranil profile image
Lina Rudashevski Author

Thanks for the detailed feedback! On points 7 and 10, I heavily prefaced that this is just my opinion. Both statements are opinions that I don't present as fact. Tks about the div note, I updated my gist. The last comment, tks for the information. On 14, I looked at the link you sent across and it says that CSS vs Javascript performance is browser dependent and CSS is still faster in some cases, so I think that it is unfair to say that 'CSS animations are actually slower than Javascript' because that is not true either

Collapse
pitops profile image
Petros Kyriakou

Lets agree to disagree :)

Collapse
sebbdk profile image
Sebastian Vargr

I tend to get bored reading too, so I skim, a lot.

That sometimes backfires tho, as I will miss crucial information, that will leave me googling impossible questions easily answered in docs...

I’ve gotten better at skimming tho, and nowadays I use source code just as much as docs.

I find source code much more paletteable to read.

Collapse
aduranil profile image
Lina Rudashevski Author

yeah i need to get better at this :( patience is a virtue

Collapse
bootcode profile image
Robin Palotai

It is a battle between trying to finish and trying to learn.

I can relate to this. Getting stuck at The ends of this spectrum is not productive on the long term. You either fiddle without progress, or eventually get swamped by all the accumulated hacks.

Swinging between the two gives development a rythm.

Collapse
aduranil profile image
Lina Rudashevski Author

100% sometimes you just gotta finish

Collapse
rudolfolah profile image
Rudolf Olah

Awesome list!

It's interesting that you used React for game dev. It would be neat to see this type of game created with Godot or Phaser or pygame using the same Django backend.

Collapse
aduranil profile image
Lina Rudashevski Author

great suggestion!

Collapse
noway profile image
Ilia

Awesome!

Does anyone know if error boundaries in react are sort of deprecated? The point about error boundaries and getDerivedStateFromError brought me to thinking that rendering an error state ui is something you are not supposed to do in react? What is a proper way then?

Collapse
aduranil profile image
Lina Rudashevski Author

hmm i dont see anything about it being deprecated, its still in the official docs without any deprecation warnings. do you have a source where you saw its deprecated? i would love to know more

Collapse
moopet profile image
Ben Sinclair

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

But I want the outline, otherwise the page is going to be really difficult to use!

Collapse
adusoft profile image
Geofrey Aduda

am new in Python, but how you have explained it in your tutorial gives me the energy to press on. I would like to learn advanced python, Django since I am a beginner which articles do you suggest

Collapse
aduranil profile image
Lina Rudashevski Author

i suggest having an idea for a project and then figuring out what you need along the way. i didn’t know a lot of what i learned before i started and would just stack overflow my way into solutions. good luck!

Collapse
adusoft profile image
Collapse
netanelravid profile image
Netanel Ravid

Such a great post.
Thanks Lina!!

Collapse
aduranil profile image
Lina Rudashevski Author

thank you for reading!

Collapse
steelwolf180 profile image
Max Ong Zong Bao

Awesome article, Django channels is one of the intresting topics that does not have tons of documentation or tutorials out there.

I'm really amazed that you were able to built something out of it.

Collapse
aduranil profile image
Lina Rudashevski Author

Thanks! Yeah there wasn't a lot of documentation which is why I included a link to a tutorial I found helpful. I might write a longer post out of this topic!

Collapse
emlautarom1 profile image
Martín Emanuel

Thanks for sharing your experiences!

Collapse
aduranil profile image
Collapse
jimtheman profile image
Jim Lynch

This is awesome. :)

Collapse
alexparra profile image
Alex Parra

Great stuff!
Now I want the spare time to do the same ;)

Collapse
aduranil profile image
Lina Rudashevski Author

it is quite a time investment

Collapse
chrisachard profile image
Chris Achard

That's quite the list of things to learn!

Collapse
aduranil profile image
Collapse
shenril profile image
Shenril

That looks like an amazing journey! Congrats to you!

Collapse
aduranil profile image
Collapse
khaldon profile image
mohamed khaled

Do you know how to make auto-reload for Django app during development or change files in the local environment