我正在通过将具有AI的可正常运行的Javascript功能的Tic Tac Toe游戏带到基于Class的游戏中,以使自己更好地理解OOP设计。我陷入了通常的问题,即在课堂上放置什么,单一的真理来源,松散的耦合等等。不是在这里寻找完整的答案,而是在暗示一些更好的策略?
这是原始的工作功能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());
这是我到目前为止基于类的版本:
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
。
我最大的问题是在squares
中捕获Game
。我应该把eventListeners
放在哪里? (我的下一个项目是做一个React版本)。
我认为,好的,可维护的和可测试的代码看起来像:一堆小而独立的函数,每个函数都有尽可能少的副作用。状态应该分散在应用程序的中央,而不是分散在应用程序中。
所以,我要做的是将您的代码分解成小的函数。我已经将状态拉入一个强制不变性的商店中。没有奇怪的中途之门-应用程序状态改变了,或者没有改变。如果更改,将重新渲染整个游戏。与UI交互的责任存在于单个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) { ... }
}
...依此类推。
但是我提出了一个反问:将这些类添加到代码中会添加任何内容吗?在我看来,面向类的面向对象存在三个基本和内在问题:
以上所有内容都意味着类总是导致严重不可维护的代码。
我认为代码通常在没有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 })
};
}
initial-state.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);
}
};
}
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);
}
}
on-square-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"
};
reset-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>