Uncover Image Game

By adamlubek

This recipe demonstrates RxJS implementation of Uncover Image Game.

Ultimate RxJS

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