将可运行的功能性Java Tic Tac Toe游戏转换为基于类以练习OOP

问题描述 投票:0回答:1

我正在通过将具有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 "&nbsp;" non-breaking space to square
      square.innnerHTML = "&nbsp;";

      // 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 = "&nbsp;";
      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版本)。

javascript oop tic-tac-toe
1个回答
1
投票

我认为,好的,可维护的和可测试的代码看起来像:一堆小而独立的函数,每个函数都有尽可能少的副作用。状态应该分散在应用程序的中央,而不是分散在应用程序中。

所以,我要做的是将您的代码分解成小的函数。我已经将状态拉入一个强制不变性的商店中。没有奇怪的中途之门-应用程序状态改变了,或者没有改变。如果更改,将重新渲染整个游戏。与UI交互的责任存在于单个render函数中。

您问了有关您问题的课程。 createGame变为:

class Game { 
  constructor() { ... }, 
  start() { ... }, 
  reset() { ... },
  play() { ... }
}

createStore变为:

class Store { 
  constructor() { ... }
  getState() { ... }, 
  setState() { ... } 
}

playAIplayHuman成为:

class AIPlayer {
  constructor(store) { ... }
  play() { ... }
}

class HumanPlayer {
  constructor(store) { ... }
  play() { ... }
}

checkForWinner变为:

class WinChecker {
  check(board) { ... }
}

...依此类推。

但是我提出了一个反问:将这些类添加到代码中会添加任何内容吗?在我看来,面向类的面向对象存在三个基本和内在问题:

  1. 它使您沿着混合应用程序状态和功能的路径前进,
  2. 类就像雪球-它们会增加功能并迅速变得过大,而
  3. 人们想出有意义的类本体论很可怕

以上所有内容都意味着类总是导致严重不可维护的代码。

我认为代码通常在没有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>
© www.soinside.com 2019 - 2024. All rights reserved.