Platform Jumper Game

By adamlubek

This recipe demonstrates RxJS implementation of Platform Jumper game.

Example Code

( StackBlitz )

index.ts

// RxJS v6+
import { interval, of, fromEvent, combineLatest } from 'rxjs';
import { scan, tap, startWith, pluck, switchMap, takeWhile } from 'rxjs/operators';
import { gameSize } from './constants';
import { render } from './html-renderer';
import { Player, Platform } from './interfaces';
import { updatePlayer, updatePlatforms, initialPlatforms, initialPlayer, handleCollisions, handleKeypresses } from './game';

const gameSpeed = 500;

const platforms$ = interval(gameSpeed)
  .pipe(
    scan<number, Platform[]>(updatePlatforms, initialPlatforms)
  );

const keys$ = (initialPlayer: Player) => fromEvent(document, 'keydown')
  .pipe(
    startWith({ key: '' }),
    pluck('key'),
    scan<string, Player>(
      (plyr: Player, key: string) => handleKeypresses(plyr, key),
      initialPlayer
    )
  );

const player$ = of(initialPlayer())
  .pipe(
    switchMap(p => combineLatest(interval(gameSpeed / 4), keys$(p))
      .pipe(
        scan<[number, Player], Player>((_, [__, player]) => updatePlayer(player))
      )),
  );

combineLatest(player$, platforms$)
  .pipe(
    scan<[Player, Platform[]], [Player, Platform[]]>(
      (_, [player, platforms]) => handleCollisions([player, platforms])),
    tap(render),
    takeWhile(([player, platforms]) => player.lives > 0)
  )
  .subscribe()

game.ts

import { Player, Platform } from './interfaces';
import { gameSize } from './constants';

const newPlatform = (x, y): Platform => ({ x, y, scored: false });
const newPlayer = (x, y, jumpValue, score, lives): Player => ({
  x,
  y,
  jumpValue,
  canJump: false,
  score: score,
  lives: lives
});
const startingY = 4;
export const initialPlayer = (): Player => newPlayer(0, startingY, 0, 0, 3);
export const initialPlatforms = [newPlatform(gameSize / 2, startingY)];

const random = y => {
  let min = Math.ceil(y - 4);
  let max = Math.floor(y + 4);
  min = min < 0 ? 0 : min;
  max = max > gameSize - 1 ? gameSize - 1 : max;

  return Math.floor(Math.random() * (max - min + 1)) + min;
};

export const updatePlatforms = (platforms: Platform[]): Platform[] => (
  platforms[platforms.length - 1].x > gameSize / 5
    ? platforms.push(newPlatform(1, random(platforms[platforms.length - 1].y)))
    : () => {},
  platforms.filter(e => e.x < gameSize - 1).map(e => newPlatform(e.x + 1, e.y))
);

export const handleKeypresses = (player: Player, key: string) =>
  key === 'ArrowRight'
    ? newPlayer(
        player.x,
        player.y + (player.y < gameSize - 1 ? 1 : 0),
        player.jumpValue,
        player.score,
        player.lives
      )
    : key === 'ArrowLeft'
    ? newPlayer(
        player.x,
        player.y - (player.y > 0 ? 1 : 0),
        player.jumpValue,
        player.score,
        player.lives
      )
    : key === 'ArrowUp'
    ? newPlayer(
        player.x,
        player.y,
        player.x === gameSize - 1 || player.canJump ? 6 : 0,
        player.score,
        player.lives
      )
    : player;

export const updatePlayer = (player: Player): Player => (
  (player.jumpValue -= player.jumpValue > 0 ? 1 : 0),
  (player.x -= player.x - 3 > 0 ? player.jumpValue : 0),
  (player.x += player.x < gameSize - 1 ? 1 : 0),
  player.x === gameSize - 1 ? ((player.lives -= 1), (player.x = 1)) : () => {},
  player
);

const handleCollidingPlatform = (
  collidingPlatform: Platform,
  player: Player
) => {
  if (player.canJump) {
    return;
  }

  if (!collidingPlatform) {
    player.canJump = false;
    return;
  }

  if (!collidingPlatform.scored) {
    player.score += 1;
  }
  collidingPlatform.scored = true;

  player.canJump = true;
};

export const handleCollisions = ([player, platforms]: [Player, Platform[]]): [
  Player,
  Platform[]
] => (
  handleCollidingPlatform(
    platforms.find(p => p.x - 1 === player.x && p.y === player.y),
    player
  ),
  (player.x = player.canJump
    ? (collidingPlatforms =>
        collidingPlatforms.length
          ? (platform => platform.x - 1)(
              collidingPlatforms[collidingPlatforms.length - 1]
            )
          : player.x)(
        platforms.filter(p => p.y === player.y && p.x >= player.x)
      )
    : player.x),
  [player, platforms]
);

interfaces.ts

export interface Player {
  x: number;
  y: number;
  jumpValue: number;
  canJump: boolean;
  score: number;
  lives: number;
}

export interface Platform {
  x: number;
  y: number;
  scored: boolean;
}

constants.ts

export const gameSize = 20;

html-renderer.ts

import { gameSize } from './constants';
import { Player, Platform } from './interfaces';

export const render = ([player, platforms]: [Player, Platform[]]) => {
  document.body.innerHTML = `Lives: ${player.lives} Score: ${player.score} </br>`;

  const game = Array(gameSize)
    .fill(0)
    .map(_ => Array(gameSize).fill(0));
  game[player.x][player.y] = '*';
  platforms.forEach(p => (game[p.x][p.y] = '_'));

  game.forEach(r => {
    r.forEach(c => (document.body.innerHTML += c === 0 ? '...' : c));
    document.body.innerHTML += '<br/>';
  });
};

Operators Used

Last updated