Learn RxJS
Search…
Space Invaders Game
By adamlubek
This recipe demonstrates RxJS implementation of Space Invaders Game.

Example Code

Space Invaders

index.ts

1
// RxJS v6+
2
import { fromEvent, interval } from 'rxjs';
3
import {
4
map,
5
scan,
6
tap,
7
startWith,
8
withLatestFrom,
9
takeUntil,
10
repeat
11
} from 'rxjs/operators';
12
import { gameUpdate, initialState } from './game';
13
import { State, Input } from './interfaces';
14
import { paint } from './html-renderer';
15
16
const spaceInvaders$ = interval(100).pipe(
17
withLatestFrom(
18
fromEvent(document, 'keydown').pipe(
19
startWith({ code: '' }),
20
takeUntil(fromEvent(document, 'keyup')),
21
repeat()
22
)
23
),
24
map(([intrvl, event]: [number, KeyboardEvent]): Input => ({
25
dlta: intrvl,
26
key: event.code
27
})),
28
scan(gameUpdate, initialState),
29
tap(e => paint(e.game, e.playerLives, e.score, e.isGameOver))
30
);
31
32
spaceInvaders$.subscribe();
Copied!

game.ts

1
import { State, Input } from './interfaces';
2
import { empty, player, invader, shot, noOfInvadersRows } from './constants';
3
4
const gameObject = (x, y) => ({ x: x, y: y });
5
const gameSize = 20;
6
const clearGame = () =>
7
Array(gameSize)
8
.fill(empty)
9
.map(e => Array(gameSize).fill(empty));
10
11
const createInvaders = () =>
12
Array.from(Array(noOfInvadersRows).keys()).reduce(
13
(invds, row) => [...invds, ...createRowOfInvaders(row)],
14
[]
15
);
16
const createRowOfInvaders = row =>
17
Array.from(Array(gameSize / 2).keys())
18
.filter(e => (row % 2 === 0 ? e % 2 === 0 : e % 2 !== 0))
19
.map(e => gameObject(row, e + 4));
20
21
const invadersDirection = (state: State): number =>
22
state.invaders.length && state.invaders[0].y <= 0
23
? 1
24
: state.invaders.length &&
25
state.invaders[state.invaders.length - 1].y >= gameSize - 1
26
? -1
27
: state.invadersDirY;
28
29
const drawGame = (state: State): number[][] => (
30
keepShipWithinGame(state),
31
(state.game = clearGame()),
32
(state.game[state.game.length - 1][state.shipY] = player),
33
state.invaders.forEach(i => (state.game[i.x][i.y] = invader)),
34
state.invadersShoots.forEach(s => (state.game[s.x][s.y] = shot)),
35
state.shoots.forEach(s => (state.game[s.x][s.y] = shot)),
36
state.game
37
);
38
39
const addInvaderShoot = state =>
40
(randomInvader => gameObject(randomInvader.x, randomInvader.y))(
41
state.invaders[Math.floor(Math.random() * state.invaders.length)]
42
);
43
44
const collision = (e1, e2) => e1.x === e2.x && e1.y === e2.y;
45
const filterOutCollisions = (c1: any[], c2: any[]): any[] =>
46
c1.filter(e1 => !c2.find(e2 => collision(e1, e2)));
47
const updateScore = (state: State): number =>
48
state.shoots.find(s => state.invaders.find(i => collision(s, i)))
49
? state.score + 1
50
: state.score;
51
52
const updateState = (state: State): State => ({
53
delta: state.delta,
54
game: drawGame(state),
55
shipY: state.shipY,
56
playerLives: state.invadersShoots.some(
57
e => e.x === gameSize - 1 && e.y === state.shipY
58
)
59
? state.playerLives - 1
60
: state.playerLives,
61
isGameOver: state.playerLives <= 0,
62
score: updateScore(state),
63
invadersDirY: invadersDirection(state),
64
invaders: !state.invaders.length
65
? createInvaders()
66
: filterOutCollisions(state.invaders, state.shoots).map(i =>
67
state.delta % 10 === 0
68
? gameObject(
69
i.x + (state.delta % (state.shootFrequency + 10) === 0 ? 1 : 0),
70
i.y + state.invadersDirY
71
)
72
: i
73
),
74
invadersShoots:
75
((state.invadersShoots =
76
state.delta % state.shootFrequency === 0
77
? [...state.invadersShoots, addInvaderShoot(state)]
78
: state.invadersShoots),
79
state.invadersShoots
80
.filter(e => e.x < gameSize - 1)
81
.map(e => gameObject(e.x + 1, e.y))),
82
shoots: filterOutCollisions(state.shoots, state.invaders)
83
.filter(e => e.x > 0)
84
.map(e => gameObject(e.x - 1, e.y)),
85
shootFrequency: !state.invaders.length
86
? state.shootFrequency - 5
87
: state.shootFrequency
88
});
89
90
const keepShipWithinGame = (state: State): number => (
91
(state.shipY = state.shipY < 0 ? 0 : state.shipY),
92
(state.shipY = state.shipY >= gameSize - 1 ? gameSize - 1 : state.shipY)
93
);
94
95
const updateShipY = (state: State, input: Input): number =>
96
input.key !== 'ArrowLeft' && input.key !== 'ArrowRight'
97
? state.shipY
98
: (state.shipY -= input.key === 'ArrowLeft' ? 1 : -1);
99
100
const addShots = (state: State, input: Input) =>
101
(state.shoots =
102
input.key === 'Space'
103
? [...state.shoots, gameObject(gameSize - 2, state.shipY)]
104
: state.shoots);
105
106
const isGameOver = (state: State): boolean =>
107
state.playerLives <= 0 ||
108
(state.invaders.length &&
109
state.invaders[state.invaders.length - 1].x >= gameSize - 1);
110
111
export const initialState: State = {
112
delta: 0,
113
game: clearGame(),
114
shipY: 10,
115
playerLives: 3,
116
isGameOver: false,
117
score: 0,
118
invadersDirY: 1,
119
invaders: createInvaders(),
120
invadersShoots: [],
121
shoots: [],
122
shootFrequency: 20
123
};
124
125
const processInput = (state: State, input: Input) => (
126
updateShipY(state, input), addShots(state, input)
127
);
128
const whileNotGameOver = (state: State, input: Input) =>
129
(state.delta = isGameOver(state) ? undefined : input.dlta);
130
131
export const gameUpdate = (state: State, input: Input): State => (
132
whileNotGameOver(state, input), processInput(state, input), updateState(state)
133
);
Copied!

constants.ts

1
export const empty = 0;
2
export const player = 1;
3
export const invader = 2;
4
export const shot = 3;
5
export const noOfInvadersRows = 6;
Copied!

interfaces.ts

1
export interface State {
2
delta: number;
3
game: number[][];
4
shipY: number;
5
playerLives: number;
6
isGameOver: boolean;
7
score: number;
8
invadersDirY: number;
9
invaders: any[];
10
invadersShoots: any[];
11
shoots: any[];
12
shootFrequency: number;
13
}
14
15
export interface Input {
16
dlta: number;
17
key: string;
18
}
Copied!

html-renderer.ts

1
import { empty, player, invader, shot } from './constants';
2
3
const createElem = col => {
4
const elem = document.createElement('div');
5
elem.classList.add('board');
6
elem.style.display = 'inline-block';
7
elem.style.marginLeft = '10px';
8
elem.style.height = '6px';
9
elem.style.width = '6px';
10
elem.style['background-color'] =
11
col === empty
12
? 'white'
13
: col === player
14
? 'cornflowerblue'
15
: col === invader
16
? 'gray'
17
: 'silver';
18
elem.style['border-radius'] = '90%';
19
return elem;
20
};
21
22
export const paint = (
23
game: number[][],
24
playerLives: number,
25
score: number,
26
isGameOver: boolean
27
) => {
28
document.body.innerHTML = '';
29
document.body.innerHTML += `Score: ${score} Lives: ${playerLives}`;
30
31
if (isGameOver) {
32
document.body.innerHTML += ' GAME OVER!';
33
return;
34
}
35
36
game.forEach(row => {
37
const rowContainer = document.createElement('div');
38
row.forEach(col => rowContainer.appendChild(createElem(col)));
39
document.body.appendChild(rowContainer);
40
});
41
};
Copied!

Operators Used

Last modified 1yr ago