Learn RxJS
  • Introduction
  • Learn RxJS
    • Operators
      • Combination
        • combineAll
        • combineLatest
        • concat
        • concatAll
        • endWith
        • forkJoin
        • merge
        • mergeAll
        • pairwise
        • race
        • startWith
        • withLatestFrom
        • zip
      • Conditional
        • defaultIfEmpty
        • every
        • iif
        • sequenceEqual
      • Creation
        • ajax
        • create
        • defer
        • empty
        • from
        • fromEvent
        • generate
        • interval
        • of
        • range
        • throw
        • timer
      • Error Handling
        • catch / catchError
        • retry
        • retryWhen
      • Multicasting
        • publish
        • multicast
        • share
        • shareReplay
      • Filtering
        • audit
        • auditTime
        • debounce
        • debounceTime
        • distinct
        • distinctUntilChanged
        • distinctUntilKeyChanged
        • filter
        • find
        • first
        • ignoreElements
        • last
        • sample
        • single
        • skip
        • skipUntil
        • skipWhile
        • take
        • takeLast
        • takeUntil
        • takeWhile
        • throttle
        • throttleTime
      • Transformation
        • buffer
        • bufferCount
        • bufferTime
        • bufferToggle
        • bufferWhen
        • concatMap
        • concatMapTo
        • exhaustMap
        • expand
        • groupBy
        • map
        • mapTo
        • mergeMap / flatMap
        • mergeScan
        • partition
        • pluck
        • reduce
        • scan
        • switchMap
        • switchMapTo
        • toArray
        • window
        • windowCount
        • windowTime
        • windowToggle
        • windowWhen
      • Utility
        • tap / do
        • delay
        • delayWhen
        • dematerialize
        • finalize / finally
        • let
        • repeat
        • timeInterval
        • timeout
        • timeoutWith
        • toPromise
      • Full Listing
    • Subjects
      • AsyncSubject
      • BehaviorSubject
      • ReplaySubject
      • Subject
    • Recipes
      • Alphabet Invasion Game
      • Battleship Game
      • Breakout Game
      • Car Racing Game
      • Catch The Dot Game
      • Click Ninja Game
      • Flappy Bird Game
      • Game Loop
      • Horizontal Scroll Indicator
      • Http Polling
      • Lockscreen
      • Matrix Digital Rain
      • Memory Game
      • Mine Sweeper Game
      • Platform Jumper Game
      • Progress Bar
      • Save Indicator
      • Smart Counter
      • Space Invaders Game
      • Stop Watch
      • Swipe To Refresh
      • Tank Battle Game
      • Tetris Game
      • Type Ahead
      • Uncover Image Game
    • Concepts
      • RxJS Primer
      • Get started transforming streams with map, pluck, and mapTo
      • Time based operators comparison
      • RxJS v5 -> v6 Upgrade
Powered by GitBook
On this page
  • Example Code
  • Operators Used
  1. Learn RxJS
  2. Recipes

Uncover Image Game

PreviousType AheadNextConcepts

Last updated 1 year ago

By

This recipe demonstrates RxJS implementation of Uncover Image Game.

Example Code

( )

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

finalize
fromEvent
interval
pluck
scan
startWith
takeWhile
tap
withLatestFrom
adamlubek
StackBlitz