Вот, что я думаю, хороший, обслуживаемый и тестируемый код выглядит следующим образом: набор небольших автономных функций, каждая из которых имеет как можно меньше побочных эффектов. И вместо того, чтобы распределять состояния по всему приложению, состояние должно существовать в едином центральном месте.
Итак, я решил разбить ваш код на маленькие функции. Я вытащил государство в единый магазин, который обеспечивает неизменность. Никаких странных промежуточных домов - состояние приложения меняется или нет. Если это изменится, вся игра будет перерисована. Ответственность за взаимодействие с пользовательским интерфейсом существует в одной render
функции.
И вы задали вопрос о классах в своем вопросе. createGame
становится:
class Game {
constructor() { ... },
start() { ... },
reset() { ... },
play() { ... }
}
createStore
становится:
class Store {
constructor() { ... }
getState() { ... },
setState() { ... }
}
playAI
и playHuman
становится:
class AIPlayer {
constructor(store) { ... }
play() { ... }
}
class HumanPlayer {
constructor(store) { ... }
play() { ... }
}
checkForWinner
становится :
class WinChecker {
check(board) { ... }
}
... и т. Д.
Но я задаю риторический вопрос: добавит ли что-нибудь эти классы к коду? На мой взгляд, есть три фундаментальные и внутренние c проблемы с ориентацией на классы объектов:
- Это ведет вас по пути смешивания состояния и функциональности приложения,
- Классы как снежки - они приобретают функциональность и быстро становятся чрезмерно большими, и
- Люди ужасны, когда приходят с осмысленными онтологиями классов
Все вышеперечисленное означает, что классы неизменно ведут к не поддерживаемый код.
Я думаю, что код, как правило, проще и удобнее в обслуживании без new
и без this
.
index. js
import { createGame } from "./create-game.js";
const game = createGame("#ttt-game");
game.start();
create-game. js
import { initialState } from "./initial-state.js";
import { createStore } from "./create-store.js";
import { render } from "./render.js";
const $ = document.querySelector.bind(document);
function start({ store, render }) {
createGameLoop({ store, render })();
}
function createGameLoop({ store, render }) {
let previousState = null;
return function loop() {
const state = store.getState();
if (state !== previousState) {
render(store);
previousState = store.getState();
}
requestAnimationFrame(loop);
};
}
export function createGame(selector) {
const store = createStore({ ...initialState, el: $(selector) });
return {
start: () => start({ store, render })
};
}
начальное состояние. js
export const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
create-store. js
export function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
рендер. js
import { onSquareClick } from "./on-square-click.js";
import { winners } from "./winners.js";
import { resetGame } from "./reset-game.js";
export function render(store) {
const { el, board, winner } = store.getState();
el.innerHTML = "";
for (let i = 0; i < board.length; i++) {
let square = document.createElement("div");
square.id = `ttt-${i}`;
square.innerText = board[i];
square.classList = "square";
if (!board[i]) {
square.addEventListener("click", onSquareClick.bind(null, store));
}
el.appendChild(square);
}
if (winner) {
const message =
winner === winners.STALEMATE ? `Stalemate!` : `${winner} wins!`;
const msgEL = document.createElement("div");
msgEL.classList = "message";
msgEL.innerText = message;
msgEL.addEventListener("click", () => resetGame(store));
el.appendChild(msgEL);
}
}
при нажатии на квадрат. js
import { play } from "./play.js";
export function onSquareClick(store, { target }) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
победителей. js
export const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
сброс игры. js
import { initialState } from "./initial-state.js";
export function resetGame(store) {
const { el } = store.getState();
store.setState({ ...initialState, el });
}
play. js
import { randomMove } from "./random-move.js";
import { checkForWinner } from "./check-for-winner.js";
import { checkForStalemate } from "./check-for-stalemate.js";
import { winners } from "./winners.js";
function playHuman({ move, store }) {
const state = store.getState();
const updatedBoard = [...state.board];
updatedBoard[move] = "O";
store.setState({ ...state, board: updatedBoard });
}
function playAI(store) {
const state = store.getState();
const move = randomMove(state.board);
const updatedBoard = [...state.board];
updatedBoard[move] = "X";
store.setState({ ...state, board: updatedBoard });
}
export function play({ move, store }) {
playHuman({ move, store });
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.HUMAN });
return;
}
if (checkForStalemate(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.STALEMATE });
return;
}
playAI(store);
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.AI });
return;
}
}
Беговая версия:
const $ = document.querySelector.bind(document);
const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
function randomMove(board) {
let open = [];
for (let i = 0; i < board.length; i++) {
if (board[i] === null) {
open.push(i);
}
}
const random = Math.floor(Math.random() * (open.length - 1));
return open[random];
}
function onSquareClick(store, target) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
function render(store) {
const { el, board, winner } = store.getState();
el.innerHTML = "";
for (let i = 0; i < board.length; i++) {
let square = document.createElement("div");
square.id = `ttt-${i}`;
square.innerText = board[i];
square.classList = "square";
if (!board[i]) {
square.addEventListener("click", ({ target }) =>
onSquareClick(store, target)
);
}
el.appendChild(square);
}
if (winner) {
const message =
winner === winners.STALEMATE ? `Stalemate!` : `${winner} wins!`;
const msgEL = document.createElement("div");
msgEL.classList = "message";
msgEL.innerText = message;
msgEL.addEventListener("click", () => resetGame(store));
el.appendChild(msgEL);
}
}
function resetGame(store) {
const { el } = store.getState();
store.setState({ ...initialState, el });
}
function playHuman({ move, store }) {
const state = store.getState();
const updatedBoard = [...state.board];
updatedBoard[move] = "O";
store.setState({ ...state, board: updatedBoard });
}
function playAI(store) {
const state = store.getState();
const move = randomMove(state.board);
const updatedBoard = [...state.board];
updatedBoard[move] = "X";
store.setState({ ...state, board: updatedBoard });
}
const patterns = [
[0,1,2], [3,4,5], [6,7,8],
[0,4,8], [2,4,6],
[0,3,6], [1,4,7], [2,5,8]
];
function checkForWinner(store) {
const { board } = store.getState();
return patterns.find(([a,b,c]) =>
board[a] === board[b] &&
board[a] === board[c] &&
board[a]);
}
function checkForStalemate(store) {
const { board } = store.getState();
return board.indexOf(null) === -1;
}
function play({ move, store }) {
playHuman({ move, store });
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.HUMAN });
return;
}
if (checkForStalemate(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.STALEMATE });
return;
}
playAI(store);
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.AI });
return;
}
}
function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
function start({ store, render }) {
createGameLoop({ store, render })();
}
function createGameLoop({ store, render }) {
let previousState = null;
return function loop() {
const state = store.getState();
if (state !== previousState) {
render(store);
previousState = store.getState();
}
requestAnimationFrame(loop);
};
}
const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
function createGame(selector) {
const store = createStore({ ...initialState, el: $(selector) });
return {
start: () => start({ store, render })
};
}
const game = createGame("#ttt-game");
game.start();
* {
box-sizing: border-box;
padding: 0;
margin: 0;
font-size: 0;
}
div.container {
width: 150px;
height: 150px;
box-shadow: 0 0 0 5px red inset;
}
div.square {
font-family: sans-serif;
font-size: 26px;
color: gray;
text-align: center;
line-height: 50px;
vertical-align: middle;
cursor: grab;
display: inline-block;
width: 50px;
height: 50px;
box-shadow: 0 0 0 2px black inset;
}
div.message {
font-family: sans-serif;
font-size: 26px;
color: white;
text-align: center;
line-height: 100px;
vertical-align: middle;
cursor: grab;
position: fixed;
top: calc(50% - 50px);
left: 0;
height: 100px;
width: 100%;
background-color: rgba(100, 100, 100, 0.7);
}
<div class="container" id="ttt-game"></div>