Space Invaders Game

By adamlubek

This recipe demonstrates RxJS implementation of Space Invaders Game.

Example Code

( StackBlitz )

index.ts

// RxJS v6+
import { fromEvent, interval } from 'rxjs';
import {
  map,
  scan,
  tap,
  startWith,
  withLatestFrom,
  takeUntil,
  repeat
} from 'rxjs/operators';
import { gameUpdate, initialState } from './game';
import { State, Input } from './interfaces';
import { paint } from './html-renderer';

const spaceInvaders$ = interval(100).pipe(
  withLatestFrom(
    fromEvent(document, 'keydown').pipe(
      startWith({ code: '' }),
      takeUntil(fromEvent(document, 'keyup')),
      repeat()
    )
  ),
  map(([intrvl, event]: [number, KeyboardEvent]): Input => ({
    dlta: intrvl,
    key: event.code
  })),
  scan(gameUpdate, initialState),
  tap(e => paint(e.game, e.playerLives, e.score, e.isGameOver))
);

spaceInvaders$.subscribe();

game.ts

import { State, Input } from './interfaces';
import { empty, player, invader, shot, noOfInvadersRows } from './constants';

const gameObject = (x, y) => ({ x: x, y: y });
const gameSize = 20;
const clearGame = () =>
  Array(gameSize)
    .fill(empty)
    .map(e => Array(gameSize).fill(empty));

const createInvaders = () =>
  Array.from(Array(noOfInvadersRows).keys()).reduce(
    (invds, row) => [...invds, ...createRowOfInvaders(row)],
    []
  );
const createRowOfInvaders = row =>
  Array.from(Array(gameSize / 2).keys())
    .filter(e => (row % 2 === 0 ? e % 2 === 0 : e % 2 !== 0))
    .map(e => gameObject(row, e + 4));

const invadersDirection = (state: State): number =>
  state.invaders.length && state.invaders[0].y <= 0
    ? 1
    : state.invaders.length &&
      state.invaders[state.invaders.length - 1].y >= gameSize - 1
    ? -1
    : state.invadersDirY;

const drawGame = (state: State): number[][] => (
  keepShipWithinGame(state),
  (state.game = clearGame()),
  (state.game[state.game.length - 1][state.shipY] = player),
  state.invaders.forEach(i => (state.game[i.x][i.y] = invader)),
  state.invadersShoots.forEach(s => (state.game[s.x][s.y] = shot)),
  state.shoots.forEach(s => (state.game[s.x][s.y] = shot)),
  state.game
);

const addInvaderShoot = state =>
  (randomInvader => gameObject(randomInvader.x, randomInvader.y))(
    state.invaders[Math.floor(Math.random() * state.invaders.length)]
  );

const collision = (e1, e2) => e1.x === e2.x && e1.y === e2.y;
const filterOutCollisions = (c1: any[], c2: any[]): any[] =>
  c1.filter(e1 => !c2.find(e2 => collision(e1, e2)));
const updateScore = (state: State): number =>
  state.shoots.find(s => state.invaders.find(i => collision(s, i)))
    ? state.score + 1
    : state.score;

const updateState = (state: State): State => ({
  delta: state.delta,
  game: drawGame(state),
  shipY: state.shipY,
  playerLives: state.invadersShoots.some(
    e => e.x === gameSize - 1 && e.y === state.shipY
  )
    ? state.playerLives - 1
    : state.playerLives,
  isGameOver: state.playerLives <= 0,
  score: updateScore(state),
  invadersDirY: invadersDirection(state),
  invaders: !state.invaders.length
    ? createInvaders()
    : filterOutCollisions(state.invaders, state.shoots).map(i =>
        state.delta % 10 === 0
          ? gameObject(
              i.x + (state.delta % (state.shootFrequency + 10) === 0 ? 1 : 0),
              i.y + state.invadersDirY
            )
          : i
      ),
  invadersShoots:
    ((state.invadersShoots =
      state.delta % state.shootFrequency === 0
        ? [...state.invadersShoots, addInvaderShoot(state)]
        : state.invadersShoots),
    state.invadersShoots
      .filter(e => e.x < gameSize - 1)
      .map(e => gameObject(e.x + 1, e.y))),
  shoots: filterOutCollisions(state.shoots, state.invaders)
    .filter(e => e.x > 0)
    .map(e => gameObject(e.x - 1, e.y)),
  shootFrequency: !state.invaders.length
    ? state.shootFrequency - 5
    : state.shootFrequency
});

const keepShipWithinGame = (state: State): number => (
  (state.shipY = state.shipY < 0 ? 0 : state.shipY),
  (state.shipY = state.shipY >= gameSize - 1 ? gameSize - 1 : state.shipY)
);

const updateShipY = (state: State, input: Input): number =>
  input.key !== 'ArrowLeft' && input.key !== 'ArrowRight'
    ? state.shipY
    : (state.shipY -= input.key === 'ArrowLeft' ? 1 : -1);

const addShots = (state: State, input: Input) =>
  (state.shoots =
    input.key === 'Space'
      ? [...state.shoots, gameObject(gameSize - 2, state.shipY)]
      : state.shoots);

const isGameOver = (state: State): boolean =>
  state.playerLives <= 0 ||
  (state.invaders.length &&
    state.invaders[state.invaders.length - 1].x >= gameSize - 1);

export const initialState: State = {
  delta: 0,
  game: clearGame(),
  shipY: 10,
  playerLives: 3,
  isGameOver: false,
  score: 0,
  invadersDirY: 1,
  invaders: createInvaders(),
  invadersShoots: [],
  shoots: [],
  shootFrequency: 20
};

const processInput = (state: State, input: Input) => (
  updateShipY(state, input), addShots(state, input)
);
const whileNotGameOver = (state: State, input: Input) =>
  (state.delta = isGameOver(state) ? undefined : input.dlta);

export const gameUpdate = (state: State, input: Input): State => (
  whileNotGameOver(state, input), processInput(state, input), updateState(state)
);

constants.ts

export const empty = 0;
export const player = 1;
export const invader = 2;
export const shot = 3;
export const noOfInvadersRows = 6;

interfaces.ts

export interface State {
  delta: number;
  game: number[][];
  shipY: number;
  playerLives: number;
  isGameOver: boolean;
  score: number;
  invadersDirY: number;
  invaders: any[];
  invadersShoots: any[];
  shoots: any[];
  shootFrequency: number;
}

export interface Input {
  dlta: number;
  key: string;
}

html-renderer.ts

import { empty, player, invader, shot } from './constants';

const createElem = col => {
  const elem = document.createElement('div');
  elem.classList.add('board');
  elem.style.display = 'inline-block';
  elem.style.marginLeft = '10px';
  elem.style.height = '6px';
  elem.style.width = '6px';
  elem.style['background-color'] =
    col === empty
      ? 'white'
      : col === player
      ? 'cornflowerblue'
      : col === invader
      ? 'gray'
      : 'silver';
  elem.style['border-radius'] = '90%';
  return elem;
};

export const paint = (
  game: number[][],
  playerLives: number,
  score: number,
  isGameOver: boolean
) => {
  document.body.innerHTML = '';
  document.body.innerHTML += `Score: ${score} Lives: ${playerLives}`;

  if (isGameOver) {
    document.body.innerHTML += ' GAME OVER!';
    return;
  }

  game.forEach(row => {
    const rowContainer = document.createElement('div');
    row.forEach(col => rowContainer.appendChild(createElem(col)));
    document.body.appendChild(rowContainer);
  });
};

Operators Used

Last updated