Compare commits
No commits in common. "023c725aae357fe594bc9004d5cda09b255a701c" and "5ff474a7eab775a5b24b2d1cacf5e7a68d3fd7c3" have entirely different histories.
023c725aae
...
5ff474a7ea
8 changed files with 214 additions and 134 deletions
|
@ -9,5 +9,16 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="game">
|
<div id="game">
|
||||||
</div>
|
</div>
|
||||||
|
<form class="menu-bar">
|
||||||
|
<button class="button new-game">
|
||||||
|
New Game
|
||||||
|
</button>
|
||||||
|
<button class="button save">
|
||||||
|
Save Game
|
||||||
|
</button>
|
||||||
|
<button class="button load">
|
||||||
|
Load Game
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { Constants } from "./Constants.js";
|
import { Constants } from "./Constants.js";
|
||||||
|
|
||||||
const saveGameKey = "connect-force-save";
|
|
||||||
const newGameClass = "new-game";
|
|
||||||
const saveGameClass = "save-game";
|
|
||||||
const loadGameClass = "load-game";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a component which represents the specified {@link game `game`}.
|
* Gets a component which represents the specified {@link game `game`}.
|
||||||
*
|
*
|
||||||
|
@ -18,49 +13,6 @@ export function App(game)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"div",
|
"div",
|
||||||
{
|
|
||||||
onclick: (event) =>
|
|
||||||
{
|
|
||||||
let target = /** @type {HTMLElement} */ (event.target);
|
|
||||||
|
|
||||||
if (target.classList.contains(newGameClass))
|
|
||||||
{
|
|
||||||
game.reset();
|
|
||||||
}
|
|
||||||
else if (target.classList.contains(saveGameClass))
|
|
||||||
{
|
|
||||||
localStorage.setItem(saveGameKey, JSON.stringify(game.dump()));
|
|
||||||
}
|
|
||||||
else if (target.classList.contains(loadGameClass))
|
|
||||||
{
|
|
||||||
game.load(JSON.parse(localStorage.getItem(saveGameKey)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (let y = 0; y < game.board.length; y++)
|
|
||||||
{
|
|
||||||
let rowLength = game.board[y].length;
|
|
||||||
|
|
||||||
for (let x = 0; x < rowLength; x++)
|
|
||||||
{
|
|
||||||
let node = document.querySelector(".board").children.item(y * rowLength + x);
|
|
||||||
|
|
||||||
if (node === target || node.contains(target))
|
|
||||||
{
|
|
||||||
if (game.addChip(x, y))
|
|
||||||
{
|
|
||||||
game.state.turnCount++;
|
|
||||||
game.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
className: `game ${game.winner ? `winner ${game.winner}` : ""}`
|
|
||||||
},
|
|
||||||
[
|
[
|
||||||
Board,
|
Board,
|
||||||
game.board
|
game.board
|
||||||
|
@ -71,8 +23,7 @@ export function App(game)
|
||||||
game.winner ?
|
game.winner ?
|
||||||
`Player ${Constants.PLAYER_NAMES[game.winner]} wins!` :
|
`Player ${Constants.PLAYER_NAMES[game.winner]} wins!` :
|
||||||
`It's player "${Constants.PLAYER_NAMES[game.currentPlayer]}"s turn`
|
`It's player "${Constants.PLAYER_NAMES[game.currentPlayer]}"s turn`
|
||||||
],
|
]
|
||||||
[MenuBar, game]
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,64 +78,3 @@ export function Field(field)
|
||||||
[])
|
[])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a menu bar.
|
|
||||||
*
|
|
||||||
* @param {import("./Game.js").Game} game
|
|
||||||
* The game controlled by the menu bar.
|
|
||||||
*
|
|
||||||
* @returns {NodeDescriptor}
|
|
||||||
* The rendered element.
|
|
||||||
*/
|
|
||||||
export function MenuBar(game)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"div",
|
|
||||||
{ className: "menu-bar" },
|
|
||||||
[
|
|
||||||
Button,
|
|
||||||
[
|
|
||||||
"New Game",
|
|
||||||
{
|
|
||||||
className: newGameClass
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Button,
|
|
||||||
[
|
|
||||||
"Save Game",
|
|
||||||
{
|
|
||||||
className: saveGameClass
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Button,
|
|
||||||
[
|
|
||||||
"Load Game",
|
|
||||||
{
|
|
||||||
className: loadGameClass
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a button.
|
|
||||||
*
|
|
||||||
* @param {...ElementDescriptor[1][]} content
|
|
||||||
* The settings of the button.
|
|
||||||
*
|
|
||||||
* @returns {NodeDescriptor}
|
|
||||||
* The rendered element.
|
|
||||||
*/
|
|
||||||
export function Button(content)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"button",
|
|
||||||
...content
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class Game
|
||||||
/**
|
/**
|
||||||
* The state of the game.
|
* The state of the game.
|
||||||
*
|
*
|
||||||
* @type {IState}
|
* @type {State}
|
||||||
*/
|
*/
|
||||||
#state;
|
#state;
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export class Game
|
||||||
constructor(id)
|
constructor(id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.#state = State.create(Game.#width, Game.#height);
|
this.#state = new State(Game.#width, Game.#height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,12 +66,10 @@ export class Game
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current player.
|
* Gets the current player.
|
||||||
*
|
|
||||||
* @type {CellOwner}
|
|
||||||
*/
|
*/
|
||||||
get currentPlayer()
|
get currentPlayer()
|
||||||
{
|
{
|
||||||
return this.state.turnCount % 2 === 0 ? "r" : "b";
|
return this.state.currentPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,7 +126,9 @@ export class Game
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
turnCount: this.state.turnCount,
|
turnCount: this.state.turnCount,
|
||||||
board: this.state.board
|
board: {
|
||||||
|
...this.state.board
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,10 @@ export class Game
|
||||||
*/
|
*/
|
||||||
load(data)
|
load(data)
|
||||||
{
|
{
|
||||||
this.#state = data;
|
this.state.turnCount = data.turnCount;
|
||||||
|
this.state.board.splice(0);
|
||||||
|
this.#state = new State(Game.#width, Game.#height);
|
||||||
|
Object.assign(this.state.board, data.board);
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +160,7 @@ export class Game
|
||||||
*/
|
*/
|
||||||
reset()
|
reset()
|
||||||
{
|
{
|
||||||
this.#state = State.create(Game.#width, Game.#height);
|
this.#state = new State(Game.#width, Game.#height);
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +194,7 @@ export class Game
|
||||||
{
|
{
|
||||||
if (this.board[i][x] === "")
|
if (this.board[i][x] === "")
|
||||||
{
|
{
|
||||||
this.board[i][x] = this.currentPlayer;
|
this.board[i][x] = this.state.currentPlayer;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,26 +4,52 @@
|
||||||
export class State
|
export class State
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new state.
|
* The board of the game.
|
||||||
|
*
|
||||||
|
* @type {Board}
|
||||||
|
*/
|
||||||
|
#board;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of turns that have been played.
|
||||||
|
*/
|
||||||
|
turnCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new instance of the {@link State `State`} class.
|
||||||
*
|
*
|
||||||
* @param {number} width
|
* @param {number} width
|
||||||
* The width of the board.
|
* The width of the board.
|
||||||
*
|
*
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
* The height of the board.
|
* The height of the board.
|
||||||
*
|
|
||||||
* @returns {IState}
|
|
||||||
* The newly created state.
|
|
||||||
*/
|
*/
|
||||||
static create(width, height)
|
constructor(width, height)
|
||||||
{
|
{
|
||||||
return {
|
this.#board = /** @type {Board} */ (
|
||||||
turnCount: 0,
|
Array(height).fill("").map(
|
||||||
board: Array(height).fill("").map(
|
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
return Array(width).fill("");
|
return Array(width).fill(
|
||||||
})
|
/** @type {CellOwner} */ (""));
|
||||||
};
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the id of the current player.
|
||||||
|
*
|
||||||
|
* @type {CellOwner}
|
||||||
|
*/
|
||||||
|
get currentPlayer()
|
||||||
|
{
|
||||||
|
return this.turnCount % 2 === 0 ? "r" : "b";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the board of the game.
|
||||||
|
*/
|
||||||
|
get board()
|
||||||
|
{
|
||||||
|
return this.#board;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
69
src/js/connect4-winner.js
Normal file
69
src/js/connect4-winner.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
let testBoard = [
|
||||||
|
["_", "_", "_", "_", "_", "_", "_"],
|
||||||
|
["_", "_", "_", "_", "_", "_", "_"],
|
||||||
|
["_", "_", "_", "_", "r", "_", "_"],
|
||||||
|
["_", "_", "_", "r", "r", "b", "b"],
|
||||||
|
["_", "_", "r", "b", "r", "r", "b"],
|
||||||
|
["b", "b", "b", "r", "r", "b", "b"]
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the specified {@link player `player`} is a winner according to the specified {@link board `board`}.
|
||||||
|
*
|
||||||
|
* @param {string} player
|
||||||
|
* The player to check for.
|
||||||
|
*
|
||||||
|
* @param {string[][]} board
|
||||||
|
* The board to check.
|
||||||
|
*
|
||||||
|
* @param {number} count
|
||||||
|
* The number of chips which need to be set in a row.
|
||||||
|
*
|
||||||
|
* @param {number} width
|
||||||
|
* The width of the board.
|
||||||
|
*
|
||||||
|
* @param {number} height
|
||||||
|
* The height of the board.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* A value indicating whether the specified {@link player `player`} did win.
|
||||||
|
*/
|
||||||
|
function connect4Winner(player, board, count = 4, width = 7, height = 6)
|
||||||
|
{
|
||||||
|
for (let yOffset = 0; yOffset <= 1; yOffset++)
|
||||||
|
{
|
||||||
|
for (let xOffset = (-1 * yOffset) + (1 - yOffset); xOffset <= 1; xOffset++)
|
||||||
|
{
|
||||||
|
let lowerBound = Math.max(0, xOffset * (count - 1) * -1);
|
||||||
|
let upperBound = Math.min(width, width - (xOffset * (count - 1)));
|
||||||
|
|
||||||
|
for (let y = 0; y < (height - yOffset * (count - 1)); y++)
|
||||||
|
{
|
||||||
|
for (let x = lowerBound; x < upperBound; x++)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
let tokens = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
tokens.push(board[y + index * yOffset][x + index * xOffset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.every((token) => token === player))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { connect4Winner };
|
||||||
|
|
||||||
|
console.log(connect4Winner("r", testBoard));
|
||||||
|
console.log(connect4Winner("b", testBoard));
|
39
src/js/elt.js
Normal file
39
src/js/elt.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Creates a new element based on the provided data.
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* The type of the element to create.
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} attrs
|
||||||
|
* The attributes to add.
|
||||||
|
*
|
||||||
|
* @param {...(Node | string)} children
|
||||||
|
* The children to add to the element.
|
||||||
|
*
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
* The newly created element.
|
||||||
|
*/
|
||||||
|
export function elt(type, attrs, ...children)
|
||||||
|
{
|
||||||
|
let node = document.createElement(type);
|
||||||
|
|
||||||
|
Object.keys(attrs).forEach(
|
||||||
|
key =>
|
||||||
|
{
|
||||||
|
node.setAttribute(key, attrs[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let child of children)
|
||||||
|
{
|
||||||
|
if (typeof child !== "string")
|
||||||
|
{
|
||||||
|
node.appendChild(child);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node.appendChild(document.createTextNode(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
|
@ -7,6 +7,30 @@ import { Game } from "./Game.js";
|
||||||
*/
|
*/
|
||||||
let game;
|
let game;
|
||||||
|
|
||||||
|
const saveGameKey = "connect-force-save";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the save button.
|
||||||
|
*
|
||||||
|
* @returns {HTMLButtonElement}
|
||||||
|
* The save button.
|
||||||
|
*/
|
||||||
|
function getSaveButton()
|
||||||
|
{
|
||||||
|
return document.querySelector(".save");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the load button.
|
||||||
|
*
|
||||||
|
* @returns {HTMLButtonElement}
|
||||||
|
* The load button.
|
||||||
|
*/
|
||||||
|
function getLoadButton()
|
||||||
|
{
|
||||||
|
return document.querySelector(".load");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the board.
|
* Initializes the board.
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +38,24 @@ function initialize()
|
||||||
{
|
{
|
||||||
game = new Game("game");
|
game = new Game("game");
|
||||||
game.initialize();
|
game.initialize();
|
||||||
|
|
||||||
|
(/** @type {HTMLElement} */ (document.querySelector(".new-game"))).onclick = (event) =>
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
game.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
getSaveButton().onclick = async (event) =>
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
localStorage.setItem(saveGameKey, JSON.stringify(game.dump()));
|
||||||
|
};
|
||||||
|
|
||||||
|
getLoadButton().onclick = async (event) =>
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
game.load(JSON.parse(localStorage.getItem(saveGameKey)));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize();
|
initialize();
|
||||||
|
|
2
src/js/types.d.ts
vendored
2
src/js/types.d.ts
vendored
|
@ -48,7 +48,7 @@ type TextDescriptor = string;
|
||||||
type ElementDescriptor = [
|
type ElementDescriptor = [
|
||||||
tag: string,
|
tag: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/array-type
|
// eslint-disable-next-line @typescript-eslint/array-type
|
||||||
...args: (NodeDescriptor | (Partial<HTMLElement> & Record<string, any>))[]
|
...args: (NodeDescriptor | Record<string, unknown>)[]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue