From f6194795f9c98332d3f34b21bab847e8a9f13c8d Mon Sep 17 00:00:00 2001 From: VicenteVivan Date: Fri, 22 Apr 2022 00:38:39 -0400 Subject: [PATCH 1/3] Created UI Changes --- backend/app.py | 167 +++++++++++++++++++++++++++++++++++--- frontend/display-board.js | 77 ++++++++++-------- frontend/index.html | 59 ++++++++++---- frontend/script.js | 158 ++++++++++++++++++++++++------------ 4 files changed, 345 insertions(+), 116 deletions(-) diff --git a/backend/app.py b/backend/app.py index a033676..6add021 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,15 +1,54 @@ #app.py +from pickle import GLOBAL from flask import Flask, request, jsonify, send_from_directory from flask_cors import CORS import numpy as np import mnk, mcts_demo -print (np.__version__) +# Libraries for SocketIO (Live Connection with Server) +from flask import render_template, session +from flask_socketio import SocketIO + app = Flask(__name__) -CORS(app) +socketio = SocketIO(app) +#CORS(app) + +# ======================================================== # +# Auxiliary Functions # +# ======================================================== # + +def unpack(dimensions): + """Given a string of the form "nxm", returns a tuple (n, m). + + Args: + dimensions (str): A string of the form "nxm". + + Returns: + typle: A tuple (n, m). + """ + size = str(dimensions) + return tuple(map(int, size.split('x'))) + +def toClientMatrix(board): + """Given a board object, return its matrix representation + to be interpreted by the client. (1 counter-clockwise rotation) + of the raw board matrix. (1 = 'X', -1 = 'O', 0 = ' ') + + Args: + board (mnk.Board): A board object. + + Returns: + np.ndarray: Matrix representation of the board. + """ + return np.rot90(board.board).tolist() -@app.route('/', methods=['GET']) + +# ======================================================== # +# Main Routing # +# ======================================================== # + +@app.route('/') def index(): return send_from_directory('../frontend/', 'index.html') @@ -17,13 +56,16 @@ def index(): def serve(webpage): return send_from_directory('../frontend/', webpage) -# Converts size "nxm" to a tuple -def unpack(dimensions): - size = str(dimensions) - return tuple(map(int, size.split('x'))) + +# ======================================================== # +# Get Boards (APIs) # +# ======================================================== # @app.route('/board/random//') def random_nxm(dimensions): + """Return an random matrix with 1s, 0s, and -1s of the + given dimensions. = string of the form "nxm". + """ size = unpack(dimensions) # Return (n,m) matrix with random numbers [-1, 0, 1] @@ -36,16 +78,117 @@ def random_nxm(dimensions): # Return the matrix as a JSON object, with name "board" return jsonify(board=matrix) + @app.route('/board/empty//') def empty_nxm(dimensions): + """Return an empty matrix of the given dimensions. + = string of the form "nxm". + """ size = unpack(dimensions) return jsonify(board=np.zeros(size).tolist()) -@app.route('/play', methods=['POST']) -def play(): - currentBoard = request.json - return '' +# ======================================================== # +# User Events (Web Sockets) # +# ======================================================== # + +@socketio.on("connection") +def new_connection(json): + print("New Connection") + # route = json['route'] + # print(route) + # Start a new game (7x7x3 Default) + session["board"] = mnk.Board(7, 3, 3) + + #socketio.emit("board_update", session["board"].board.tolist()) + + print(session["board"].board) + +@socketio.on("new_game") +def new_game(json): + print("New Game") + + print(json['k']) + # { m: m, n:n, k: k } + m = int(json['m']) + n = int(json['n']) + k = int(json['k']) + + # Start a new game + session["board"] = mnk.Board(m, n, k) + print(session["board"].board) + socketio.emit("board_update", session["board"].board.tolist()) + + +@socketio.on("user_move") +def user_move(json): + board = session["board"] + iterations = 1000 # Hard Coded for now + # (TODO: Change iterations to user input. Send as JSON from client) + + # Get the user's move. Json = {i : row, j : column} + i = json["i"] + j = json["j"] + + print("User Move:", i, j) + print("Board Max:", board.m, board.n) + # ======================================================== # + # Note: the moves are somewhat messed up, because the board is + # represented as a matrix differently in the server and + # client. To fix it we do the following: + + # 1. Invert i <-> j (Could have done this above, but it's a bit more clear this way) + # i, j = j + 1, board.m - i + + # 2 Invert the board vertically (i.e. invert j) + #j = board.m - j - 1 + # + # End of Note :) + # ======================================================== # + print("User Move (Change):", i, j) + print("Board Max:", board.m, board.n) + # Make the move on the board + board.move(i, j) # <---- + print(board) + print(board.board) + print(toClientMatrix(board)) + + # Check user's move for win + if board.who_won() != 2: + # User won + socketio.emit("win", 1) + return + + # Get move from MCTS + print('AI is thinking') + root = mcts_demo.Node() + if root.isLeaf: + root.expand(board.legal_moves()) + + root = mcts_demo.AI(board, root, iterations) + board.move(*root.last_move) + + # Check AI's move for win + if board.who_won() != 2: + socketio.emit("win", 0) + + socketio.emit("board_update", board.board.tolist()) + + # Send the board to the client + # message = { + # "board": toClientMatrix(board), + # "who_won": board.who_won() + # } + + + + +# ======================================================== # +# Start Server # +# ======================================================== # if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + # app.run(debug=True) + socketio.run(app) + + diff --git a/frontend/display-board.js b/frontend/display-board.js index bc8a587..36bd616 100644 --- a/frontend/display-board.js +++ b/frontend/display-board.js @@ -1,56 +1,65 @@ -env = 'http://127.0.0.1:5000' -var n = 7 -var m = 7 +env = "http://127.0.0.1:5000"; +var m = 7; +var n = 3; main(); async function main() { - board = await getEmptyBoard(env, n, m) - displayBoard(board) + board = await getEmptyBoard(env, m, n); + displayBoard(board); - // Just testing this - await postBoard(board) + // Just testing this + await postBoard(board); } async function postBoard(board) { - await fetch(`${env}/play`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(board) - }) + await fetch(`${env}/play`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(board), + }); } -async function getRandomBoard(env, n, m) { - return (await fetch(`${env}/board/random/${n}x${m}/`).then(response => { return response.json() })).board +async function getRandomBoard(env, m, n) { + return ( + await fetch(`${env}/board/random/${m}x${n}/`).then((response) => { + return response.json(); + }) + ).board; } -async function getEmptyBoard(env, n, m) { - return (await fetch(`${env}/board/empty/${n}x${m}/`).then(response => { return response.json() })).board +async function getEmptyBoard(env, m, n) { + return ( + await fetch(`${env}/board/empty/${m}x${n}/`).then((response) => { + return response.json(); + }) + ).board; } function displayBoard(board) { + var m = board.length; + var n = board[0].length; - var n = board.length - var m = board[0].length + let cellSize = 100 - Math.min((Math.max(m, n) - 3) * 8, 50); + document.documentElement.style.setProperty("--cell-size", `${cellSize}px`); - let cellSize = 100 - Math.min(((Math.max(n, m) - 3) * 8), 50) - document.documentElement.style.setProperty("--cell-size", `${cellSize}px`) + boardHtml = document.getElementById("board"); - boardHtml = document.getElementById('board') + boardHtml.style.setProperty("grid-template-columns", `repeat(${n}, 1fr)`); - boardHtml.style.setProperty("grid-template-columns", `repeat(${m}, 1fr)`) + boardHtml.style.setProperty("height", `${m} * var(--cell-size) + ${m - 1} * var(--gap-size)`); + boardHtml.style.setProperty("width", `${n} * var(--cell-size) + ${n - 1} * var(--gap-size)`); - boardHtml.style.setProperty("width", `${m} * var(--cell-size) + ${m-1} * var(--gap-size)`) - boardHtml.style.setProperty("height", `${n} * var(--cell-size) + ${n-1} * var(--gap-size)`) + let cellType = [" O", "", " X"]; + let display = ""; - let cellType = [" O", "", " X"] - let display = "" + for (var i = 0; i < m; i++) + for (var j = 0; j < n; j++) + display += `
`; - for (var i = 0; i < n; i++) - for (var j = 0; j < m; j++) - display += `
` - - boardHtml.innerHTML = display -} \ No newline at end of file + boardHtml.innerHTML = display; +} diff --git a/frontend/index.html b/frontend/index.html index 9bae58e..66782da 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,23 +1,48 @@ + + + + + + + + + Tic-Tac-Toe + - - - - - - - Tic-Tac-Toe - + + TODO: Win detection +
+
m x n x k?
+ +
+ + x + + x + +
+
+ +
+ +
- - TODO: Win detection -
- -
-
- -
- +
+
+
+ +
+ diff --git a/frontend/script.js b/frontend/script.js index b0b9f2d..72779ac 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -1,71 +1,123 @@ -var cells, board, winningMessage, winningMessageText, newGameButton, xIsNext +var cells, board, winningMessage, winningMessageText, newGameButton, xIsNext; // I wrote this terrible hacky code in this way because newGame() MUST trigger AFTER displayBoard() // has fully finished and all elements have been loaded onto the page, since it operates on those elements. // Adding a forced delay of 0.5s was just the easiest (but def not best) way to make it work window.onpageshow = (event) => { - setTimeout(() => { - newGame() - newGameButton.addEventListener('click', newGame) - }, 500); + setTimeout(() => { + console.log("Page has been shown"); + newGame(); + newGameButton.addEventListener("click", newGame); + }, 500); }; +// Change size of board based on user input +document.getElementById("start").addEventListener("click", () => { + changeMNK( + document.getElementById("m").value, + document.getElementById("n").value, + document.getElementById("k").value + ); +}); + +function updateBoard(board) { + displayBoard(board); + + cells = document.querySelectorAll(".cell"); + + cells.forEach((cell) => { + cell.addEventListener("click", handleClick, { once: true }); + }); +} + function newGame() { - cells = document.querySelectorAll('.cell') - board = document.getElementById('board') - winningMessage = document.querySelector('.winningMessage') - winningMessageText = document.querySelector('.winningMessageText') - newGameButton = document.querySelector('.newGameButton') - xIsNext = true - - board.classList.add('X') - board.classList.remove('O') - winningMessage.classList.remove('show') - - cells.forEach(cell => { - cell.classList.remove('X') - cell.classList.remove('O') - cell.addEventListener('click', handleClick, {once: true}) - }) + socket.emit("new_game", { m: m, n: n, k: k }); + console.log("Page has been shown 2"); + cells = document.querySelectorAll(".cell"); + board = document.getElementById("board"); + winningMessage = document.querySelector(".winningMessage"); + winningMessageText = document.querySelector(".winningMessageText"); + newGameButton = document.querySelector(".newGameButton"); + xIsNext = true; + + board.classList.add("X"); + board.classList.remove("O"); + winningMessage.classList.remove("show"); + + cells.forEach((cell) => { + cell.classList.remove("X"); + cell.classList.remove("O"); + cell.addEventListener("click", handleClick, { once: true }); + }); +} + +function getCoordinates(str) { + // Given a string in the form "i, j" return the coordinates as an array [i, j] + var coordinates = str.split(","); + return [parseInt(coordinates[0]), parseInt(coordinates[1])]; +} + +async function changeMNK(m, n, k) { + board = await getEmptyBoard(env, m, n); + displayBoard(board); + newGame(); + + // Post to server signal that new n, m, k have been chosen + socket.emit("new_game", { m: m, n: n, k: k }); } function handleClick(e) { - // place mark - const cell = e.target - const player = xIsNext ? 'X' : 'O' - cell.classList.add(player) - // check for win - if (checkForWin(player)) { - winningMessageText.innerText = `${xIsNext ? 'X' : 'O'} Wins!` - winningMessage.classList.add('show') - } - // check for draw - else if ([...cells].every(cell => { - return cell.classList.contains('X') || cell.classList.contains('O') - })) { - winningMessageText.innerText = `It's a draw.` - winningMessage.classList.add('show') - } - // switch turns - else - { - xIsNext = !xIsNext - if (xIsNext) { - board.classList.add('X') - board.classList.remove('O') - } else { - board.classList.add('O') - board.classList.remove('X') - } - } + // place mark + const cell = e.target; + const player = xIsNext ? "X" : "O"; + cell.classList.add(player); + + // check for draw + if ( + [...cells].every((cell) => { + return cell.classList.contains("X") || cell.classList.contains("O"); + }) + ) { + winningMessageText.innerText = `It's a draw.`; + winningMessage.classList.add("show"); + } + // switch turns + else { + // xIsNext = !xIsNext; + if (xIsNext) { + board.classList.add("X"); + board.classList.remove("O"); + } else { + board.classList.add("O"); + board.classList.remove("X"); + } + } + + // Post to move to server. Send coordinates previously encoded in cell ID. + const coordinates = getCoordinates(e.target.getAttribute("id")); + socket.emit("user_move", { i: coordinates[0], j: coordinates[1] }); } +// check for win +function displayWinningMessage(who_won) { + winningMessageText.innerText = `${who_won ? "X" : "O"} Wins!`; + winningMessage.classList.add("show"); +} + +// Receives board from server and displays it +socket.on("board_update", (board) => { + updateBoard(board); +}); + +socket.on("win", (who_won) => { + displayWinningMessage(who_won); +}); + // TODO function checkForWin(player) { + return false; - return false; - - /* + /* var m = 7, n = 7 var k_in_a_row = 3 @@ -78,7 +130,7 @@ function checkForWin(player) { // TODO function winFromPos(r, c, k_in_a_row) { - /* + /* var m = 7, n = 7 var k_in_a_row = 3 From 5c439f4b4a095cb65b87ceb13f8e467aa1b786bb Mon Sep 17 00:00:00 2001 From: VicenteVivan Date: Fri, 22 Apr 2022 15:21:44 -0400 Subject: [PATCH 2/3] UI & General case --- frontend/display-board.js | 3 +- frontend/index.html | 40 ++++++--- frontend/script.js | 38 +++------ frontend/style.css | 168 +++++++++++++++++++++++++------------- 4 files changed, 153 insertions(+), 96 deletions(-) diff --git a/frontend/display-board.js b/frontend/display-board.js index 36bd616..a66b482 100644 --- a/frontend/display-board.js +++ b/frontend/display-board.js @@ -1,6 +1,7 @@ -env = "http://127.0.0.1:5000"; +env = "https://team-game-bot.herokuapp.com/"; var m = 7; var n = 3; +var k = 3; main(); diff --git a/frontend/index.html b/frontend/index.html index 66782da..fb53c6c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,8 @@ + +