diff --git a/README.md b/README.md index 2402a1f..c913a2a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ # odin-tic-tac-toe -Tic Tac Toe \ No newline at end of file +Tic Tac Toe + +using IIFE to prevent unwanted user-forced changes, using addEventListeners + +utilizing pubsub, and splitting functions into parts used by others for code readability :) + +## todo: + +* display points, turn, etc - info in GUI +* "setup screen" on first startup +* reset everything btn +* localstorage or other similar implementation to save progress and players +* edit player settings - name, marker + +# preview: +![preview](image.png) \ No newline at end of file diff --git a/image.png b/image.png new file mode 100644 index 0000000..0f47d1f Binary files /dev/null and b/image.png differ diff --git a/index.html b/index.html index d1d65a6..aae516b 100644 --- a/index.html +++ b/index.html @@ -6,9 +6,13 @@ Document + +
+
+
\ No newline at end of file diff --git a/pubsub.js b/pubsub.js new file mode 100644 index 0000000..7dc078a --- /dev/null +++ b/pubsub.js @@ -0,0 +1,46 @@ +// PubSub from: https://gist.github.com/fatihacet/1290216 +var pubsub = {}; +(function(q) { + var topics = {}, subUid = -1; + q.subscribe = function(topic, func) { + if (!topics[topic]) { + topics[topic] = []; + } + var token = (++subUid).toString(); + topics[topic].push({ + token: token, + func: func + }); + return token; + }; + + q.publish = function(topic, args) { + if (!topics[topic]) { + return false; + } + setTimeout(function() { + var subscribers = topics[topic], + len = subscribers ? subscribers.length : 0; + + while (len--) { + subscribers[len].func(topic, args); + } + }, 0); + return true; + + }; + + q.unsubscribe = function(token) { + for (var m in topics) { + if (topics[m]) { + for (var i = 0, j = topics[m].length; i < j; i++) { + if (topics[m][i].token === token) { + topics[m].splice(i, 1); + return token; + } + } + } + } + return false; + }; +}(pubsub)); \ No newline at end of file diff --git a/script.js b/script.js index e69de29..0363af6 100644 --- a/script.js +++ b/script.js @@ -0,0 +1,193 @@ +"use strict"; + +function initiateGame(playersInput) { + const game = (function() { + const mainGameDiv = document.querySelector("#main-game"); + let gameboard = new Array(9); + gameboard.fill(null); + + // ----------------- + + function checkWin() { + const combinations = [ + "123", "456", "789", + "147", "258", "369", + "159", "357", + ]; + + let combinationsValidate = {}; + const allEqualInArray = arr => arr.every( v => v === arr[0] ) + + const player = activePlayer.player; + let win = false; + combinations.forEach(combination => { + combinationsValidate[combination] = []; + combination.split('').forEach(box => { + combinationsValidate[combination].push(gameboard[+box - 1] == player.marker); + }) + if (allEqualInArray(combinationsValidate[combination]) && combinationsValidate[combination][0]) { + player.addPoint(); + console.log(`${JSON.stringify({name:player.name, marker:player.marker, points:player.points})} won`); + pubsub.publish("winTrue"); + return; + } + }); + if (!win) { + if (gameboard.find(field => field === null) === null) { + pubsub.publish("continueGame"); + } else { + pubsub.publish("draw"); + } + } + } + + // ----------------- + + function addDismissivePopup(popup, popupContent) { + function dismissPopup() { + popup.remove(); + } + popup.addEventListener("click", function(event) { + if (this !== event.target) return; + dismissPopup(); + }, false); + function dismissPopupKey(event) { + if (event.key.toLowerCase() == "escape") { + popup.remove(); + document.body.removeEventListener("keydown", dismissPopupKey); + } + } + + const dismissBtn = document.createElement("button"); + dismissBtn.setAttribute("type", "button"); + dismissBtn.classList.add("dismiss-button"); + dismissBtn.textContent = "Dismiss"; + dismissBtn.addEventListener("click", dismissPopup); + document.body.addEventListener("keydown", dismissPopupKey); + + return dismissBtn; + } + + function generatePopup(dismissive) { + const popup = document.createElement("div"); + popup.classList.add("popup"); + const popupContent = document.createElement("div"); + popupContent.classList.add("popup-content"); + popup.append(popupContent); + document.body.append(popup); + + if (dismissive) { + const dismissBtn = addDismissivePopup(popup, popupContent); + return {popup, popupContent, dismissBtn}; + } + return {popup, popupContent}; + } + function popupWin() { + const {popup, popupContent, dismissBtn} = generatePopup(true); + popupContent.textContent = `${activePlayer.player.name} (${activePlayer.player.marker}) won!`; + popupContent.append(dismissBtn); + } + function popupDraw() { + const {popup, popupContent, dismissBtn} = generatePopup(true); + popupContent.textContent = `It's a draw!`; + popupContent.append(dismissBtn); + } + + // ----------------- + + function clearController() { + while(mainGameDiv.lastElementChild) { + mainGameDiv.lastElementChild.remove(); + } + } + function displayController() { + clearController(); + let boxes = []; + for (let i = 0; i < 9; i++) { + let boxDiv = document.createElement("div"); + boxDiv.classList.add("box"); + boxDiv.setAttribute("data-id", i); + boxDiv.textContent = gameboard[boxDiv.getAttribute("data-id")]; + boxDiv.addEventListener("click", function(event) { + changeField(boxDiv.getAttribute("data-id")); + }); + boxes.push(boxDiv); + } + boxes.forEach(box => mainGameDiv.appendChild(box)); + } + + // ----------------- + + class Player { + constructor(name, marker) { + this.name = name; + this.marker = marker; + this.points = 0; + } + changeName(newName) { + this.name = newName; + } + changeMarker(newMarker) { + this.marker = newMarker; + } + addPoint() { + this.points++; + } + } + let players = { + player1: new Player(playersInput.player1.name, playersInput.player1.marker), + player2: new Player(playersInput.player2.name, playersInput.player2.marker), + } + let activePlayer = Object.create({ + change() { + this.player == players.player1 + ? this.player = players.player2 + : this.player = players.player1; + }, + set(player) { + if (player instanceof Player) { + this.player = player; + } else { + throw new Error("No such player exists"); + } + } + }); + + // ----------------- + + function changeField(field) { + + if (gameboard[field] === null) { + gameboard[field] = activePlayer.player.marker; + pubsub.publish("fieldChange"); + } else if (gameboard[field] === undefined) { + console.warn("No such field exists"); + } else { + console.warn("Value already set"); + } + } + + // ----------------- + + pubsub.subscribe("fieldChange", displayController); + pubsub.subscribe("fieldChange", checkWin); + pubsub.subscribe("continueGame", () => {activePlayer.change()}); + pubsub.subscribe("draw", popupDraw); + pubsub.subscribe("winTrue", popupWin); + pubsub.subscribe("draw", () => {gameboard.fill(null);}) + pubsub.subscribe("winTrue", () => {gameboard.fill(null);}) + displayController(); + activePlayer.change() + })(); +} + +initiateGame( + {player1: { + name: "Oskar", + marker: "O", + }, + player2: { + name: "NetMan", + marker: "X", + }} +); \ No newline at end of file diff --git a/style.css b/style.css index e69de29..6b4cadd 100644 --- a/style.css +++ b/style.css @@ -0,0 +1,74 @@ +:root { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +html, body { + min-height: 100dvh; +} + +#center-contain { + display: flex; + justify-content: center; + align-items: center; + min-height: 100dvh; +} + +#main-game { + display: grid; + grid-template: repeat(3, 80px) / repeat(3, 80px); + padding: 10px; + gap: 10px; +} + +.box { + border: 1px solid #000; + box-sizing: border-box; + background-color: #e2e2e2; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 3rem; + transition: 0.1s background-color ease-in-out, + 0.1s transform ease-in-out; +} + +.box:hover { + background-color: #f8f8f8; + transform: scale(1.03); +} + +.box:active { + background-color: #efefef; + transform: scale(0.99); +} + +.popup { + background: #00000044; + backdrop-filter: blur(6px); + display: flex; + justify-content: center; + align-items: center; + min-height: 100dvh; + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +.popup-content { + border: 2px solid #000000aa; + padding: 25px; + border-radius: 10px; + background: #ccc; + color: #000; +} + +.dismiss-button { + padding: 5px; + border-radius: 8px; + background-color: #fff; + color: #000; + border: 1px solid #000; + cursor: pointer; +} \ No newline at end of file