Car Racing Game

By adamlubek

This recipe demonstrates RxJS implementation of Car Racing game.

Ultimate RxJS

Example Code

( StackBlitz )

Car Racing

index.ts

// RxJS v6+
import {
interval,
fromEvent,
combineLatest,
of,
BehaviorSubject,
noop
} from 'rxjs';
import {
scan,
tap,
pluck,
startWith,
takeWhile,
finalize,
switchMap
} from 'rxjs/operators';
import { Car, Road, Player, Game } from './interfaces';
import { gameHeight, gameWidth, levelDuration } from './constants';
import { updateState } from './state';
import { render, renderGameOver } from './html-renderer';
const car = (x: number, y: number): Car => ({ x, y, scored: false });
const randomCar = (): Car =>
car(0, Math.floor(Math.random() * Math.floor(gameWidth)));
const gameSpeed$ = new BehaviorSubject(200);
const road$ = gameSpeed$.pipe(
switchMap(i =>
interval(i).pipe(
scan(
(road: Road, _: number): Road => (
(road.cars = road.cars.filter(c => c.x < gameHeight - 1)),
road.cars[0].x === gameHeight / 2
? road.cars.push(randomCar())
: noop,
road.cars.forEach(c => c.x++),
road
),
{ cars: [randomCar()] }
)
)
)
);
const keys$ = fromEvent(document, 'keyup').pipe(
startWith({ code: '' }),
pluck('code')
);
const player$ = keys$.pipe(
scan(
(player: Player, key: string): Player => (
(player.y +=
key === 'ArrowLeft' && player.y > 0
? -1
: key === 'ArrowRight' && player.y < gameWidth - 1
? 1
: 0),
player
),
{ y: 0 }
)
);
const state$ = of({
score: 1,
lives: 3,
level: 1,
duration: levelDuration,
interval: 200
});
const isNotGameOver = ([state]: Game) => state.lives > 0;
const game$ = combineLatest(state$, road$, player$).pipe(
scan(updateState(gameSpeed$)),
tap(render),
takeWhile(isNotGameOver),
finalize(renderGameOver)
);
game$.subscribe();

state.ts

import { BehaviorSubject, noop } from 'rxjs';
import { Game } from './interfaces';
import { gameHeight, gameWidth, levelDuration } from './constants';
const handleScoreIncrease = ([state, road, player]: Game) =>
!road.cars[0].scored &&
road.cars[0].y !== player.y &&
road.cars[0].x === gameHeight - 1
? ((road.cars[0].scored = true), (state.score += 1))
: noop;
const handleCollision = ([state, road, player]: Game) =>
road.cars[0].x === gameHeight - 1 && road.cars[0].y === player.y
? (state.lives -= 1)
: noop;
const updateSpeed = ([state]: Game, gameSpeed: BehaviorSubject<number>) => (
(state.duration -= 10),
state.duration < 0
? ((state.duration = levelDuration * state.level),
state.level++,
(state.interval -= state.interval > 60 ? 20 : 0),
gameSpeed.next(state.interval))
: () => {}
);
export const updateState = (gameSpeed: BehaviorSubject<number>) => (
_,
game: Game
) => (
handleScoreIncrease(game),
handleCollision(game),
updateSpeed(game, gameSpeed),
game
);

html-renderer.ts

import { Game } from './interfaces';
import { gameHeight, gameWidth, car, player } from './constants';
const createElem = (column: number) =>
(elem => (
(elem.style.display = 'inline-block'),
(elem.style.marginLeft = '3px'),
(elem.style.height = '12px'),
(elem.style.width = '6px'),
(elem.style.borderRadius = '40%'),
(elem.style['background-color'] =
column === car ? 'green' : column === player ? 'blue' : 'white'),
elem
))(document.createElement('div'));
export const render = ([state, road, playerPosition]: Game) =>
(renderFrame => (
road.cars.forEach(c => (renderFrame[c.x][c.y] = car)),
(document.getElementById(
'game'
).innerHTML = `Score: ${state.score} Lives: ${state.lives} Level: ${state.level}`),
(renderFrame[gameHeight - 1][playerPosition.y] = player),
renderFrame.forEach(r => {
const rowContainer = document.createElement('div');
r.forEach(c => rowContainer.appendChild(createElem(c)));
document.getElementById('game').appendChild(rowContainer);
})
))(
Array(gameHeight)
.fill(0)
.map(e => Array(gameWidth).fill(0))
);
export const renderGameOver = () =>
(document.getElementById('game').innerHTML += '<br/>GAME OVER!!!');

interfaces.ts

export interface Car {
x: number;
y: number;
scored: boolean;
}
export interface Road {
cars: Car[];
}
export interface State {
score: number;
lives: number;
level: number;
duration: number;
interval: number;
}
export interface Player {
y: number;
}
export type Game = [State, Road, Player];

constants.ts

export const gameHeight = 10;
export const gameWidth = 6;
export const levelDuration = 500;
export const car = 1;
export const player = 2;

index.html

<style>
.road {
width: 100px;
height: 180px;
margin-top: 25px;
overflow: hidden;
position: absolute;
}
.dotted {
margin-top: -100px;
height: 300px;
border-left: 2px dashed lightgray;
position: absolute;
animation: road-moving 1s infinite linear;
}
@keyframes road-moving {
100% {
transform: translateY(100px);
}
}
</style>
<div class="road">
<div class="dotted" style="margin-left: 0px;"></div>
<div class="dotted" style="margin-left: 9px;"></div>
<div class="dotted" style="margin-left: 18px;"></div>
<div class="dotted" style="margin-left: 27px;"></div>
<div class="dotted" style="margin-left: 36px;"></div>
<div class="dotted" style="margin-left: 45px;"></div>
<div class="dotted" style="margin-left: 54px;"></div>
</div>
<div id="game"></div>

Operators Used