Play, uselessness & learning

I love doing side-projects. But I sometimes think of side-projects as a guilty pleasure, since I almost never finish them, and because most of them are utterly useless.

For instance, I love making small retro games as a side-project. But I don’t care about the games in themselves. Hey, I don’t even play games (besides from with my oldest son).

So, why games? Games often deal with complex state management, and they often involve lots of side-effects. And as a result, games are the perfect medium for experiments and playing with new knowledge.

Currently I’m interested in functional programming, and I thought it would be kind of cool to make a game-ish demo using techniques from functional programming in JavaScript. Nope, I didn’t finish the game but you can move around a ship and crash into walls. I’ve actually taken the play part one step further and made hand-drawn graphics, photographed and modified in an image editor.

JavaScript isn’t a functional programming language, at least not if you would, leaning on someone like Erik Meyer, advocate a line of demarcation between ‘dogmatic’ functional programming (such as Haskell and other, ‘pure’ languages) and a functional ‘style’. A functional ‘style’ is very much possible using JavaScript, although ‘dogmatic’ functional programming is not.

Following Johan Huizinga’s view that the ‘disinterestedness of play’ has unique qualities for advancing insights, perhaps I shouldn’t be embarrassed, feeling that I’m wasting time. Few things strengthen your skills as playing around with programming, I think. Also, as Huizinga writes in Homo Ludus,

Not being “ordinary” life it stands outside the immediate satisfaction of wants and appetites) indeed it interrupts the appetitive process. It interpolates itself as a temporary activity satisfying in itself and ending there.

When making a game using JavaScript, in a style influenced by functional programming, we can’t be too dogmatic. We also do have other considerations, such as readability and efficiency. Our platform is the browser, we need input (from the keyboard, mouse or some other device) and we constantly (at least if it’s a game making use of graphics) output things to the screen. Keeping it fully pure may end up a quest for a grail we can’t possible hope to find.

The only thing interesting about the game might be, if you’re not super-familiar with functional programming, how it’s implemented.

I’ve attempted to respect ‘purity’, ‘single responsibility’, ‘immutability’, to avoid unnecessary assignments and when having them more viewing these as ‘labels’, and other principles often linked to functional programming.

My main learning from this project is that things easily ends up a bit obscure when your aims of bending JavaScript to the paradigm of functional programming go to far. JavaScript is not Haskell. Also, a great hazard when doing functional programming in JavaScript is efficiency (let’s not forget about recursion spinning into stack overflow).

A mountain-view code walk-through

The code (only about 200 lines) is appended as an example of how you can solve state-management and side-effects in a game, using functional programming and JavaScript.

Gary Bernhardt speaks about ‘functional core, imperative shell’ as a principle that should guide code. It’s a principle striving for reducing the amount of impurity and side-effects, aiming to isolate them and thereby controlling them. I’ve tried to structure my code in this fashion.

This section is not intended to be a tutorial and I guess it helps with a general awareness of how smaller games usually are done, imperative style. Therefore, when glancing the code don’t bother about the details, view it from a larger distance to get a hold of the broader strokes — how state is dealt with, how values transform and the manner in which side-effects are isolated.

Before the game logic as such initiates, we fetch all necessary resources, in this case an image (holding all the sprites and the tiles) and level data. The level is represented as a JSON 2d-array.

const getData = async () => {
const LEVEL = 'https://herebeseaswines.net/tiny-space-game/level.json';
const IMG_DATA = 'https://herebeseaswines.net/tiny-space-game/image-data.json';
const levelResponse = await fetch(LEVEL);
const level = await levelResponse.json();
const imgDataResponse = await fetch(IMG_DATA);
const imgData = await imgDataResponse.json();
return { level, imgData };
};
(async () => {
const ctx = document.querySelector('#canvas').getContext('2d');
const image = new Image();
image.src = 'assets/graphics.png';
const { imgData, level } = await getData();
requestAnimationFrame(() => gameLoop({
isPlaying: true,
TILE_SIZE: 20,
ctx,
e: eventHandler(),
G: {
imgData,
image,
},
pos: { x: 0, y: 200 },
loadTime: 50,
speed: 1,
level,
explosions: [],
missiles: [],
}));
})();

We also pass the context, a reference to the HTML canvas element on which all game graphics are drawn, and an eventHandler ,

const eventHandler = () => {
let keys = {
ArrowDown: false,
ArrowUp: false,
ArrowLeft: false,
ArrowRight: false,
};
const handleKeyDown = (e) => keys = e.key in keys && { ...keys, [e.key]: true };
const handleKeyUp = (e) => keys = e.key in keys && { ...keys, [e.key]: false };
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
return {
getValues() {
return keys;
},
};
};

Just like in an imperative implementation of a game, a functional style implementation uses a game loop. I use compose for this ‘loop’, allowing functions changing a part of the state. 

Since some functions are pure side-effects and we pass a game object without returning a modified state, we use a function I call tee , named after the Unix command both outputting a value and passing it on in a pipe.

const tee = (fn) => (data) => {
  fn(data);
  return data;
};

const compose = (...fns) => (initial) => fns.reduceRight(
  (a, b) => b(a),
  initial,
);

const gameLoop = (prevState) => {
  const nextState = compose(
    handleMovement,
    tee(drawPlayer),
    tee(drawNav),
    tee(drawBackground),
    tee(drawLevel),
  )(prevState);
  return nextState.isPlaying
    ? requestAnimationFrame(() => gameLoop(nextState))
    : drawMessage(prevState.ctx, 'GAME OVER');
};

If the game isPlaying , we continue the game loop with our new, transformed state, if not we print ‘game over’ to the screen. That’s it. In this very basic demo we don’t use a proper FPS algorithm but instead rely on requestAnimationFrame solving the update frequency.

In this demo, handleMovement is the only function transforming the state,

const isCanMove = ({ x1, y1, x2, y2 }, level, TILE_SIZE) => {
  const _x1 = Math.floor(x1 / TILE_SIZE);
  const _y1 = Math.floor(y1 / TILE_SIZE);
  const _x2 = Math.floor(x2 / TILE_SIZE);
  const _y2 = Math.floor(y2 / TILE_SIZE);
  return (level[_y1][_x1] === 255)
         && (level[_y2][_x1] === 255)
         && (level[_y1][_x2] === 255)
         && (level[_y2][_x2] === 255);
};

const isMovingTo = (direction) => direction === true;

const isPlayerAttemptingToMove = (e) => Object.values(e).some(isMovingTo);

const handleMovement = (app) => {
  const { pos, speed } = app;
  if (!isCanMove(
    {
      x1: pos.x, y1: pos.y, x2: pos.x + 145, y2: pos.y + 80,
    },
    app.level,
    app.TILE_SIZE,
  )) {
    return { ...app, isPlaying: false };
  }
  if (isPlayerAttemptingToMove(app.e.getValues())) {
    const [direction] = Object
      .entries(app.e.getValues())
      .find(([_, v]) => v === true);
    if (direction === 'ArrowDown') {
      return {
        ...app,
        pos: {
          x: pos.x + speed,
          y: pos.y + speed,
        },
      };
    }
    if (direction === 'ArrowUp') {
      return {
        ...app,
        pos: {
          x: pos.x + speed,
          y: pos.y - speed,
        },
      };
    }
    if (direction === 'ArrowLeft') {
      return {
        ...app,
        speed: speed >= 1 ? speed - 1 : 1,
        pos: { ...pos, x: pos.x + speed },
      };
    }
    if (direction === 'ArrowRight') {
      return {
        ...app,
        speed: speed <= 5 ? speed + 1 : 5,
        pos: { ...pos, x: pos.x + speed },
      };
    }
  }
  return {
    ...app,
    pos: {
      x: pos.x + speed,
      y: pos.y,
    },
  };
};

Since our tee function executes a function and still return the state, we don’t have to think to much about our draw functions (returning nothing).

In a more serious game-ish demo, it’s perhaps not advisable to use — at least it’s cleaner  — to use a normal for -loop(s) iterating the level, however much they’re forbidden because of the implicit mutation(s). I believe it’s absurd to bend a language not explicitly made for functional programming too much, just for the sake of some sort of miss-guided fidelity to a set of principles. However, since this is only ‘play’, using recursion is always good exercise.

const drawLevel = ({
  ctx, G, level, pos, TILE_SIZE,
}) => {
  ctx.fillStyle = '#FFFFFF';
  ctx.fillRect(0, 0, 900, 600);
  const lowerBoundary = (Math.floor(pos.x / TILE_SIZE) - 4) < 0
    ? 0
    : Math.floor(pos.x / TILE_SIZE);
  const upperBoundary = lowerBoundary + 50;
  (function loopY(y = 0) {
    if (y >= 30) {
      return;
    }
    (function loopX(x = lowerBoundary) {
      if (x >= upperBoundary) {
        return;
      }
      if (level[y][x] !== 255) {
        ctx.drawImage(
          G.image,
          ...G.imgData.tile,
          (x * TILE_SIZE) - pos.x,
          (y * TILE_SIZE),
          TILE_SIZE,
          TILE_SIZE,
        );
      }
      return loopX(x + 1);
    }());
    return loopY(y + 1);
  }());
};

const drawPlayer = ({ ctx, G, pos }) => ctx.drawImage(
  G.image,
  ...G.imgData.plr,
  75,
  pos.y,
  80,
  80,
);

const drawNav = ({ e, ctx, G }) => {
  ctx.drawImage(
    G.image,
    ...G.imgData.instructions,
    690,
    375,
    200,
    216,
  );
  if (isPlayerAttemptingToMove(e.getValues())) {
    const [direction] = Object
      .entries(e.getValues())
      .find(([_, v]) => v === true);
    ctx.drawImage(G.image, ...G.imgData.pressed[direction], 690, 375, 200, 216);
  }
};

// I've excluded some draw functions...

I believe the strategies used in this toy project could scale to a small indie game, but as I’ve repetitively stated — this is not the purpose. Of course, when we experiment with new concepts and patterns we could play with even more isolated code. But I think it’s more fun when things move about. Also, I believe it’s always more instructive with realistic examples. We get a better sense of how things could be done.

At times, I felt I shouldn’t have tried to stick to purity in the degree I did (with some exceptions). But the great thing with playing, is that you can make unsound decisions. Decisions you never would’ve made if your aim really was to build a game, make a real product.

The contrast emerging from being a bit dogmatic makes differences more visible, and implicitly helps us seeing them (and what’s important and in fact sound). Sometimes that which lack a clear purpose end up being the most useful of things.

Visit GitHub to see the full project

Leave a Reply