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