Car Racing Game

By adamlubek

This recipe demonstrates RxJs implementation of Car Racing game.

Example Code

( StackBlitz )

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

results matching ""

    No results matching ""