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

Example Code

Tetris Game

index.ts

1
// RxJS v6+
2
import { fromEvent, of, interval, combineLatest } from 'rxjs';
3
import {
4
finalize,
5
map,
6
pluck,
7
scan,
8
startWith,
9
takeWhile,
10
tap
11
} from 'rxjs/operators';
12
import { score, randomBrick, clearGame, initialState } from './game';
13
import { render, renderGameOver } from './html-renderer';
14
import { handleKeyPress, resetKey } from './keyboard';
15
import { collide } from './collision';
16
import { rotate } from './rotation';
17
import { BRICK } from './constants';
18
import { State, Brick, Key } from './interfaces';
19
20
const player$ = combineLatest(
21
of(randomBrick()),
22
of({ code: '' }),
23
fromEvent(document, 'keyup').pipe(
24
startWith({ code: undefined }),
25
pluck('code')
26
)
27
).pipe(
28
map(
29
([brick, key, keyCode]: [Brick, Key, string]) => (
30
(key.code = keyCode), [brick, key]
31
)
32
)
33
);
34
35
const state$ = interval(1000).pipe(
36
scan < number,
37
State > ((state, _) => (state.x++, state), initialState)
38
);
39
40
const game$ = combineLatest(state$, player$).pipe(
41
scan < [State, [Brick, Key]],
42
[State, [Brick, Key]] >
43
(([state, [brick, key]]) => (
44
(state = handleKeyPress(state, brick, key)),
45
(([newState, rotatedBrick]: [State, Brick]) => (
46
(state = newState), (brick = rotatedBrick)
47
))(rotate(state, brick, key)),
48
(([newState, collidedBrick]: [State, Brick]) => (
49
(state = newState), (brick = collidedBrick)
50
))(collide(state, brick)),
51
(state = score(state)),
52
resetKey(key),
53
[state, [brick, key]]
54
)),
55
tap(([state, [brick, key]]) => render(state, brick)),
56
takeWhile(([state, [brick, key]]) => !state.game[1].some(c => c === BRICK)),
57
finalize(renderGameOver)
58
);
59
60
game$.subscribe();
Copied!

game.ts

1
import { GAME_SIZE, EMPTY, BRICK } from './constants';
2
import { State } from './interfaces';
3
4
const bricks = [
5
[
6
[0, 0, 0],
7
[1, 1, 1],
8
[0, 0, 0]
9
],
10
[
11
[1, 1, 1],
12
[0, 1, 0],
13
[0, 1, 0]
14
],
15
[
16
[0, 1, 1],
17
[0, 1, 0],
18
[0, 1, 0]
19
],
20
[
21
[1, 1, 0],
22
[0, 1, 0],
23
[0, 1, 0]
24
],
25
[
26
[1, 1, 0],
27
[1, 1, 0],
28
[0, 0, 0]
29
]
30
];
31
32
export const clearGame = () =>
33
Array(GAME_SIZE)
34
.fill(EMPTY)
35
.map(e => Array(GAME_SIZE).fill(EMPTY));
36
export const updatePosition = (position: number, column: number) =>
37
position === 0 ? column : position;
38
export const validGame = (game: number[][]) =>
39
game.map(r => r.filter((_, i) => i < GAME_SIZE));
40
export const validBrick = (brick: number[][]) =>
41
brick.filter(e => e.some(b => b === BRICK));
42
export const randomBrick = () =>
43
bricks[Math.floor(Math.random() * bricks.length)];
44
45
export const score = (state: State): State =>
46
(scoreIndex =>
47
scoreIndex > -1
48
? ((state.score += 1),
49
state.game.splice(scoreIndex, 1),
50
(state.game = [Array(GAME_SIZE).fill(EMPTY), ...state.game]),
51
state)
52
: state)(state.game.findIndex(e => e.every(e => e === BRICK)));
53
54
export const initialState = {
55
game: clearGame(),
56
x: 0,
57
y: 0,
58
score: 0
59
};
Copied!

collision.ts

1
import { GAME_SIZE, BRICK, EMPTY } from './constants';
2
import { validBrick, validGame, updatePosition, randomBrick } from './game';
3
import { State, Brick } from './interfaces';
4
5
const isGoingToLevelWithExistingBricks = (
6
state: State,
7
brick: Brick
8
): boolean => {
9
const gameHeight = state.game.findIndex(r => r.some(c => c === BRICK));
10
const brickBottomX = state.x + brick.length - 1;
11
return gameHeight > -1 && brickBottomX + 1 > gameHeight;
12
};
13
14
const areAnyBricksColliding = (state: State, brick: Brick): boolean =>
15
validBrick(brick).some((r, i) =>
16
r.some((c, j) =>
17
c === EMPTY
18
? false
19
: ((x, y) => state.game[x][y] === c)(i + state.x, j + state.y)
20
)
21
);
22
23
const collideBrick = (
24
state: State,
25
brick: Brick,
26
isGoingToCollide: boolean
27
): State => {
28
const xOffset = isGoingToCollide ? 1 : 0;
29
validBrick(brick).forEach((r, i) => {
30
r.forEach(
31
(c, j) =>
32
(state.game[i + state.x - xOffset][j + state.y] = updatePosition(
33
state.game[i + state.x - xOffset][j + state.y],
34
c
35
))
36
);
37
});
38
state.game = validGame(state.game);
39
state.x = 0;
40
state.y = GAME_SIZE / 2 - 1;
41
return state;
42
};
43
44
export const collide = (state: State, brick: Brick): [State, Brick] => {
45
const isGoingToCollide =
46
isGoingToLevelWithExistingBricks(state, brick) &&
47
areAnyBricksColliding(state, brick);
48
49
const isOnBottom = state.x + validBrick(brick).length > GAME_SIZE - 1;
50
51
if (isGoingToCollide || isOnBottom) {
52
state = collideBrick(state, brick, isGoingToCollide);
53
brick = randomBrick();
54
}
55
56
return [state, brick];
57
};
Copied!

rotation.ts

1
import { GAME_SIZE, BRICK_SIZE, EMPTY } from './constants';
2
import { State, Brick, Key } from './interfaces';
3
4
const rightOffsetAfterRotation = (
5
state: State,
6
brick: Brick,
7
rotatedBrick: Brick
8
) =>
9
state.y + rotatedBrick.length === GAME_SIZE + 1 &&
10
brick.every(e => e[2] === EMPTY)
11
? 1
12
: 0;
13
14
const leftOffsetAfterRotation = (game: State) => (game.y < 0 ? 1 : 0);
15
const emptyBrick = (): Brick =>
16
Array(BRICK_SIZE)
17
.fill(EMPTY)
18
.map(e => Array(BRICK_SIZE).fill(EMPTY));
19
20
const rotateBrick = (
21
state: State,
22
brick: Brick,
23
rotatedBrick: Brick
24
): [State, Brick] => (
25
brick.forEach((r, i) =>
26
r.forEach((c, j) => (rotatedBrick[j][brick[0].length - 1 - i] = c))
27
),
28
(state.y -= rightOffsetAfterRotation(state, brick, rotatedBrick)),
29
(state.y += leftOffsetAfterRotation(state)),
30
[state, rotatedBrick]
31
);
32
33
export const rotate = (state: State, brick: Brick, key: Key): [State, Brick] =>
34
key.code === 'ArrowUp'
35
? rotateBrick(state, brick, emptyBrick())
36
: [state, brick];
Copied!

keyboard.ts

1
import { GAME_SIZE } from './constants';
2
import { State, Brick, Key } from './interfaces';
3
4
const xOffset = (brick: Brick, columnIndex: number) =>
5
brick.every(e => e[columnIndex] === 0) ? 1 : 0;
6
7
export const handleKeyPress = (state: State, brick: Brick, key: Key): State => (
8
(state.x += key.code === 'ArrowDown' ? 1 : 0),
9
(state.y +=
10
key.code === 'ArrowLeft' && state.y > 0 - xOffset(brick, 0)
11
? -1
12
: key.code === 'ArrowRight' && state.y < GAME_SIZE - 3 + xOffset(brick, 2)
13
? 1
14
: 0),
15
state
16
);
17
18
export const resetKey = key => (key.code = undefined);
Copied!

html-renderer.ts

1
import { BRICK } from './constants';
2
import { State, Brick } from './interfaces';
3
import { updatePosition, validGame, validBrick, clearGame } from './game';
4
5
const createElem = (column: number): HTMLElement =>
6
(elem => (
7
(elem.style.display = 'inline-block'),
8
(elem.style.marginLeft = '3px'),
9
(elem.style.height = '6px'),
10
(elem.style.width = '6px'),
11
(elem.style['background-color'] = column === BRICK ? 'green' : 'aliceblue'),
12
elem
13
))(document.createElement('div'));
14
15
export const render = (state: State, brick: Brick): void => {
16
const gameFrame = clearGame();
17
18
state.game.forEach((r, i) => r.forEach((c, j) => (gameFrame[i][j] = c)));
19
validBrick(brick).forEach((r, i) =>
20
r.forEach(
21
(c, j) =>
22
(gameFrame[i + state.x][j + state.y] = updatePosition(
23
gameFrame[i + state.x][j + state.y],
24
c
25
))
26
)
27
);
28
29
document.body.innerHTML = `score: ${state.score} <br/>`;
30
validGame(gameFrame).forEach(r => {
31
const rowContainer = document.createElement('div');
32
r.forEach(c => rowContainer.appendChild(createElem(c)));
33
document.body.appendChild(rowContainer);
34
});
35
};
36
37
export const renderGameOver = () =>
38
(document.body.innerHTML += '<br/>GAME OVER!');
Copied!

interfaces.ts

1
export interface State {
2
game: number[][];
3
x: number;
4
y: number;
5
score: number;
6
}
7
8
export interface Key {
9
code: string;
10
}
11
12
export type Brick = number[][];
Copied!

constants.ts

1
export const GAME_SIZE = 10;
2
export const BRICK_SIZE = 3;
3
export const EMPTY = 0;
4
export const BRICK = 1;
Copied!

Operators Used