将工作功能 Javascript 井字游戏转换为 Class 以练习 OOP
Converting working functional Javascript Tic Tac Toe game to Class based to practice OOP
我正在为自己做一个练习,以便更好地理解 OOP 设计,方法是将一个可用的 Javascript 功能性 Tic Tac Toe 游戏与 AI 转换为一个基于 Class 的游戏。我陷入了一些常见问题,例如在 classes 中放置什么、单一事实来源、松散耦合等。不是在这里寻找完整的答案,而是寻找一些关于更好策略的提示?
这是原始的工作功能 TTT:
import "./styles.css";
// functional TIC TAC TOE
// Human is 'O'
// Player is 'X'
let ttt = {
board: [], // array to hold the current game
reset: function() {
// reset board array and get HTML container
ttt.board = [];
const container = document.getElementById("ttt-game"); // the on div declared in HTML file
container.innerHTML = "";
// redraw swuares
// create a for loop to build board
for (let i = 0; i < 9; i++) {
// push board array with null
ttt.board.push(null);
// set square to create DOM element with 'div'
let square = document.createElement("div");
// insert " " non-breaking space to square
square.innnerHTML = " ";
// set square.dataset.idx set to i of for loop
square.dataset.idx = i;
// build square id's with i from loop / 'ttt-' + i - concatnate iteration
square.id = "ttt-" + i;
// add click eventlistener to square to fire ttt.play();
square.addEventListener("click", ttt.play);
// appendChild with square (created element 'div') to container
container.appendChild(square);
}
},
play: function() {
// ttt.play() : when the player selects a square
// play is fired when player selects square
// (A) Player's move - Mark with "O"
// set move to this.dataset.idx
let move = this.dataset.idx;
// assign ttt.board array with move to 0
ttt.board[move] = 0;
// assign "O" to innerHTML for this
this.innerHTML = "O";
// add "Player" to a classList for this
this.classList.add("Player");
// remove the eventlistener 'click' and fire ttt.play
this.removeEventListener("click", ttt.play);
// (B) No more moves available - draw
// check to see if board is full
if (ttt.board.indexOf(null) === -1) {
// alert "No winner"
alert("No Winner!");
// ttt.reset();
ttt.reset();
} else {
// (C) Computer's move - Mark with 'X'
// capture move made with dumbAI or notBadAI
move = ttt.dumbAI();
// assign ttt.board array with move to 1
ttt.board[move] = 1;
// assign sqaure to AI move with id "ttt-" + move (concatenate)
let square = document.getElementById("ttt-" + move);
// assign "X" to innerHTML for this
square.innerHTML = "X";
// add "Computer" to a classList for this
square.classList.add("Computer");
// square removeEventListener click and fire ttt.play
square.removeEventListener("click", ttt.play);
// (D) Who won?
// assign win to null (null, "x", "O")
let win = null;
// Horizontal row checks
for (let i = 0; i < 9; i += 3) {
if (
ttt.board[i] != null &&
ttt.board[i + 1] != null &&
ttt.board[i + 2] != null
) {
if (
ttt.board[i] == ttt.board[i + 1] &&
ttt.board[i + 1] == ttt.board[i + 2]
) {
win = ttt.board[i];
}
}
if (win !== null) {
break;
}
}
// Vertical row checks
if (win === null) {
for (let i = 0; i < 3; i++) {
if (
ttt.board[i] !== null &&
ttt.board[i + 3] !== null &&
ttt.board[i + 6] !== null
) {
if (
ttt.board[i] === ttt.board[i + 3] &&
ttt.board[i + 3] === ttt.board[i + 6]
) {
win = ttt.board[i];
}
if (win !== null) {
break;
}
}
}
}
// Diaganal row checks
if (win === null) {
if (
ttt.board[0] != null &&
ttt.board[4] != null &&
ttt.board[8] != null
) {
if (ttt.board[0] == ttt.board[4] && ttt.board[4] == ttt.board[8]) {
win = ttt.board[4];
}
}
}
if (win === null) {
if (
ttt.board[2] != null &&
ttt.board[4] != null &&
ttt.board[6] != null
) {
if (ttt.board[2] == ttt.board[4] && ttt.board[4] == ttt.board[6]) {
win = ttt.board[4];
}
}
}
// We have a winner
if (win !== null) {
alert("WINNER - " + (win === 0 ? "Player" : "Computer"));
ttt.reset();
}
}
},
dumbAI: function() {
// ttt.dumbAI() : dumb computer AI, randomly chooses an empty slot
// Extract out all open slots
let open = [];
for (let i = 0; i < 9; i++) {
if (ttt.board[i] === null) {
open.push(i);
}
}
// Randomly choose open slot
const random = Math.floor(Math.random() * (open.length - 1));
return open[random];
},
notBadAI: function() {
// ttt.notBadAI() : AI with a little more intelligence
// (A) Init
var move = null;
var check = function(first, direction, pc) {
// checkH() : helper function, check possible winning row
// PARAM square : first square number
// direction : "R"ow, "C"ol, "D"iagonal
// pc : 0 for player, 1 for computer
var second = 0,
third = 0;
if (direction === "R") {
second = first + 1;
third = first + 2;
} else if (direction === "C") {
second = first + 3;
third = first + 6;
} else {
second = 4;
third = first === 0 ? 8 : 6;
}
if (
ttt.board[first] === null &&
ttt.board[second] === pc &&
ttt.board[third] === pc
) {
return first;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === null &&
ttt.board[third] === pc
) {
return second;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === pc &&
ttt.board[third] === null
) {
return third;
}
return null;
};
// (B) Priority #1 - Go for the win
// (B1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 1);
if (move !== null) {
break;
}
}
// (B2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 1);
if (move !== null) {
break;
}
}
}
// (B3) Check diagonal
if (move === null) {
move = check(0, "D", 1);
}
if (move === null) {
move = check(2, "D", 1);
}
// (C) Priority #2 - Block player from winning
// (C1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 0);
if (move !== null) {
break;
}
}
// (C2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 0);
if (move !== null) {
break;
}
}
}
// (C3) Check diagonal
if (move === null) {
move = check(0, "D", 0);
}
if (move === null) {
move = check(2, "D", 0);
}
// (D) Random move if nothing
if (move === null) {
move = ttt.dumbAI();
}
return move;
}
};
document.addEventListener("DOMContentLoaded", ttt.reset());
这是我目前基于 class 的版本:
import "./styles.css";
class Gameboard {
constructor() {
this.board = [];
this.container = document.getElementById("ttt-game");
this.container.innerHTML = "";
}
reset() {
this.board = [];
}
build() {
for (let i = 0; i < 9; i++) {
this.board.push(null);
const square = document.createElement("div");
square.innerHTML = " ";
square.dataset.idx = i;
square.id = "ttt-" + i;
square.addEventListener("click", () => {
// What method do I envoke here?
console.log(square)
});
this.container.appendChild(square);
}
}
};
class Game {
constructor() {
this.gameBoard = new Gameboard();
this.player = new Player();
this.computer = new Computer();
}
play() {
this.gameBoard.build();
}
};
class Player {
};
class Computer {
};
class DumbAI {
};
const game = new Game();
document.addEventListener("DOMContentLoaded", game.play());
我的 HTML 文件非常简单,只有一个 <div id="ttt-game"></div>
开始,CSS 文件是 grid
。
我遇到的最大问题是在 Game
中捕获 squares
。我应该把 eventListeners
放在哪里? (我的下一个项目是做一个React版本)。
这是我的想法,良好的、可维护的和可测试的代码看起来像:一堆小的、独立的函数,每个函数都有尽可能少的副作用。而不是让状态散布在应用程序周围,状态应该存在于一个单一的中央位置。
所以,我所做的是将您的代码分解成小函数。我已将状态拉入一个强制不变性的单一存储中。没有奇怪的中转站——应用程序状态改变,或者没有改变。如果它发生变化,整个游戏将被重新渲染。与 UI 交互的责任存在于单个 render
函数中。
并且您在问题中询问了 classes。 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) { ... }
}
...等等。
但我问了一个反问:添加这些 class 是否会给代码添加任何内容?在我看来,class 面向对象存在三个基本和内在的问题:
- 它引导您走上混合应用程序状态和功能的道路,
- 类 就像滚雪球 - 它们会增加功能并迅速变得过大,并且
- 人们很难想出有意义的 class 本体
以上所有意味着 classes 总是导致严重无法维护的代码。
我认为没有 new
和没有 this
.
的代码通常更简单,更易于维护
index.js
import { createGame } from "./create-game.js";
const game = createGame("#ttt-game");
game.start();
创建-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 })
};
}
初始-state.js
export const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
创建-store.js
export function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
render.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);
}
}
正方形-click.js
import { play } from "./play.js";
export function onSquareClick(store, { target }) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
winners.js
export const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
重置-game.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>
我正在为自己做一个练习,以便更好地理解 OOP 设计,方法是将一个可用的 Javascript 功能性 Tic Tac Toe 游戏与 AI 转换为一个基于 Class 的游戏。我陷入了一些常见问题,例如在 classes 中放置什么、单一事实来源、松散耦合等。不是在这里寻找完整的答案,而是寻找一些关于更好策略的提示?
这是原始的工作功能 TTT:
import "./styles.css";
// functional TIC TAC TOE
// Human is 'O'
// Player is 'X'
let ttt = {
board: [], // array to hold the current game
reset: function() {
// reset board array and get HTML container
ttt.board = [];
const container = document.getElementById("ttt-game"); // the on div declared in HTML file
container.innerHTML = "";
// redraw swuares
// create a for loop to build board
for (let i = 0; i < 9; i++) {
// push board array with null
ttt.board.push(null);
// set square to create DOM element with 'div'
let square = document.createElement("div");
// insert " " non-breaking space to square
square.innnerHTML = " ";
// set square.dataset.idx set to i of for loop
square.dataset.idx = i;
// build square id's with i from loop / 'ttt-' + i - concatnate iteration
square.id = "ttt-" + i;
// add click eventlistener to square to fire ttt.play();
square.addEventListener("click", ttt.play);
// appendChild with square (created element 'div') to container
container.appendChild(square);
}
},
play: function() {
// ttt.play() : when the player selects a square
// play is fired when player selects square
// (A) Player's move - Mark with "O"
// set move to this.dataset.idx
let move = this.dataset.idx;
// assign ttt.board array with move to 0
ttt.board[move] = 0;
// assign "O" to innerHTML for this
this.innerHTML = "O";
// add "Player" to a classList for this
this.classList.add("Player");
// remove the eventlistener 'click' and fire ttt.play
this.removeEventListener("click", ttt.play);
// (B) No more moves available - draw
// check to see if board is full
if (ttt.board.indexOf(null) === -1) {
// alert "No winner"
alert("No Winner!");
// ttt.reset();
ttt.reset();
} else {
// (C) Computer's move - Mark with 'X'
// capture move made with dumbAI or notBadAI
move = ttt.dumbAI();
// assign ttt.board array with move to 1
ttt.board[move] = 1;
// assign sqaure to AI move with id "ttt-" + move (concatenate)
let square = document.getElementById("ttt-" + move);
// assign "X" to innerHTML for this
square.innerHTML = "X";
// add "Computer" to a classList for this
square.classList.add("Computer");
// square removeEventListener click and fire ttt.play
square.removeEventListener("click", ttt.play);
// (D) Who won?
// assign win to null (null, "x", "O")
let win = null;
// Horizontal row checks
for (let i = 0; i < 9; i += 3) {
if (
ttt.board[i] != null &&
ttt.board[i + 1] != null &&
ttt.board[i + 2] != null
) {
if (
ttt.board[i] == ttt.board[i + 1] &&
ttt.board[i + 1] == ttt.board[i + 2]
) {
win = ttt.board[i];
}
}
if (win !== null) {
break;
}
}
// Vertical row checks
if (win === null) {
for (let i = 0; i < 3; i++) {
if (
ttt.board[i] !== null &&
ttt.board[i + 3] !== null &&
ttt.board[i + 6] !== null
) {
if (
ttt.board[i] === ttt.board[i + 3] &&
ttt.board[i + 3] === ttt.board[i + 6]
) {
win = ttt.board[i];
}
if (win !== null) {
break;
}
}
}
}
// Diaganal row checks
if (win === null) {
if (
ttt.board[0] != null &&
ttt.board[4] != null &&
ttt.board[8] != null
) {
if (ttt.board[0] == ttt.board[4] && ttt.board[4] == ttt.board[8]) {
win = ttt.board[4];
}
}
}
if (win === null) {
if (
ttt.board[2] != null &&
ttt.board[4] != null &&
ttt.board[6] != null
) {
if (ttt.board[2] == ttt.board[4] && ttt.board[4] == ttt.board[6]) {
win = ttt.board[4];
}
}
}
// We have a winner
if (win !== null) {
alert("WINNER - " + (win === 0 ? "Player" : "Computer"));
ttt.reset();
}
}
},
dumbAI: function() {
// ttt.dumbAI() : dumb computer AI, randomly chooses an empty slot
// Extract out all open slots
let open = [];
for (let i = 0; i < 9; i++) {
if (ttt.board[i] === null) {
open.push(i);
}
}
// Randomly choose open slot
const random = Math.floor(Math.random() * (open.length - 1));
return open[random];
},
notBadAI: function() {
// ttt.notBadAI() : AI with a little more intelligence
// (A) Init
var move = null;
var check = function(first, direction, pc) {
// checkH() : helper function, check possible winning row
// PARAM square : first square number
// direction : "R"ow, "C"ol, "D"iagonal
// pc : 0 for player, 1 for computer
var second = 0,
third = 0;
if (direction === "R") {
second = first + 1;
third = first + 2;
} else if (direction === "C") {
second = first + 3;
third = first + 6;
} else {
second = 4;
third = first === 0 ? 8 : 6;
}
if (
ttt.board[first] === null &&
ttt.board[second] === pc &&
ttt.board[third] === pc
) {
return first;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === null &&
ttt.board[third] === pc
) {
return second;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === pc &&
ttt.board[third] === null
) {
return third;
}
return null;
};
// (B) Priority #1 - Go for the win
// (B1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 1);
if (move !== null) {
break;
}
}
// (B2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 1);
if (move !== null) {
break;
}
}
}
// (B3) Check diagonal
if (move === null) {
move = check(0, "D", 1);
}
if (move === null) {
move = check(2, "D", 1);
}
// (C) Priority #2 - Block player from winning
// (C1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 0);
if (move !== null) {
break;
}
}
// (C2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 0);
if (move !== null) {
break;
}
}
}
// (C3) Check diagonal
if (move === null) {
move = check(0, "D", 0);
}
if (move === null) {
move = check(2, "D", 0);
}
// (D) Random move if nothing
if (move === null) {
move = ttt.dumbAI();
}
return move;
}
};
document.addEventListener("DOMContentLoaded", ttt.reset());
这是我目前基于 class 的版本:
import "./styles.css";
class Gameboard {
constructor() {
this.board = [];
this.container = document.getElementById("ttt-game");
this.container.innerHTML = "";
}
reset() {
this.board = [];
}
build() {
for (let i = 0; i < 9; i++) {
this.board.push(null);
const square = document.createElement("div");
square.innerHTML = " ";
square.dataset.idx = i;
square.id = "ttt-" + i;
square.addEventListener("click", () => {
// What method do I envoke here?
console.log(square)
});
this.container.appendChild(square);
}
}
};
class Game {
constructor() {
this.gameBoard = new Gameboard();
this.player = new Player();
this.computer = new Computer();
}
play() {
this.gameBoard.build();
}
};
class Player {
};
class Computer {
};
class DumbAI {
};
const game = new Game();
document.addEventListener("DOMContentLoaded", game.play());
我的 HTML 文件非常简单,只有一个 <div id="ttt-game"></div>
开始,CSS 文件是 grid
。
我遇到的最大问题是在 Game
中捕获 squares
。我应该把 eventListeners
放在哪里? (我的下一个项目是做一个React版本)。
这是我的想法,良好的、可维护的和可测试的代码看起来像:一堆小的、独立的函数,每个函数都有尽可能少的副作用。而不是让状态散布在应用程序周围,状态应该存在于一个单一的中央位置。
所以,我所做的是将您的代码分解成小函数。我已将状态拉入一个强制不变性的单一存储中。没有奇怪的中转站——应用程序状态改变,或者没有改变。如果它发生变化,整个游戏将被重新渲染。与 UI 交互的责任存在于单个 render
函数中。
并且您在问题中询问了 classes。 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) { ... }
}
...等等。
但我问了一个反问:添加这些 class 是否会给代码添加任何内容?在我看来,class 面向对象存在三个基本和内在的问题:
- 它引导您走上混合应用程序状态和功能的道路,
- 类 就像滚雪球 - 它们会增加功能并迅速变得过大,并且
- 人们很难想出有意义的 class 本体
以上所有意味着 classes 总是导致严重无法维护的代码。
我认为没有 new
和没有 this
.
index.js
import { createGame } from "./create-game.js";
const game = createGame("#ttt-game");
game.start();
创建-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 })
};
}
初始-state.js
export const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
创建-store.js
export function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
render.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);
}
}
正方形-click.js
import { play } from "./play.js";
export function onSquareClick(store, { target }) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
winners.js
export const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
重置-game.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>