Learn RxJS
Search…
Battleship Game
By adamlubek
This recipe demonstrates an RxJS implementation of Battleship Game where you play against the computer.

Example Code

Battleship Game

index.ts

1
// RxJS v6+
2
import { concat, merge } from 'rxjs';
3
import { switchMap, takeWhile, finalize } from 'rxjs/operators';
4
import { NUMBER_OF_SHIP_PARTS } from './constants';
5
import { displayGameOver, paintBoards$ } from './html-renderer';
6
import { shots$, computerScore$, playerScore$, isNotGameOver } from './game';
7
import { setup$, emptyBoards$ } from './setup';
8
import { Boards } from './interfaces';
9
10
const game$ = emptyBoards$.pipe(
11
paintBoards$,
12
switchMap((boards: Boards) =>
13
concat(setup$(boards), shots$(boards)).pipe(
14
takeWhile(isNotGameOver),
15
finalize(displayGameOver(computerScore$))
16
)
17
)
18
);
19
20
merge(game$, computerScore$, playerScore$).subscribe();
Copied!

setup.ts

1
import { concat, interval, of, fromEvent, pipe, noop } from 'rxjs';
2
import { filter, map, scan, take, tap } from 'rxjs/operators';
3
import {
4
GAME_SIZE,
5
NUMBER_OF_SHIP_PARTS,
6
EMPTY,
7
COMPUTER,
8
PLAYER
9
} from './constants';
10
import {
11
paintBoards$,
12
computerScoreContainer,
13
playerScoreContainer
14
} from './html-renderer';
15
import { random, validClicks$ } from './game';
16
import { Boards } from './interfaces';
17
18
const isThereEnoughSpaceForNextMove = (
19
board: number[][],
20
ship: number,
21
x: number,
22
y: number
23
) => {
24
const row = [...board[x]];
25
row[y] = ship;
26
const col = board.map(r => r.filter((c, j) => j === y)[0]);
27
col[x] = ship;
28
29
const shipStartInCol = col.indexOf(ship);
30
const shipEndInCol = col.lastIndexOf(ship);
31
const shipStartInRow = row.indexOf(ship);
32
const shipEndInRow = row.lastIndexOf(ship);
33
34
const checkSpace = (arr, start, end) => {
35
const startIndex = arr.lastIndexOf(
36
(e, i) => e !== EMPTY && e !== ship && i < start
37
);
38
const endIndex = arr.findIndex(
39
(e, i) => e !== EMPTY && e !== ship && i > end
40
);
41
const room = arr.slice(startIndex + 1, endIndex);
42
return room.length >= ship;
43
};
44
45
return shipStartInCol !== shipEndInCol
46
? checkSpace(col, shipStartInCol, shipEndInCol)
47
: shipStartInRow !== shipEndInRow
48
? checkSpace(row, shipStartInRow, shipEndInRow)
49
: true;
50
};
51
52
const getTwoValidMoves = (row: number[], ship: number): [number, number] => [
53
row.indexOf(ship) - 1,
54
row.lastIndexOf(ship) + 1
55
];
56
57
const getValidMoves = (
58
expectedPlayer: string,
59
boards: Boards,
60
ship: number,
61
[name, x, y]
62
): any[] => {
63
const board = boards[expectedPlayer];
64
const rowIndex = board.findIndex(r => r.some(c => c === ship));
65
if (!isThereEnoughSpaceForNextMove(board, ship, x, y)) {
66
return [];
67
}
68
if (rowIndex >= 0) {
69
const row = board[rowIndex];
70
const colIndex = row.findIndex(e => e === ship);
71
72
const isHorizontal =
73
row[colIndex - 1] === ship || row[colIndex + 1] === ship;
74
if (isHorizontal) {
75
const [left, right] = getTwoValidMoves(row, ship);
76
return [
77
{ x: rowIndex, y: left },
78
{ x: rowIndex, y: right }
79
];
80
}
81
82
const isVertical =
83
(board[rowIndex - 1] ? board[rowIndex - 1][colIndex] === ship : false) ||
84
(board[rowIndex + 1] ? board[rowIndex + 1][colIndex] === ship : false);
85
if (isVertical) {
86
const [up, down] = getTwoValidMoves(
87
board.map(r => r.filter((c, j) => j === colIndex)[0]),
88
ship
89
);
90
return [
91
{ x: up, y: colIndex },
92
{ x: down, y: colIndex }
93
];
94
}
95
96
return [
97
{ x: rowIndex, y: colIndex - 1 },
98
{ x: rowIndex, y: colIndex + 1 },
99
{ x: rowIndex - 1, y: colIndex },
100
{ x: rowIndex + 1, y: colIndex }
101
];
102
}
103
104
return [{ x: x, y: y }];
105
};
106
107
const isCellEmpty = (boards: Boards, [name, x, y]): boolean =>
108
boards[name][x][y] === EMPTY;
109
110
const areSpacesAroundCellEmpty = (boards: Boards, [name, x, y]): boolean =>
111
(board =>
112
(board[x - 1] && board[x - 1][y] === EMPTY) ||
113
(board[x + 1] && board[x + 1][y] === EMPTY) ||
114
board[x][y - 1] === EMPTY ||
115
board[x][y + 1] === EMPTY)(boards[name]);
116
117
const canMove = (
118
expectedPlayer: string,
119
boards: Boards,
120
ship: number,
121
[name, x, y]
122
): boolean => {
123
if (!isCellEmpty(boards, [name, x, y]) || name !== expectedPlayer) {
124
return false;
125
}
126
127
const validMoves = getValidMoves(expectedPlayer, boards, ship, [name, x, y]);
128
const isValidMove = validMoves.some(e => e.x === x && e.y === y);
129
130
return isValidMove;
131
};
132
133
const addShips$ = (player: string, boards: Boards) =>
134
pipe(
135
map((e: string) => e.split(',')),
136
filter(e => e.length === 3),
137
map(e => [e[0], parseInt(e[1]), parseInt(e[2])]),
138
scan(
139
(a, coords: any) => (
140
(a.validMove =
141
a.shipPartsLeft > 0
142
? canMove(player, boards, a.ship, coords)
143
: isCellEmpty(boards, coords) &&
144
(a.ship - 1 === 1 || areSpacesAroundCellEmpty(boards, coords))),
145
a.validMove
146
? a.shipPartsLeft > 0
147
? (a.shipPartsLeft -= 1)
148
: ((a.ship = a.ship - 1), (a.shipPartsLeft = a.ship - 1))
149
: noop,
150
(a.coords = coords),
151
a
152
),
153
{ ship: 5, shipPartsLeft: 5, coords: [], validMove: true }
154
),
155
filter(({ validMove }) => validMove),
156
map(
157
({ ship, coords }) => (
158
(boards[player][coords[1]][coords[2]] = ship), boards
159
)
160
),
161
paintBoards$,
162
take(NUMBER_OF_SHIP_PARTS)
163
);
164
165
const playerSetup$ = (boards: Boards) =>
166
fromEvent(document, 'click').pipe(validClicks$, addShips$(PLAYER, boards));
167
168
const computerSetup$ = (boards: Boards) =>
169
interval().pipe(
170
tap(i => (i % 70 === 0 ? (playerScoreContainer.innerHTML += '.') : noop)),
171
map(_ => `${COMPUTER}, ${random()}, ${random()}`),
172
addShips$(COMPUTER, boards)
173
);
174
175
const info$ = (container: HTMLElement, text: string) =>
176
of({}).pipe(tap(_ => (container.innerHTML = text)));
177
178
const createBoard = () =>
179
Array(GAME_SIZE)
180
.fill(EMPTY)
181
.map(_ => Array(GAME_SIZE).fill(EMPTY));
182
183
export const emptyBoards$ = of({
184
});
185
186
export const setup$ = (boards: Boards) =>
187
concat(
188
info$(computerScoreContainer, 'Setup your board!!!'),
189
playerSetup$(boards),
190
info$(playerScoreContainer, 'Computer setting up!!!'),
191
computerSetup$(boards)
192
);
Copied!

game.ts

1
import { fromEvent, pipe, noop, Subject, BehaviorSubject, merge } from 'rxjs';
2
import { repeatWhen, delay, filter, map, takeWhile, tap } from 'rxjs/operators';
3
import {
4
GAME_SIZE,
5
EMPTY,
6
MISS,
7
HIT,
8
SHORTEST_SHIP,
9
LONGEST_SHIP,
10
PLAYER,
11
COMPUTER,
12
NUMBER_OF_SHIP_PARTS
13
} from './constants';
14
import { paintBoards, paintScores } from './html-renderer';
15
import { Boards, ComputerMove } from './interfaces';
16
17
export const random = () => Math.floor(Math.random() * Math.floor(GAME_SIZE));
18
19
export const validClicks$ = pipe(
20
map((e: MouseEvent) => e.target['id']),
21
filter(e => e)
22
);
23
24
const playerMove = new Subject();
25
const computerMove = new BehaviorSubject({ playerBoard: [], hits: {} });
26
27
const shot = (
28
boards: Boards,
29
player: string,
30
x: number,
31
y: number
32
): [number, number, boolean, number] =>
33
((boardValue): [number, number, boolean, number] => (
34
(boards[player][x][y] = boardValue === EMPTY ? MISS : HIT),
35
[x, y, boards[player][x][y] === HIT, boardValue]
36
))(boards[player][x][y]);
37
38
const isValidMove = (boards: Boards, player, x, y): boolean =>
39
boards[player][x][y] !== HIT && boards[player][x][y] !== MISS;
40
41
const performShot$ = (
42
boards: Boards,
43
player: string,
44
nextMove: (x, y, wasHit, boardValue) => void
45
) =>
46
pipe(
47
tap(([player, x, y]) =>
48
!isValidMove(boards, player, x, y)
49
? nextMove(x, y, true, boards[player][x][y])
50
: noop
51
),
52
filter(([player, x, y]) => isValidMove(boards, player, x, y)),
53
map(([_, x, y]) => shot(boards, player, x, y)),
54
tap(
55
([x, y, wasHit, boardValue]) => (
56
paintBoards(boards),
57
nextMove(x, y, wasHit, boardValue),
58
paintScores(computerScore$, playerScore$)
59
)
60
)
61
);
62
63
const computerHits = (
64
playerBoard: number[][],
65
x: number,
66
y: number,
67
wasHit: boolean,
68
boardValue: number
69
): ComputerMove => {
70
if ([EMPTY, HIT, MISS].some(e => e === boardValue)) {
71
return computerMove.value;
72
}
73
if (!computerMove.value.hits[boardValue]) {
74
computerMove.value.hits[boardValue] = [];
75
}
76
computerMove.value.hits[boardValue].push({ x, y });
77
computerMove.value.playerBoard = playerBoard;
78
79
return computerMove.value;
80
};
81
82
const nextComputerMove = (): [string, number, number] => {
83
const hits = computerMove.value.hits;
84
const shipToPursue = Object.keys(hits).find(
85
e => hits[e].length !== parseInt(e)
86
);
87
if (!shipToPursue) {
88
return [PLAYER, random(), random()];
89
}
90
91
const playerBoard = computerMove.value.playerBoard;
92
const shipHits = hits[shipToPursue];
93
if (shipHits.length === 1) {
94
const hit = shipHits[0];
95
96
const shotCandidates = [
97
[hit.x, hit.y - 1],
98
[hit.x, hit.y + 1],
99
[hit.x - 1, hit.y],
100
[hit.x + 1, hit.y]
101
].filter(
102
([x, y]) =>
103
playerBoard[x] &&
104
playerBoard[x][y] !== undefined &&
105
playerBoard[x][y] !== MISS &&
106
playerBoard[x][y] !== HIT
107
);
108
109
return [PLAYER, shotCandidates[0][0], shotCandidates[0][1]];
110
}
111
112
const getOrderedHits = key =>
113
(orderedHits => [orderedHits[0], orderedHits[orderedHits.length - 1]])(
114
shipHits.sort((h1, h2) => (h1[key] > h2[key] ? 1 : -1))
115
);
116
const isHorizontal = shipHits.every(e => e.x === shipHits[0].x);
117
118
if (isHorizontal) {
119
const [min, max] = getOrderedHits('y');
120
return [
121
PLAYER,
122
min.x,
123
playerBoard[min.x][min.y - 1] !== undefined &&
124
playerBoard[min.x][min.y - 1] !== HIT &&
125
playerBoard[min.x][min.y - 1] !== MISS
126
? min.y - 1
127
: max.y + 1
128
];
129
}
130
131
const [min, max] = getOrderedHits('x');
132
return [
133
PLAYER,
134
playerBoard[min.x - 1] !== undefined &&
135
playerBoard[min.x - 1][min.y] !== HIT &&
136
playerBoard[min.x - 1][min.y] !== MISS
137
? min.x - 1
138
: max.x + 1,
139
min.y
140
];
141
};
142
143
const initialScore = () => ({
144
score: 0,
145
ships: { 5: 5, 4: 4, 3: 3, 2: 2, 1: 1 }
146
});
147
export const playerScore$ = new BehaviorSubject(initialScore());
148
export const computerScore$ = new BehaviorSubject(initialScore());
149
export const isNotGameOver = _ =>
150
computerScore$.value.score < NUMBER_OF_SHIP_PARTS &&
151
playerScore$.value.score < NUMBER_OF_SHIP_PARTS;
152
153
const scoreChange = (subject: BehaviorSubject<any>, boardValue: number) =>
154
boardValue >= SHORTEST_SHIP && boardValue <= LONGEST_SHIP
155
? ((subject.value.ships[boardValue] -= 1),
156
subject.next({
157
score: subject.value.score + 1,
158
ships: subject.value.ships
159
}))
160
: noop;
161
162
const computerShot$ = (boards: Boards) =>
163
computerMove.pipe(
164
delay(200),
165
map(_ => nextComputerMove()),
166
performShot$(boards, PLAYER, (x, y, wasHit, boardValue) =>
167
wasHit
168
? (scoreChange(computerScore$, boardValue),
169
computerMove.next(
170
computerHits(boards[PLAYER], x, y, wasHit, boardValue)
171
))
172
: playerMove.next()
173
)
174
);
175
176
const playerShot$ = (boards: Boards) =>
177
fromEvent(document, 'click').pipe(
178
validClicks$,
179
map((click: string) => click.split(',')),
180
filter(([player]) => player === COMPUTER),
181
performShot$(boards, COMPUTER, (x, y, wasHit, boardValue) =>
182
wasHit
183
? scoreChange(playerScore$, boardValue)
184
: computerMove.next(computerMove.value)
185
),
186
takeWhile(([x, y, wasHit]) => wasHit),
187
repeatWhen(_ => playerMove)
188
);
189
190
export const shots$ = (boards: Boards) =>
191
merge(playerShot$(boards), computerShot$(boards));
Copied!

html-renderer.ts

1
import { BehaviorSubject, pipe } from "rxjs";
2
import { tap } from "rxjs/operators";
3
import {
4
NUMBER_OF_SHIP_PARTS,
5
EMPTY,
6
MISS,
7
HIT,
8
PLAYER,
9
COMPUTER
10
} from "./constants";
11
import { Boards } from "./interfaces";
12
13
const byId = (id: string): HTMLElement => document.getElementById(id);
14
export const computerScoreContainer = byId("computer_score");
15
export const playerScoreContainer = byId("player_score");
16
17
const playerCells = (cell: number): string | number =>
18
cell !== EMPTY ? (cell === MISS ? "o" : cell === HIT ? "x" : cell) : "_";
19
const computerCells = (cell: number): string | number =>
20
cell === HIT || cell === MISS ? (cell === MISS ? "o" : "x") : "_";
21
22
export const paintBoard = (
23
container: HTMLElement,
24
playerName: string,
25
board: number[][]
26
) => (
27
(container.innerHTML = ""),
28
board.forEach((r, i) =>
29
r.forEach(
30
(c, j) =>
31
(container.innerHTML += `
32
<div id=${playerName},${i},${j}
33
style='float:left; margin-left: 5px'>
34
${playerName === PLAYER ? playerCells(c) : computerCells(c)}
35
</div>`),
36
(container.innerHTML += "<br/>")
37
)
38
),
39
(container.innerHTML += "<br/><br/>")
40
);
41
42
export const paintShipsInfo = (scoreSubject: BehaviorSubject<any>) =>
43
Object.keys(scoreSubject.value.ships).reduce(
44
(a, c) => ((a += `<b>${c} </b>: ${scoreSubject.value.ships[c]} | `), a),
45
""
46
);
47
48
export const paintScores = (
49
computerScore: BehaviorSubject<any>,
50
playerScore: BehaviorSubject<any>
51
) =>
52
((c: HTMLElement, p: HTMLElement) => (
53
(c.innerHTML = ""),
54
(c.innerHTML += "Computer score: " + computerScore.value.score + "<br/>"),
55
paintShipsInfo(computerScore),
56
(c.innerHTML += "Ships: " + paintShipsInfo(computerScore)),
57
(p.innerHTML = ""),
58
(p.innerHTML += "Player score: " + playerScore.value.score + "<br/>"),
59
(p.innerHTML += "Ships: " + paintShipsInfo(playerScore))
60
))(computerScoreContainer, playerScoreContainer);
61
62
export const paintBoards = (boards: Boards) => (
63
paintBoard(byId("player_board"), PLAYER, boards[PLAYER]),
64
paintBoard(byId("computer_board"), COMPUTER, boards[COMPUTER])
65
);
66
67
export const paintBoards$ = pipe<any, any>(tap(paintBoards));
68
69
export const displayGameOver = (computerScore: BehaviorSubject<any>) => () => {
70
const gameOverText = `GAME OVER,
71
${
72
computerScore.value.score === NUMBER_OF_SHIP_PARTS
73
? "Computer"
74
: "Player"
75
}
76
won`;
77
playerScoreContainer.innerHTML = gameOverText;
78
computerScoreContainer.innerHTML = gameOverText;
79
};
Copied!

interfaces.ts

1
export interface Boards {
2
player: [string, number[][]];
3
computer: [string, number[][]];
4
}
5
6
export interface ComputerMove {
7
playerBoard: number[];
8
hits: {};
9
}
Copied!

constants.ts

1
export const GAME_SIZE = 12;
2
export const NUMBER_OF_SHIP_PARTS = 15;
3
export const EMPTY = 0;
4
export const MISS = 8;
5
export const HIT = 9;
6
export const SHORTEST_SHIP = 1;
7
export const LONGEST_SHIP = 5;
8
export const PLAYER = 'p';
9
export const COMPUTER = 'c';
Copied!

Operators Used

Subjects Used