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

Example Code

Could not load image
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

Last modified 1yr ago