Learn RxJS
Search…
Uncover Image Game
By adamlubek
This recipe demonstrates RxJS implementation of Uncover Image Game.

Example Code

index.ts

1
// RxJS v6+
2
import { interval } from 'rxjs';
3
import { finalize, scan, takeWhile, tap, withLatestFrom } from 'rxjs/operators';
4
import { keyboardEvents$ } from './keyboard';
5
import { initialGame, updateGame, isGameOn } from './game';
6
import { paintGame, paintGameOver } from './html-renderer';
7
8
interval(15)
9
.pipe(
10
withLatestFrom(keyboardEvents$),
11
scan(updateGame, initialGame),
12
tap(paintGame),
13
takeWhile(isGameOn),
14
finalize(paintGameOver)
15
)
16
.subscribe();
Copied!

game.ts

1
import { noop } from 'rxjs';
2
import { Enemy, State, Move } from './interfaces';
3
import { size } from './constants';
4
import { newPlayerFrom, movePlayer } from './player';
5
import { newEnemiesFrom } from './enemy';
6
7
const intersect = (state: State): State => (
8
state.moves.some((m: Move) =>
9
state.enemies.some(e => m.x === e.x && m.y === e.y)
10
)
11
? ((state.player.lives -= 1),
12
(state.player.x = 0),
13
(state.player.y = 0),
14
(state.moves = []))
15
: noop,
16
state
17
);
18
19
const initialGame: State = {
20
player: { x: 0, y: 0, lives: 3 },
21
enemies: [
22
{ x: 10, y: 10, moveDuration: 0, dirX: 1, dirY: 1 },
23
{ x: 50, y: 50, moveDuration: 0, dirX: -1, dirY: 1 }
24
],
25
key: '',
26
moves: [],
27
corners: []
28
};
29
30
const updateGame = (state: State, [_, key]: [number, string]): State => (
31
(state.enemies = newEnemiesFrom(state)),
32
(state.player = newPlayerFrom(state, key)),
33
(state = intersect(state)),
34
(state = movePlayer(state, key)),
35
(state.key = key),
36
state
37
);
38
39
const isGameOn = (state: State): boolean => state.player.lives > 0;
40
41
export { initialGame, updateGame, isGameOn };
Copied!

player.ts

1
import { noop } from 'rxjs';
2
import { Player, State, Move } from './interfaces';
3
import { size } from './constants';
4
import { keyToDirection } from './keyboard';
5
6
const up = 'ArrowUp';
7
const down = 'ArrowDown';
8
const left = 'ArrowLeft';
9
const right = 'ArrowRight';
10
11
const newPlayerFrom = (state: State, key: string): Player => (
12
(state.player.x += keyToDirection(key, down, up)),
13
(state.player.y += keyToDirection(key, right, left)),
14
state.player.x < 0 ? (state.player.x = 0) : noop,
15
state.player.x > size ? (state.player.x = size) : noop,
16
state.player.y < 0 ? (state.player.y = 0) : noop,
17
state.player.y > size ? (state.player.y = size) : noop,
18
state.player
19
);
20
21
const getEnclosedArea = (state: State): Move[] =>
22
state.moves.length <= 1
23
? []
24
: [
25
...state.moves
26
.slice(
27
state.moves.findIndex(
28
e => e.x === state.player.x && e.y === state.player.y
29
)
30
)
31
.filter(e => e.dirChange),
32
state.moves.pop()
33
];
34
35
const movePlayer = (state: State, key: string): State => (
36
state.moves.some(e => e.x === state.player.x && e.y === state.player.y)
37
? ((state.corners = getEnclosedArea(state)), (state.moves = []))
38
: state.moves.push({
39
x: state.player.x,
40
y: state.player.y,
41
dirChange: state.key !== key
42
}),
43
state
44
);
45
46
export { newPlayerFrom, movePlayer };
Copied!

enemy.ts

1
import { noop } from 'rxjs';
2
import { Enemy, State } from './interfaces';
3
import { size } from './constants';
4
5
const newEnemiesFrom = (state: State): Enemy[] => (
6
state.enemies.forEach(
7
e => (
8
e.x <= 0 || e.x > size ? (e.dirX *= -1) : noop,
9
e.y <= 0 || e.y > size ? (e.dirY *= -1) : noop,
10
(e.x += e.dirX),
11
(e.y += e.dirY),
12
(e.moveDuration += 1),
13
e.moveDuration > 100
14
? ((e.dirX = Math.random() > 0.5 ? 1 : -1),
15
(e.dirY = Math.random() > 0.5 ? 1 : -1),
16
(e.moveDuration = 0))
17
: noop
18
)
19
),
20
state.enemies
21
);
22
23
export { newEnemiesFrom };
Copied!

keyboard.ts

1
import { fromEvent } from 'rxjs';
2
import { pluck, startWith } from 'rxjs/operators';
3
4
const positionChangeUnit = 2;
5
6
export const keyboardEvents$ = fromEvent(document, 'keydown').pipe(
7
pluck < KeyboardEvent,
8
string > 'code',
9
startWith('')
10
);
11
12
export const keyToDirection = (
13
key: string,
14
key1: string,
15
key2: string
16
): number =>
17
key === key1 ? positionChangeUnit : key === key2 ? -positionChangeUnit : 0;
Copied!

interfaces.ts

1
interface GameObject {
2
x: number;
3
y: number;
4
}
5
6
interface Player extends GameObject {
7
lives: number;
8
}
9
10
interface Enemy extends GameObject {
11
moveDuration: number;
12
dirX: number;
13
dirY: number;
14
}
15
16
interface Move extends GameObject {
17
dirChange: boolean;
18
}
19
20
interface State {
21
player: Player;
22
enemies: Enemy[];
23
key: string;
24
moves: Move[];
25
corners: Move[];
26
}
27
28
export { GameObject, Player, Enemy, State };
Copied!

constants.ts

1
const size = 200;
2
3
export { size };
Copied!

html-renderer.ts

1
import { State, GameObject } from './interfaces';
2
3
const clearPlayerPath = _ =>
4
document
5
.querySelectorAll('circle')
6
.forEach(e => document.querySelector('#svg_container').removeChild(e));
7
8
const addCircleColored = (color: string) => (e: GameObject) => {
9
const circle = document.createElementNS(
10
'http://www.w3.org/2000/svg',
11
'circle'
12
);
13
circle.setAttribute('cx', e.y);
14
circle.setAttribute('cy', e.x);
15
circle.setAttribute('r', '2');
16
circle.setAttribute('stroke', color);
17
circle.setAttribute('strokeWidth', '1');
18
document.querySelector('#svg_container').appendChild(circle);
19
};
20
21
const addPlayerPath = (state: State) =>
22
state.moves.forEach(addCircleColored('gray'));
23
const addEnemy = (state: State) =>
24
state.enemies.forEach(addCircleColored('red'));
25
26
const addHoles = (state: State) => {
27
if (!state.corners.length) {
28
return;
29
}
30
31
const createPathFromCorners = (a, c) =>
32
(a += `${a.endsWith('Z') ? 'M' : 'L'} ${c.y} ${c.x} ${
33
c.dirChange ? '' : 'Z'
34
}`);
35
const newPath =
36
`M${state.corners[0].y} ${state.corners[0].x}` +
37
state.corners.reduce(createPathFromCorners, '');
38
const maskPath = document.querySelector('#mask_path');
39
40
const currentPath = maskPath.getAttribute('d');
41
const path = newPath + ' ' + currentPath;
42
43
maskPath.setAttribute('d', path);
44
};
45
46
const paintInfo = (text: string) =>
47
(document.querySelector('#info').innerHTML = text);
48
const paintLives = (state: State) => paintInfo(`lives: ${state.player.lives}`);
49
50
const updateSvgPath = (state: State) =>
51
[clearPlayerPath, addPlayerPath, addEnemy, addHoles, paintLives].forEach(fn =>
52
fn(state)
53
);
54
55
const paintGame = updateSvgPath;
56
const paintGameOver = () => paintInfo('Game Over !!!');
57
58
export { paintGame, paintGameOver };
Copied!

index.html

1
<div id="info"></div>
2
<svg width="200" height="200" id="svg_container">
3
<style>
4
.rxjs {
5
font: 95px serif;
6
fill: purple;
7
}
8
</style>
9
<defs>
10
<mask id="Mask" maskContentUnits="">
11
<rect width="200" height="200" fill="white" opacity="1" />
12
<path id="mask_path" />
13
</mask>
14
</defs>
15
<text x="10" y="120" class="rxjs">RxJs</text>
16
<rect width="200" height="200" mask="url(#Mask)" />
17
</svg>
18
<div>Use arrows to uncover image!!!</div>
Copied!

Operators Used

Last modified 1yr ago