|
enum C { |
|
NOWALL = 0, |
|
NOP = 1, |
|
WALL = 2, |
|
DOT = 3, |
|
P1 = 4, |
|
P2 = 5, |
|
} |
|
|
|
type Coord = [number, number]; |
|
|
|
type Board = C[][]; |
|
|
|
type State = { |
|
isP1: boolean; |
|
p1Score: number; |
|
p2Score: number; |
|
isOver: boolean; |
|
}; |
|
|
|
function coord(note: string): Coord { |
|
const x = note.toUpperCase()[0].charCodeAt(0) - 65; |
|
const y = +note.toUpperCase().slice(1) - 1; |
|
return [x * 2, y * 2]; |
|
} |
|
|
|
function note(coord: Coord): string { |
|
const xAxis = String.fromCharCode(65 + Math.floor(coord[0] / 2)); |
|
const yAxis = Math.floor(coord[1] / 2); |
|
return xAxis + yAxis; |
|
} |
|
|
|
function getCell(board: Board, coord: Coord): C { |
|
const [width, height] = getSize(board); |
|
const [x, y] = coord; |
|
if (x >= width || x < 0 || y >= height || y < 0) { |
|
throw new Error(`getCell invalid coord (${x}, ${y})`); |
|
} |
|
return board[y][x]; |
|
} |
|
|
|
function setCell(board: Board, coord: Coord, cell: C): Board { |
|
return board.map((r, y) => |
|
r.map((c, x) => { |
|
if (y === coord[1] && x === coord[0]) { |
|
return cell; |
|
} else { |
|
return c; |
|
} |
|
}) |
|
); |
|
} |
|
|
|
/** @return [from, wall, to] */ |
|
function getSpan(fromS: string, toS: string): [Coord, Coord, Coord] { |
|
const from = coord(fromS); |
|
const to = coord(toS); |
|
if (from[0] === to[0] && Math.abs(from[1] - to[1]) === 2) { |
|
if (from[1] - to[1] > 0) { |
|
return [from, [from[0], to[1] + 1], to]; |
|
} else { |
|
return [from, [from[0], from[1] + 1], to]; |
|
} |
|
} else if (from[1] === to[1] && Math.abs(from[0] - to[0]) === 2) { |
|
if (from[0] - to[0] > 0) { |
|
return [from, [to[0] + 1, from[1]], to]; |
|
} else { |
|
return [from, [from[0] + 1, from[1]], to]; |
|
} |
|
} else { |
|
throw new Error(`Invalid move ${fromS}-${toS}`); |
|
} |
|
} |
|
|
|
function getSize(board: Board): [number, number] { |
|
if (board.length < 1) { |
|
return [0, 0]; |
|
} else { |
|
return [board[0].length, board.length]; |
|
} |
|
} |
|
|
|
function getRoomsSurroundingWall(board: Board, wall: Coord): Coord[] { |
|
const [width, height] = getSize(board); |
|
const rooms: Coord[] = []; |
|
if (wall[1] % 2 === 0) { |
|
if (wall[1] + 1 < height) { |
|
rooms.push([wall[0], wall[1] + 1]); |
|
} |
|
if (wall[1] - 1 >= 0) { |
|
rooms.push([wall[0], wall[1] - 1]); |
|
} |
|
} else if (wall[1] % 2 === 1) { |
|
if (wall[0] + 1 < width) { |
|
rooms.push([wall[0] + 1, wall[1]]); |
|
} |
|
if (wall[0] - 1 >= 0) { |
|
rooms.push([wall[0] - 1, wall[1]]); |
|
} |
|
} |
|
return rooms; |
|
} |
|
|
|
function getWallsSurroundingRoom(board: Board, room: Coord): Coord[] { |
|
const [width, height] = getSize(board); |
|
const walls: Coord[] = []; |
|
if (room[1] + 1 < height) { |
|
walls.push([room[0], room[1] + 1]); |
|
} |
|
if (room[1] - 1 >= 0) { |
|
walls.push([room[0], room[1] - 1]); |
|
} |
|
if (room[0] + 1 < width) { |
|
walls.push([room[0] + 1, room[1]]); |
|
} |
|
if (room[0] - 1 >= 0) { |
|
walls.push([room[0] - 1, room[1]]); |
|
} |
|
return walls; |
|
} |
|
|
|
function isEnclosed(board: Board, room: Coord): boolean { |
|
const walls = getWallsSurroundingRoom(board, room); |
|
return walls.every((wall) => getCell(board, wall) === C.WALL); |
|
} |
|
|
|
function getRoomSize(board: Board): number { |
|
const [width, height] = getSize(board); |
|
return Math.floor(width / 2) * Math.floor(height / 2); |
|
} |
|
|
|
function flatMap<T>(l: T[][]): T[] { |
|
return l.reduce((a, n) => a.concat(n), []); |
|
} |
|
|
|
function isOutOfBounds(board: Board, coord: Coord): boolean { |
|
const [width, height] = getSize(board); |
|
const [x, y] = coord; |
|
return x >= width || x < 0 || y >= height || y < 0; |
|
} |
|
|
|
export function getInitialState(isP1Start?: boolean): State { |
|
return { |
|
isP1: isP1Start !== false, |
|
p1Score: 0, |
|
p2Score: 0, |
|
isOver: false, |
|
}; |
|
} |
|
|
|
export function stepGame( |
|
board: Board, |
|
state: State, |
|
answer: string |
|
): [Board, State] { |
|
// 1. Check input. |
|
if (state.isOver) { |
|
throw new Error("Game is over!"); |
|
} |
|
|
|
const [fromS, toS, ..._] = answer.toUpperCase().split(/\-|\s/); |
|
const [from, wall, to] = getSpan(fromS, toS); |
|
|
|
if (isOutOfBounds(board, from) || isOutOfBounds(board, to)) { |
|
throw new Error(`Input "${answer}" is out of bounds!`); |
|
} |
|
|
|
if (getCell(board, wall) !== C.NOWALL) { |
|
throw new Error("A wall is already there! Please choose another."); |
|
} |
|
|
|
// 2. Place wall. |
|
const boardP = setCell(board, wall, C.WALL); |
|
const rooms = getRoomsSurroundingWall(boardP, wall); |
|
|
|
// 3. Mark enclosed rooms from the latest wall placement. |
|
const roomsEnclosed = rooms.filter( |
|
(room) => isEnclosed(boardP, room) && getCell(boardP, room) === C.NOP |
|
); |
|
const score = roomsEnclosed.length; |
|
const boardPP = roomsEnclosed.reduce( |
|
(board, room) => setCell(board, room, state.isP1 ? C.P1 : C.P2), |
|
boardP |
|
); |
|
|
|
// 4. Calculate new scores. |
|
const p1Score = state.p1Score + (state.isP1 ? score : 0); |
|
const p2Score = state.p2Score + (state.isP1 ? 0 : score); |
|
|
|
const newState = { |
|
isP1: score > 0 ? state.isP1 : !state.isP1, |
|
p1Score, |
|
p2Score, |
|
isOver: p1Score + p2Score >= getRoomSize(board), |
|
roomSize: getRoomSize(board), |
|
}; |
|
|
|
return [boardPP, newState]; |
|
} |
|
|
|
export function generateBoard(w: number, h: number): Board { |
|
const board = []; |
|
for (let r = 0; r < h - 1; r++) { |
|
const cornerRow = flatMap(Array(w - 1).fill([C.DOT, C.NOWALL])).concat([ |
|
C.DOT, |
|
]) as C[]; |
|
const noCornerRow = flatMap(Array(w - 1).fill([C.NOWALL, C.NOP])).concat([ |
|
C.NOWALL, |
|
]) as C[]; |
|
board.push(cornerRow); |
|
board.push(noCornerRow); |
|
} |
|
|
|
const cornerRow = flatMap(Array(w - 1).fill([C.DOT, C.NOWALL])).concat([ |
|
C.DOT, |
|
]) as C[]; |
|
board.push(cornerRow); |
|
return board; |
|
} |
|
|
|
const cellToChar = (c: C, y: number): string => { |
|
switch (c) { |
|
case C.NOWALL: |
|
return " "; |
|
case C.NOP: |
|
return " "; |
|
case C.WALL: |
|
return y % 2 === 0 ? "-" : "|"; |
|
case C.DOT: |
|
return "•"; |
|
case C.P1: |
|
return "1"; |
|
case C.P2: |
|
return "2"; |
|
default: |
|
return " "; |
|
} |
|
}; |
|
|
|
export function formatBoard(board: Board): string { |
|
const rows = board.map((r, y) => r.map((c) => cellToChar(c, y)).join("")); |
|
|
|
// Add axes for algebraic notation. |
|
const [width, _] = getSize(board); |
|
const xAxis = |
|
" " + |
|
Array(Math.ceil(width / 2)) |
|
.fill(0) |
|
.map((_, i) => String.fromCharCode(i + 65)) |
|
.join(" "); |
|
const formatYAxis = (y: number) => { |
|
const yN = Math.floor(y / 2) + 1; |
|
return y % 2 === 0 ? `${yN}` : " "; |
|
}; |
|
return [xAxis].concat(rows.map((row, y) => formatYAxis(y) + row)).join("\n"); |
|
} |