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<Input, State>(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

results matching ""

    No results matching ""