Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 20, 2019 11:57 pm GMT

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

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. Im 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. Its 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 cant 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 objectGame.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, youll 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 fileREACT_APP_WS_HOST=localhost:8000REACT_APP_HOST=http://localhost:8000REACT_APP_PREFIX=ws
# my env.production fileREACT_APP_WS_HOST=selfies-2020.herokuapp.comREACT_APP_HOST=https://selfies-2020.herokuapp.comREACT_APP_PREFIX=wss
// using the environment variableconst 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 analyzeyarn 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 bashroot@e079c83c8e1c:/selfies# python manage.py shellPython 3.7.4 (default, Jul 13 2019, 14:20:24) [GCC 6.3.0 20170516] on linuxType "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. Its fairly simple and one of the few setups where there isnt some gotcha or bug that I need to fix.

; pytest.ini, create this file in your root folder[pytest]DJANGO_SETTINGS_MODULE = selfies.settingsaddopts = -s --ignore integrations --ignore tests --ignore integration_testspython_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 appreformatted /Users/lina.rudashevski/code/selfies/app/services/message_service.pyAll 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 didnt want to work on the project a lot, especially when I was stuck. Its important to say Ill 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, Id sometimes work all day, or sometimes Id 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 troublesomeI 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.


Original Link: https://dev.to/aduranil/53-learnings-from-writing-a-multiplayer-strategy-game-3ijd

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To