瀏覽代碼

Initial commit.

simondevyoutube 5 年之前
父節點
當前提交
d7a7c2900c
共有 4 個文件被更改,包括 389 次插入0 次删除
  1. 70 0
      index.html
  2. 319 0
      main.js
  3. 二進制
      o.png
  4. 二進制
      x.png

+ 70 - 0
index.html

@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Javascript Beginner Projects: Tic Tac Toe</title>
+  <style>
+    .header {
+      font-size: 3em;
+      color: white;
+      background: #404040;
+      text-align: center;
+      height: 2.5em;
+      text-shadow: 4px 4px 4px black;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .container {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      margin: 1em;
+    }
+
+    #description {
+      font-size: 2em;
+      color: white;
+      height: 50px;
+      text-shadow: 2px 2px 2px black;
+    }
+
+    .row {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+    }
+
+    .square {
+      width: 192px;
+      height: 192px;
+      background: white;
+      border-radius: 16px;
+      border: 4px solid grey;
+      margin: 4px;
+    }
+
+    .highlight {
+      border-color: red;
+    }
+
+    body {
+      background: #202020;
+      margin: 0;
+    }
+  </style>
+</head>
+<body>
+  <div class="header">
+    <span>Tic Tac Toe!</span>
+  </div>
+  <div class="container">
+    <div id="description"></div>
+  </div>
+  <div id="rows">
+  </div>
+  <script src="main.js" type="module">
+  </script>
+</body>
+</html>

+ 319 - 0
main.js

@@ -0,0 +1,319 @@
+console.log('JavaScript Beginner Projects: Tic Tac Toe!');
+
+window.addEventListener('DOMContentLoaded', () => {
+  _Setup();
+});
+
+const BOARD_STATE = {
+  player: 1,
+  ai: 2,
+  blank: 3,
+  draw: 4,
+};
+
+let _GAMESTATE = null;
+
+function _Setup() {
+  _CreateBoard();
+  _InitializeState();
+}
+
+function _CreateBoard() {
+  const rows = document.getElementById('rows');
+  for (let x = 0; x < 3; x++) {
+    const curRow = document.createElement('div');
+    curRow.id = 'row' + x;
+    curRow.className = 'row';
+    rows.appendChild(curRow);
+
+    for (let y = 0; y < 3; y++) {
+      const node = document.createElement('img');
+      node.className = 'square';
+      node.id = x + '.' + y;
+      node.onclick = _HandlePlayerClick;
+      curRow.appendChild(node);
+    }
+  }
+}
+
+function _InitializeState() {
+  _GAMESTATE = {
+    turn: 'player',
+    active: true,
+  };
+}
+
+function _HandlePlayerClick(evt) {
+  const isBlank = !evt.target.src.length;
+  if (isBlank &&
+      _GAMESTATE.active &&
+      _GAMESTATE.turn == 'player') {
+    evt.target.src = 'x.png';
+    _CheckGameOver();
+    _AISelectMove();
+  }
+}
+
+function _CheckGameOver() {
+  const winner = _EvaluateBoard(_GetBoardStates());
+  if (winner == null) {
+    return;
+  }
+
+  _GAMESTATE.active = false;
+
+  let desc = '';
+
+  if (winner == BOARD_STATE.ai) {
+    desc = 'You lose!';
+  } else if (winner == BOARD_STATE.player) {
+    desc = 'You win!';
+  } else {
+    desc = 'Tie game, try again.'
+  }
+
+  document.getElementById('description').innerText = desc;
+}
+
+function _GetBoardStates() {
+  const boardStates = [];
+  for (let x = 0; x < 3; x++) {
+    const row = [];
+    for (let y = 0; y < 3; y++) {
+      const node = document.getElementById(x + '.' + y);
+      if (node.src.includes('x.png')) {
+        row.push(BOARD_STATE.player);
+      } else if (node.src.includes('o.png')) {
+        row.push(BOARD_STATE.ai);
+      } else {
+        row.push(BOARD_STATE.blank);
+      }
+    }
+    boardStates.push(row);
+  }
+  return boardStates;
+}
+
+function _GetSquareElementNodes() {
+  const nodes = [];
+  for (let x = 0; x < 3; x++) {
+    for (let y = 0; y < 3; y++) {
+      nodes.push(document.getElementById(x + '.' + y))
+    }
+  }
+  return nodes;
+}
+
+function _HighlightSquares(blinks) {
+  if (blinks === undefined) {
+    blinks = 10;
+  }
+
+  const nodes = _GetSquareElementNodes();
+  for (const n of nodes) {
+    n.className = 'square';
+  }
+  if (blinks >= 0) {
+    setTimeout(() => {
+      _AISelectMove(blinks - 1);
+    }, 100);
+    const x = Math.floor(Math.random() * 3);
+    const y = Math.floor(Math.random() * 3);
+    const node = document.getElementById(x + '.' + y);
+    node.className = 'square highlight';
+    return true;
+  }
+  return false;
+}
+
+function _AISelectMove(blinks) {
+  _GAMESTATE.turn = 'ai';
+
+  if (_HighlightSquares(blinks)) {
+    return;
+  }
+
+  const boardStates = _GetBoardStates();
+  const [_, choice] = _Minimax(boardStates, BOARD_STATE.ai);
+
+  if (choice != null) {
+    const [x, y] = choice;
+    document.getElementById(x + '.' + y).src = 'o.png';
+  }
+
+  _CheckGameOver();
+
+  _GAMESTATE.turn = 'player';
+}
+
+function _EvaluateBoard(boardStates) {
+  const winningStates = [
+    // Horizontals
+    [[0, 0], [0, 1], [0, 2]],
+    [[1, 0], [1, 1], [1, 2]],
+    [[2, 0], [2, 1], [2, 2]],
+    [[0, 0], [1, 0], [2, 0]],
+    [[0, 1], [1, 1], [2, 1]],
+    [[0, 2], [1, 2], [2, 2]],
+
+    // Diagonals
+    [[0, 0], [1, 1], [2, 2]],
+    [[2, 0], [1, 1], [0, 2]],
+  ];
+
+  for (const possibleState of winningStates) {
+    let curPlayer = null;
+    let isWinner = true;
+    for (const [x, y] of possibleState) {
+      const occupant = boardStates[x][y];
+      if (curPlayer == null && occupant != BOARD_STATE.blank) {
+        curPlayer = occupant;
+      } else if (curPlayer != occupant) {
+        isWinner = false;
+      }
+    }
+    if (isWinner) {
+      return curPlayer;
+    }
+  }
+
+  let hasMoves = false;
+  for (let x = 0; x < 3; x++) {
+    for (let y = 0; y < 3; y++) {
+      if (boardStates[x][y] == BOARD_STATE.blank) {
+        hasMoves = true;
+      }
+    }
+  }
+  if (!hasMoves) {
+    return BOARD_STATE.draw;
+  }
+
+  return null;
+}
+
+function _Minimax(boardStates, player) {
+  // First check if the game has already been won.
+  const winner = _EvaluateBoard(boardStates);
+  if (winner == BOARD_STATE.ai) {
+    return [1, null];
+  } else if (winner == BOARD_STATE.player) {
+    return [-1, null];
+  }
+
+  let move, moveScore;
+  if (player == BOARD_STATE.ai) {
+    [moveScore, move] = _Minimax_Maximize(boardStates);
+  } else {
+    [moveScore, move] = _Minimax_Minimize(boardStates);
+  }
+
+  if (move == null) {
+    moveScore = 0;
+  }
+
+  // No move, so it's a draw
+  return [moveScore, move];
+}
+
+function _Minimax_Maximize(boardStates) {
+  let moveScore = Number.NEGATIVE_INFINITY;
+  let move = null;
+
+  for (let x = 0; x < 3; x++) {
+    for (let y = 0; y < 3; y++) {
+      if (boardStates[x][y] == BOARD_STATE.blank) {
+        const newBoardStates = boardStates.map(r => r.slice());
+
+        newBoardStates[x][y] = BOARD_STATE.ai;
+
+        const [newMoveScore, _] =  _Minimax(
+            newBoardStates, BOARD_STATE.player);
+
+        if (newMoveScore > moveScore) {
+          move = [x, y];
+          moveScore = newMoveScore;
+        }
+      }
+    }
+  }
+
+  return [moveScore, move];
+}
+
+function _Minimax_Minimize(boardStates) {
+  let moveScore = Number.POSITIVE_INFINITY;
+  let move = null;
+
+  for (let x = 0; x < 3; x++) {
+    for (let y = 0; y < 3; y++) {
+      if (boardStates[x][y] == BOARD_STATE.blank) {
+        const newBoardStates = boardStates.map(r => r.slice());
+
+        newBoardStates[x][y] = BOARD_STATE.player;
+
+        const [newMoveScore, _] =  _Minimax(
+            newBoardStates, BOARD_STATE.ai);
+
+        if (newMoveScore < moveScore) {
+          move = [x, y];
+          moveScore = newMoveScore;
+        }
+      }
+    }
+  }
+
+  return [moveScore, move];
+}
+
+
+function _Minimax2(boardStates, aiTurn) {
+  // First check if the game has already been won.
+  const winner = _EvaluateBoard(boardStates);
+  if (winner == BOARD_STATE.ai) {
+    return [1, null];
+  } else if (winner == BOARD_STATE.player) {
+    return [-1, null];
+  }
+
+  let moveCost = Number.NEGATIVE_INFINITY;
+  if (!aiTurn) {
+    moveCost = Number.POSITIVE_INFINITY;
+  }
+  let move = null;
+
+  for (let x = 0; x < 3; x++) {
+    for (let y = 0; y < 3; y++) {
+      if (boardStates[x][y] == BOARD_STATE.blank) {
+        const newBoardStates = boardStates.map(r => r.slice());
+
+        if (aiTurn) {
+          newBoardStates[x][y] = BOARD_STATE.ai;
+        } else {
+          newBoardStates[x][y] = BOARD_STATE.player;
+        }
+
+        const [newMoveCost, _] =  _Minimax(newBoardStates, !aiTurn);
+
+        if (aiTurn) {
+          if (newMoveCost > moveCost) {
+            move = [x, y];
+            moveCost = newMoveCost;
+          }
+        } else {
+          if (newMoveCost < moveCost) {
+            move = [x, y];
+            moveCost = newMoveCost;
+          }
+        }
+      }
+    }
+  }
+
+  if (move != null) {
+    return [moveCost, move];
+  }
+
+  // No move, so it's a draw
+  return [0, null];
+}

二進制
o.png


二進制
x.png