Learn RxJS
Search…
Car Racing Game
By adamlubek
This recipe demonstrates RxJS implementation of Car Racing game.

Example Code

Car Racing

index.ts

1
// RxJS v6+
2
import {
3
interval,
4
fromEvent,
5
combineLatest,
6
of,
7
BehaviorSubject,
8
noop
9
} from 'rxjs';
10
import {
11
scan,
12
tap,
13
pluck,
14
startWith,
15
takeWhile,
16
finalize,
17
switchMap
18
} from 'rxjs/operators';
19
import { Car, Road, Player, Game } from './interfaces';
20
import { gameHeight, gameWidth, levelDuration } from './constants';
21
import { updateState } from './state';
22
import { render, renderGameOver } from './html-renderer';
23
24
const car = (x: number, y: number): Car => ({ x, y, scored: false });
25
const randomCar = (): Car =>
26
car(0, Math.floor(Math.random() * Math.floor(gameWidth)));
27
const gameSpeed$ = new BehaviorSubject(200);
28
29
const road$ = gameSpeed$.pipe(
30
switchMap(i =>
31
interval(i).pipe(
32
scan(
33
(road: Road, _: number): Road => (
34
(road.cars = road.cars.filter(c => c.x < gameHeight - 1)),
35
road.cars[0].x === gameHeight / 2
36
? road.cars.push(randomCar())
37
: noop,
38
road.cars.forEach(c => c.x++),
39
road
40
),
41
{ cars: [randomCar()] }
42
)
43
)
44
)
45
);
46
47
const keys$ = fromEvent(document, 'keyup').pipe(
48
startWith({ code: '' }),
49
pluck('code')
50
);
51
52
const player$ = keys$.pipe(
53
scan(
54
(player: Player, key: string): Player => (
55
(player.y +=
56
key === 'ArrowLeft' && player.y > 0
57
? -1
58
: key === 'ArrowRight' && player.y < gameWidth - 1
59
? 1
60
: 0),
61
player
62
),
63
{ y: 0 }
64
)
65
);
66
67
const state$ = of({
68
score: 1,
69
lives: 3,
70
level: 1,
71
duration: levelDuration,
72
interval: 200
73
});
74
75
const isNotGameOver = ([state]: Game) => state.lives > 0;
76
77
const game$ = combineLatest(state$, road$, player$).pipe(
78
scan(updateState(gameSpeed$)),
79
tap(render),
80
takeWhile(isNotGameOver),
81
finalize(renderGameOver)
82
);
83
84
game$.subscribe();
Copied!

state.ts

1
import { BehaviorSubject, noop } from 'rxjs';
2
import { Game } from './interfaces';
3
import { gameHeight, gameWidth, levelDuration } from './constants';
4
5
const handleScoreIncrease = ([state, road, player]: Game) =>
6
!road.cars[0].scored &&
7
road.cars[0].y !== player.y &&
8
road.cars[0].x === gameHeight - 1
9
? ((road.cars[0].scored = true), (state.score += 1))
10
: noop;
11
12
const handleCollision = ([state, road, player]: Game) =>
13
road.cars[0].x === gameHeight - 1 && road.cars[0].y === player.y
14
? (state.lives -= 1)
15
: noop;
16
17
const updateSpeed = ([state]: Game, gameSpeed: BehaviorSubject<number>) => (
18
(state.duration -= 10),
19
state.duration < 0
20
? ((state.duration = levelDuration * state.level),
21
state.level++,
22
(state.interval -= state.interval > 60 ? 20 : 0),
23
gameSpeed.next(state.interval))
24
: () => {}
25
);
26
27
export const updateState = (gameSpeed: BehaviorSubject<number>) => (
28
_,
29
game: Game
30
) => (
31
handleScoreIncrease(game),
32
handleCollision(game),
33
updateSpeed(game, gameSpeed),
34
game
35
);
Copied!

html-renderer.ts

1
import { Game } from './interfaces';
2
import { gameHeight, gameWidth, car, player } from './constants';
3
4
const createElem = (column: number) =>
5
(elem => (
6
(elem.style.display = 'inline-block'),
7
(elem.style.marginLeft = '3px'),
8
(elem.style.height = '12px'),
9
(elem.style.width = '6px'),
10
(elem.style.borderRadius = '40%'),
11
(elem.style['background-color'] =
12
column === car ? 'green' : column === player ? 'blue' : 'white'),
13
elem
14
))(document.createElement('div'));
15
16
export const render = ([state, road, playerPosition]: Game) =>
17
(renderFrame => (
18
road.cars.forEach(c => (renderFrame[c.x][c.y] = car)),
19
(document.getElementById(
20
'game'
21
).innerHTML = `Score: ${state.score} Lives: ${state.lives} Level: ${state.level}`),
22
(renderFrame[gameHeight - 1][playerPosition.y] = player),
23
renderFrame.forEach(r => {
24
const rowContainer = document.createElement('div');
25
r.forEach(c => rowContainer.appendChild(createElem(c)));
26
document.getElementById('game').appendChild(rowContainer);
27
})
28
))(
29
Array(gameHeight)
30
.fill(0)
31
.map(e => Array(gameWidth).fill(0))
32
);
33
34
export const renderGameOver = () =>
35
(document.getElementById('game').innerHTML += '<br/>GAME OVER!!!');
Copied!

interfaces.ts

1
export interface Car {
2
x: number;
3
y: number;
4
scored: boolean;
5
}
6
7
export interface Road {
8
cars: Car[];
9
}
10
11
export interface State {
12
score: number;
13
lives: number;
14
level: number;
15
duration: number;
16
interval: number;
17
}
18
19
export interface Player {
20
y: number;
21
}
22
23
export type Game = [State, Road, Player];
Copied!

constants.ts

1
export const gameHeight = 10;
2
export const gameWidth = 6;
3
4
export const levelDuration = 500;
5
6
export const car = 1;
7
export const player = 2;
Copied!

index.html

1
<style>
2
.road {
3
width: 100px;
4
height: 180px;
5
margin-top: 25px;
6
overflow: hidden;
7
position: absolute;
8
}
9
10
.dotted {
11
margin-top: -100px;
12
height: 300px;
13
border-left: 2px dashed lightgray;
14
position: absolute;
15
animation: road-moving 1s infinite linear;
16
}
17
18
@keyframes road-moving {
19
100% {
20
transform: translateY(100px);
21
}
22
}
23
</style>
24
25
<div class="road">
26
<div class="dotted" style="margin-left: 0px;"></div>
27
<div class="dotted" style="margin-left: 9px;"></div>
28
<div class="dotted" style="margin-left: 18px;"></div>
29
<div class="dotted" style="margin-left: 27px;"></div>
30
<div class="dotted" style="margin-left: 36px;"></div>
31
<div class="dotted" style="margin-left: 45px;"></div>
32
<div class="dotted" style="margin-left: 54px;"></div>
33
</div>
34
<div id="game"></div>
Copied!

Operators Used

Last modified 1yr ago