Преобразование рабочего функционала Javascript Ti c Ta c Toe в класс на основе практики OOP - PullRequest
0 голосов
/ 19 марта 2020

Я делаю упражнение для себя, чтобы лучше понять OOP дизайн, взяв рабочую Javascript функциональную игру Ti c Ta c Toe с ИИ на классовую. Я застреваю в обычных вопросах о том, что ставить, где на уроках, единый источник правды, слабая связь и т. Д. c. Не ищите полные ответы здесь, но, возможно, некоторые намеки на лучшую стратегию?

Вот оригинальный рабочий функционал 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).

1 Ответ

1 голос
/ 20 марта 2020

Вот, что я думаю, хороший, обслуживаемый и тестируемый код выглядит следующим образом: набор небольших автономных функций, каждая из которых имеет как можно меньше побочных эффектов. И вместо того, чтобы распределять состояния по всему приложению, состояние должно существовать в едином центральном месте.

Итак, я решил разбить ваш код на маленькие функции. Я вытащил государство в единый магазин, который обеспечивает неизменность. Никаких странных промежуточных домов - состояние приложения меняется или нет. Если это изменится, вся игра будет перерисована. Ответственность за взаимодействие с пользовательским интерфейсом существует в одной 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 проблемы с ориентацией на классы объектов:

  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 })
  };
}

начальное состояние. 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>
...