Compare commits

..

7 commits

Author SHA1 Message Date
023c725aae
Set class if someone wins
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-15 14:21:43 +01:00
30cbc40d38
Move all click handlers to the App 2022-12-15 14:17:11 +01:00
80b55732e7
Move all click events to Component.js 2022-12-15 13:47:23 +01:00
1e47864ee2
Remove obsolete files 2022-12-15 12:02:53 +01:00
cd88607ad3
Add an event handler for the click event 2022-12-15 11:52:17 +01:00
e6cea57d65
Store state as an object 2022-12-15 11:51:59 +01:00
c94dc1cb8f
Refactor the ElementDescriptor type 2022-12-15 10:55:46 +01:00
8 changed files with 134 additions and 214 deletions

View file

@ -9,16 +9,5 @@
<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>

View file

@ -1,5 +1,10 @@
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`}.
* *
@ -13,6 +18,49 @@ 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
@ -23,7 +71,8 @@ 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]
]; ];
} }
@ -78,3 +127,64 @@ 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
];
}

View file

@ -25,7 +25,7 @@ export class Game
/** /**
* The state of the game. * The state of the game.
* *
* @type {State} * @type {IState}
*/ */
#state; #state;
@ -45,7 +45,7 @@ export class Game
constructor(id) constructor(id)
{ {
this.id = id; this.id = id;
this.#state = new State(Game.#width, Game.#height); this.#state = State.create(Game.#width, Game.#height);
} }
/** /**
@ -66,10 +66,12 @@ export class Game
/** /**
* Gets the current player. * Gets the current player.
*
* @type {CellOwner}
*/ */
get currentPlayer() get currentPlayer()
{ {
return this.state.currentPlayer; return this.state.turnCount % 2 === 0 ? "r" : "b";
} }
/** /**
@ -126,9 +128,7 @@ export class Game
{ {
return { return {
turnCount: this.state.turnCount, turnCount: this.state.turnCount,
board: { board: this.state.board
...this.state.board
}
}; };
} }
@ -140,10 +140,7 @@ export class Game
*/ */
load(data) load(data)
{ {
this.state.turnCount = data.turnCount; this.#state = data;
this.state.board.splice(0);
this.#state = new State(Game.#width, Game.#height);
Object.assign(this.state.board, data.board);
this.draw(); this.draw();
} }
@ -160,7 +157,7 @@ export class Game
*/ */
reset() reset()
{ {
this.#state = new State(Game.#width, Game.#height); this.#state = State.create(Game.#width, Game.#height);
this.draw(); this.draw();
} }
@ -194,7 +191,7 @@ export class Game
{ {
if (this.board[i][x] === "") if (this.board[i][x] === "")
{ {
this.board[i][x] = this.state.currentPlayer; this.board[i][x] = this.currentPlayer;
return true; return true;
} }
} }

View file

@ -4,52 +4,26 @@
export class State export class State
{ {
/** /**
* The board of the game. * Creates a new state.
*
* @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.
*/ */
constructor(width, height) static create(width, height)
{ {
this.#board = /** @type {Board} */ ( return {
Array(height).fill("").map( turnCount: 0,
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;
} }
} }

View file

@ -1,69 +0,0 @@
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));

View file

@ -1,39 +0,0 @@
/**
* 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;
}

View file

@ -7,30 +7,6 @@ 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.
*/ */
@ -38,24 +14,6 @@ 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
View file

@ -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 | Record<string, unknown>)[] ...args: (NodeDescriptor | (Partial<HTMLElement> & Record<string, any>))[]
]; ];
/** /**