Uncover Image Game

By adamlubek

This recipe demonstrates RxJS implementation of Uncover Image Game.

Example Code

( StackBlitz )

index.ts

// RxJS v6+
import { interval } from 'rxjs';
import { finalize, scan, takeWhile, tap, withLatestFrom } from 'rxjs/operators';
import { keyboardEvents$ } from './keyboard';
import { initialGame, updateGame, isGameOn } from './game';
import { paintGame, paintGameOver } from './html-renderer';

interval(15)
  .pipe(
    withLatestFrom(keyboardEvents$),
    scan(updateGame, initialGame),
    tap(paintGame),
    takeWhile(isGameOn),
    finalize(paintGameOver)
  )
  .subscribe();

game.ts

import { noop } from 'rxjs';
import { Enemy, State, Move } from './interfaces';
import { size } from './constants';
import { newPlayerFrom, movePlayer } from './player';
import { newEnemiesFrom } from './enemy';

const intersect = (state: State): State => (
  state.moves.some((m: Move) =>
    state.enemies.some(e => m.x === e.x && m.y === e.y)
  )
    ? ((state.player.lives -= 1),
      (state.player.x = 0),
      (state.player.y = 0),
      (state.moves = []))
    : noop,
  state
);

const initialGame: State = {
  player: { x: 0, y: 0, lives: 3 },
  enemies: [
    { x: 10, y: 10, moveDuration: 0, dirX: 1, dirY: 1 },
    { x: 50, y: 50, moveDuration: 0, dirX: -1, dirY: 1 }
  ],
  key: '',
  moves: [],
  corners: []
};

const updateGame = (state: State, [_, key]: [number, string]): State => (
  (state.enemies = newEnemiesFrom(state)),
  (state.player = newPlayerFrom(state, key)),
  (state = intersect(state)),
  (state = movePlayer(state, key)),
  (state.key = key),
  state
);

const isGameOn = (state: State): boolean => state.player.lives > 0;

export { initialGame, updateGame, isGameOn };

player.ts

import { noop } from 'rxjs';
import { Player, State, Move } from './interfaces';
import { size } from './constants';
import { keyToDirection } from './keyboard';

const up = 'ArrowUp';
const down = 'ArrowDown';
const left = 'ArrowLeft';
const right = 'ArrowRight';

const newPlayerFrom = (state: State, key: string): Player => (
  (state.player.x += keyToDirection(key, down, up)),
  (state.player.y += keyToDirection(key, right, left)),
  state.player.x < 0 ? (state.player.x = 0) : noop,
  state.player.x > size ? (state.player.x = size) : noop,
  state.player.y < 0 ? (state.player.y = 0) : noop,
  state.player.y > size ? (state.player.y = size) : noop,
  state.player
);

const getEnclosedArea = (state: State): Move[] =>
  state.moves.length <= 1
    ? []
    : [
        ...state.moves
          .slice(
            state.moves.findIndex(
              e => e.x === state.player.x && e.y === state.player.y
            )
          )
          .filter(e => e.dirChange),
        state.moves.pop()
      ];

const movePlayer = (state: State, key: string): State => (
  state.moves.some(e => e.x === state.player.x && e.y === state.player.y)
    ? ((state.corners = getEnclosedArea(state)), (state.moves = []))
    : state.moves.push({
        x: state.player.x,
        y: state.player.y,
        dirChange: state.key !== key
      }),
  state
);

export { newPlayerFrom, movePlayer };

enemy.ts

import { noop } from 'rxjs';
import { Enemy, State } from './interfaces';
import { size } from './constants';

const newEnemiesFrom = (state: State): Enemy[] => (
  state.enemies.forEach(
    e => (
      e.x <= 0 || e.x > size ? (e.dirX *= -1) : noop,
      e.y <= 0 || e.y > size ? (e.dirY *= -1) : noop,
      (e.x += e.dirX),
      (e.y += e.dirY),
      (e.moveDuration += 1),
      e.moveDuration > 100
        ? ((e.dirX = Math.random() > 0.5 ? 1 : -1),
          (e.dirY = Math.random() > 0.5 ? 1 : -1),
          (e.moveDuration = 0))
        : noop
    )
  ),
  state.enemies
);

export { newEnemiesFrom };

keyboard.ts

import { fromEvent } from 'rxjs';
import { pluck, startWith } from 'rxjs/operators';

const positionChangeUnit = 2;

export const keyboardEvents$ = fromEvent(document, 'keydown').pipe(
  pluck < KeyboardEvent,
  string > 'code',
  startWith('')
);

export const keyToDirection = (
  key: string,
  key1: string,
  key2: string
): number =>
  key === key1 ? positionChangeUnit : key === key2 ? -positionChangeUnit : 0;

interfaces.ts

interface GameObject {
  x: number;
  y: number;
}

interface Player extends GameObject {
  lives: number;
}

interface Enemy extends GameObject {
  moveDuration: number;
  dirX: number;
  dirY: number;
}

interface Move extends GameObject {
  dirChange: boolean;
}

interface State {
  player: Player;
  enemies: Enemy[];
  key: string;
  moves: Move[];
  corners: Move[];
}

export { GameObject, Player, Enemy, State };

constants.ts

const size = 200;

export { size };

html-renderer.ts

import { State, GameObject } from './interfaces';

const clearPlayerPath = _ =>
  document
    .querySelectorAll('circle')
    .forEach(e => document.querySelector('#svg_container').removeChild(e));

const addCircleColored = (color: string) => (e: GameObject) => {
  const circle = document.createElementNS(
    'http://www.w3.org/2000/svg',
    'circle'
  );
  circle.setAttribute('cx', e.y);
  circle.setAttribute('cy', e.x);
  circle.setAttribute('r', '2');
  circle.setAttribute('stroke', color);
  circle.setAttribute('strokeWidth', '1');
  document.querySelector('#svg_container').appendChild(circle);
};

const addPlayerPath = (state: State) =>
  state.moves.forEach(addCircleColored('gray'));
const addEnemy = (state: State) =>
  state.enemies.forEach(addCircleColored('red'));

const addHoles = (state: State) => {
  if (!state.corners.length) {
    return;
  }

  const createPathFromCorners = (a, c) =>
    (a += `${a.endsWith('Z') ? 'M' : 'L'} ${c.y} ${c.x} ${
      c.dirChange ? '' : 'Z'
    }`);
  const newPath =
    `M${state.corners[0].y} ${state.corners[0].x}` +
    state.corners.reduce(createPathFromCorners, '');
  const maskPath = document.querySelector('#mask_path');

  const currentPath = maskPath.getAttribute('d');
  const path = newPath + ' ' + currentPath;

  maskPath.setAttribute('d', path);
};

const paintInfo = (text: string) =>
  (document.querySelector('#info').innerHTML = text);
const paintLives = (state: State) => paintInfo(`lives: ${state.player.lives}`);

const updateSvgPath = (state: State) =>
  [clearPlayerPath, addPlayerPath, addEnemy, addHoles, paintLives].forEach(fn =>
    fn(state)
  );

const paintGame = updateSvgPath;
const paintGameOver = () => paintInfo('Game Over !!!');

export { paintGame, paintGameOver };

index.html

<div id="info"></div>
<svg width="200" height="200" id="svg_container">
  <style>
    .rxjs {
      font: 95px serif;
      fill: purple;
    }
  </style>
  <defs>
    <mask id="Mask" maskContentUnits="">
      <rect width="200" height="200" fill="white" opacity="1" />
      <path id="mask_path" />
    </mask>
  </defs>
  <text x="10" y="120" class="rxjs">RxJs</text>
  <rect width="200" height="200" mask="url(#Mask)" />
</svg>
<div>Use arrows to uncover image!!!</div>

Operators Used

Last updated