RxJS 배우기
  • 소개
  • RxJS 배우기
    • 연산자
      • Combination
        • combineAll
        • combineLatest
        • concat
        • concatAll
        • endWith
        • forkJoin
        • merge
        • mergeAll
        • pairwise
        • race
        • startWith
        • withLatestFrom
        • zip
      • Conditional
        • defaultIfEmpty
        • every
        • iif
        • sequenceEqual
      • Creation
        • ajax
        • create
        • defer
        • empty
        • from
        • fromEvent
        • generate
        • interval
        • of
        • range
        • throw
        • timer
      • Error Handling
        • catch / catchError
        • retry
        • retryWhen
      • Multicasting
        • publish
        • multicast
        • share
        • shareReplay
      • Filtering
        • audit
        • auditTime
        • debounce
        • debounceTime
        • distinct
        • distinctUntilChanged
        • distinctUntilKeyChanged
        • filter
        • find
        • first
        • ignoreElements
        • last
        • sample
        • single
        • skip
        • skipUntil
        • skipWhile
        • take
        • takeLast
        • takeUntil
        • takeWhile
        • throttle
        • throttleTime
      • Transformation
        • buffer
        • bufferCount
        • bufferTime
        • bufferToggle
        • bufferWhen
        • concatMap
        • concatMapTo
        • exhaustMap
        • expand
        • groupBy
        • map
        • mapTo
        • mergeMap / flatMap
        • mergeScan
        • partition
        • pluck
        • reduce
        • scan
        • switchMap
        • switchMapTo
        • toArray
        • window
        • windowCount
        • windowTime
        • windowToggle
        • windowWhen
      • Utility
        • tap / do
        • delay
        • delayWhen
        • dematerialize
        • finalize / finally
        • let
        • repeat
        • timeInterval
        • timeout
        • timeoutWith
        • toPromise
      • 전체 목록
    • Subjects
      • AsyncSubject
      • BehaviorSubject
      • ReplaySubject
      • Subject
    • 사용예시
      • Alphabet Invasion Game
      • Battleship Game
      • Breakout Game
      • Car Racing Game
      • Catch The Dot Game
      • Click Ninja Game
      • Flappy Bird Game
      • Game Loop
      • Horizontal Scroll Indicator
      • Http Polling
      • Lockscreen
      • Matrix Digital Rain
      • Memory Game
      • Mine Sweeper Game
      • Platform Jumper Game
      • Progress Bar
      • Save Indicator
      • Smart Counter
      • Space Invaders Game
      • Stop Watch
      • Swipe To Refresh
      • Tank Battle Game
      • Tetris Game
      • Type Ahead
      • Uncover Image Game
    • 개념
      • RxJS 입문서
      • RxJS v5 -> v6 업그레이드
      • 시간 기반의 연산자 비교
      • 연산자 imports의 이해
Powered by GitBook
On this page
  • Example Code
  • index.ts
  • game.ts
  • player.ts
  • enemy.ts
  • keyboard.ts
  • interfaces.ts
  • constants.ts
  • html-renderer.ts
  • index.html
  • Operators Used

Was this helpful?

  1. RxJS 배우기
  2. 사용예시

Uncover Image Game

PreviousType AheadNext개념

Last updated 5 years ago

Was this helpful?

By

This recipe demonstrates RxJS implementation of Uncover Image Game.

Example Code

( )

index.ts

// RxJS v6+
import { interval } from 'rxjs';
import { finalize, scan, takeWhile, tap, withLatestFrom } from 'rxjs/operators'; 
import { keyboardEvents$ } from './keyboard';
import { initialGame, updateGame, isGameOn } from './game';
import { paintGame, paintGameOver } from './html-renderer';

interval(15)
  .pipe(
    withLatestFrom(keyboardEvents$),
    scan(updateGame, initialGame),
    tap(paintGame),
    takeWhile(isGameOn),
    finalize(paintGameOver)
  )
  .subscribe();

game.ts

import { noop } from 'rxjs';
import { Enemy, State, Move } from './interfaces';
import { size } from './constants';
import { newPlayerFrom, movePlayer } from './player';
import { newEnemiesFrom } from './enemy';

const intersect = (state: State): State => (
  state.moves.some((m: Move) => 
    state.enemies.some(e => m.x === e.x && m.y === e.y)
  ) 
    ? (
      state.player.lives -= 1,
      state.player.x = 0,
      state.player.y = 0,
      state.moves = []
    )
    : noop,
  state
);

const initialGame: State = {
  player: {x: 0, y: 0, lives: 3},
  enemies: [
    {x: 10, y: 10, moveDuration: 0, dirX: 1, dirY: 1}, 
    {x: 50, y: 50, moveDuration: 0, dirX: -1, dirY: 1}
  ],
  key: '',
  moves: [],
  corners: []
};

const updateGame = (state: State, [_, key]: [number, string]): State => (
  state.enemies = newEnemiesFrom(state),
  state.player = newPlayerFrom(state, key),
  state = intersect(state),
  state = movePlayer(state, key),
  state.key = key,
  state
);

const isGameOn = (state: State): boolean => state.player.lives > 0

export { initialGame, updateGame, isGameOn }

player.ts

import { noop } from 'rxjs';
import { Player, State, Move } from './interfaces';
import { size } from './constants';
import { keyToDirection } from './keyboard';

const up = 'ArrowUp';
const down = 'ArrowDown';
const left = 'ArrowLeft';
const right = 'ArrowRight';

const newPlayerFrom = (state: State, key: string): Player => (
  state.player.x += keyToDirection(key, down, up),
  state.player.y += keyToDirection(key, right, left),
  state.player.x < 0 ? state.player.x = 0 : noop,
  state.player.x > size ? state.player.x = size : noop,
  state.player.y < 0 ? state.player.y = 0 : noop,
  state.player.y > size ? state.player.y = size : noop,
  state.player
);

const getEnclosedArea = (state: State): Move[] => 
  state.moves.length <= 1
    ? []
    : [
        ...state.moves
          .slice(state.moves.findIndex(e => 
            e.x === state.player.x && e.y === state.player.y))
          .filter(e => e.dirChange),
        state.moves.pop()
      ];

const movePlayer = (state: State, key: string): State => (
  state.moves.some(e => e.x === state.player.x && e.y === state.player.y) 
    ? (
      state.corners = getEnclosedArea(state),
      state.moves = []
    )
    : state.moves.push({
        x: state.player.x,
        y: state.player.y,
        dirChange: state.key !== key 
      }),
  state
);

export { newPlayerFrom, movePlayer }

enemy.ts

import { noop } from 'rxjs';
import { Enemy, State } from './interfaces';
import { size } from './constants';

const newEnemiesFrom = (state: State): Enemy[] => (
  state.enemies.forEach(e => ( 
    e.x <= 0 || e.x > size ? e.dirX *= -1 : noop,
    e.y <= 0 || e.y > size ? e.dirY *= -1 : noop,
    e.x += e.dirX,
    e.y += e.dirY,
    e.moveDuration += 1,
    e.moveDuration > 100 
      ? (
        e.dirX = Math.random() > 0.5 ? 1 : -1,
        e.dirY = Math.random() > 0.5 ? 1 : -1,
        e.moveDuration = 0
      ) 
      : noop
  )),
  state.enemies
);

export { newEnemiesFrom }

keyboard.ts

import { fromEvent } from "rxjs";
import { pluck, startWith } from "rxjs/operators";

const positionChangeUnit = 2;

export const keyboardEvents$ = fromEvent(document, "keydown")
  .pipe(
    pluck<KeyboardEvent, string>("code"),
    startWith('')
  );

export const keyToDirection = (key: string, key1: string, key2: string): number => 
  key === key1
    ? positionChangeUnit
    : key === key2
      ? -positionChangeUnit
      : 0;

interfaces.ts

interface GameObject {
  x: number;
  y: number;
}

interface Player extends GameObject {
  lives: number;
}

interface Enemy extends GameObject {
  moveDuration: number;
  dirX: number; 
  dirY: number;
}

interface Move extends GameObject {
  dirChange: boolean;
}

interface State {
  player: Player;
  enemies: Enemy[];
  key: string;
  moves: Move[],
  corners: Move[]
}

export { GameObject, Player, Enemy, State }

constants.ts

const size = 200;

export { size }

html-renderer.ts

import { State, GameObject } from './interfaces';

const clearPlayerPath = _ => 
  document
    .querySelectorAll('circle')
    .forEach(e => document.querySelector('#svg_container').removeChild(e));

const addCircleColored = (color: string) => (e: GameObject) => {
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  circle.setAttribute('cx', e.y);
  circle.setAttribute('cy', e.x);
  circle.setAttribute('r', '2');
  circle.setAttribute('stroke', color);
  circle.setAttribute('strokeWidth', '1');
  document.querySelector('#svg_container').appendChild(circle);
}

const addPlayerPath = (state: State) => 
  state.moves.forEach(addCircleColored('gray'));
const addEnemy = (state: State) => 
  state.enemies.forEach(addCircleColored('red'));

const addHoles = (state: State) => {
  if (!state.corners.length) {
    return;
  }

  const createPathFromCorners = (a, c) => a += 
    `${a.endsWith('Z') ? 'M' : 'L'} ${c.y} ${c.x} ${c.dirChange ? '' : 'Z'}`;
  const newPath = `M${state.corners[0].y} ${state.corners[0].x}` 
    + state.corners.reduce(createPathFromCorners, '');
  const maskPath = document.querySelector('#mask_path');

  const currentPath = maskPath.getAttribute('d');
  const path = newPath + ' ' + currentPath;

  maskPath.setAttribute('d', path);
}

const paintInfo = (text: string) => 
  document.querySelector('#info').innerHTML = text;
const paintLives = (state: State) => 
  paintInfo(`lives: ${state.player.lives}`);

const updateSvgPath = (state: State) => 
  [clearPlayerPath, addPlayerPath, addEnemy, addHoles, paintLives]
    .forEach(fn => fn(state));

const paintGame = updateSvgPath;
const paintGameOver = () => paintInfo('Game Over !!!');

export { paintGame, paintGameOver }

index.html

<div id="info"></div>
<svg width="200" height="200" id="svg_container">
  <style>
    .rxjs { font: 95px serif; fill: purple; }
  </style>
  <defs>
    <mask id="Mask" maskContentUnits="">
      <rect width="200" height="200" fill="white" opacity="1"/>
      <path id="mask_path"/>
    </mask>
  </defs>
  <text x="10" y="120" class="rxjs">RxJs</text>
  <rect width="200" height="200" mask="url(#Mask)"/>
</svg>
<div>Use arrows to uncover image!!!</div>

Operators Used

adamlubek
StackBlitz
finalize
fromEvent
interval
pluck
scan
startWith
takeWhile
tap
withLatestFrom