commit 60f978f790bbafbfe3260c1ebd3fb1191e1ba8ed Author: hkri Date: Sun Jul 18 02:38:42 2021 +0800 Initial commit. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e2a321 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Tic-Tac-Toe + +A simple tic-tac-toe game. + +

+ screenshot +

+ +## Local Setup + +- Run with PHP local server, then open http://localhost:8080: + ```sh + php -S localhost:8080 -t dist + ``` +- Directly open `dist/index.html` on a web browser. diff --git a/dist/css/app.css b/dist/css/app.css new file mode 100644 index 0000000..88532d7 --- /dev/null +++ b/dist/css/app.css @@ -0,0 +1,244 @@ +* { + outline: none; +} + +body { + background-color: #171717; + font-family: sans-serif; +} + +body:after { + content: ''; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.3; + background-image: url(data:image/png;base64,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); + pointer-events: none; + z-index: 0; +} + +main { + margin: 0 auto; + max-width: 420px; + min-width: 280px; +} + +@media(max-width: 864px) { + main { + padding: 0 16px; + } +} + +footer { + margin-top: 4em; + text-align: center; + font-size: 12px; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.7); + text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.9); +} + +footer a { + text-decoration: none; + color: inherit; +} + +.metallic { + background: rgb(238,238,238); + background: linear-gradient(0deg, rgba(238,238,238,1) 0%, rgba(212,212,212,1) 50%, rgba(223,223,223,1) 50%, rgba(255,255,255,1) 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.shadow { + text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.25); +} + +/* Player Panel */ + +section.player-panel { + width: 100%; + height: 55px; + margin-bottom: 16px; + display: flex; + justify-content: space-between; +} + +section.player-panel button { + position: relative; + height: 100%; + width: 25%; + padding: 0 1em; + box-sizing: border-box; + border: 1px solid rgba(0, 0, 0, 0.8); + background: linear-gradient(0deg, rgba(40, 40, 40, 1) 0%, rgba(52, 52, 53, 1) 100%); + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.6); + border-radius: 6px; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.9); + overflow: hidden; +} + +section.player-panel button:before { + content: ' '; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-top: 1px solid rgb(255, 255, 255, 0.1); + border-radius: 6px; + box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 0.2); +} + +section.player-panel button:after { + content: ' '; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-bottom: 3px solid rgb(0, 0, 0, 0.4); + border-radius: 6px; + box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 0.2); + transition: box-shadow 0.1s ease-in-out; +} + +section.player-panel button:hover:after { + box-shadow: inset 0px 0px 3px rgba(9, 167, 240, 0.7); +} + +section.player-panel :not(button[disabled]):active { + padding-top: 2px; + box-shadow: 0 0 1px rgba(255, 255, 255, 0.2); +} + +section.player-panel :not(button[disabled]):active:before { + border: none; + box-shadow: none; +} + +section.player-panel :not(button[disabled]):active:after { + box-shadow: inset 0px 0px 16px rgba(0, 0, 0, 0.8); +} + +section.player-panel button[disabled] { + box-shadow: 0 1px 1px rgba(255, 255, 255, 0.08); + background: linear-gradient(0deg, rgba(42, 42, 42, 1) 0%, rgba(47, 47, 47, 1) 100%); +} + +section.player-panel button[disabled]:before, +section.player-panel button[disabled]:after { + border: none; + box-shadow: none; +} + +section.player-panel button[disabled]:before { + border: 1px solid rgb(255, 255, 255, 0.08); +} + +section.player-panel button[disabled]:hover:after { + box-shadow: none; +} + +section.player-panel button.player-select { + width: 30%; +} + +section.player-panel button span.light { + position: absolute; + width: 16px; + height: 5px; + background-color: rgb(0, 0, 0, 0.7); + left: 50%; + top: 8px; + transform: translateX(-50%) translateY(-50%); + border-top: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 2px; + transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +section.player-panel :not(button[disabled]):active span.light { + top: 9px; + transition: background-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out; +} + +section.player-panel button span.light.blue { + background-color: rgb(12, 158, 255); + box-shadow: 0 0 10px rgba(10, 165, 255, 0.8); +} + +section.player-panel button span.light.red { + background-color: rgba(255, 12, 12); + box-shadow: 0 0 10px rgb(255, 21, 25); +} + +section.player-panel button span.light.green { + background-color: rgb(59, 255, 131); + box-shadow: 0 0 10px rgb(45, 255, 80); +} + +/* /Player Panel */ + +/* Stage */ + +section.stage { + overflow: hidden; + display: grid; + background-color: #0a0a0a; + box-shadow: 0px 0px 3px rgba(255, 255, 255, 0.15); + border: 2px solid #020205; + border-radius: 16px; + grid-template-columns: 33.33% 33.33% 33.33%; +} + +section.stage button { + color: white; + font-size: 100px; + height: 180px; + padding: 0; + outline: none; + border: 1px solid #131314; + background-color: rgb(10, 10, 10); + box-shadow: inset 0px 0px 8px rgba(0, 0, 0, 1); + border-radius: 8px; + transition: border 0.15s ease-in-out, + box-shadow 0.15s ease-in-out, + background-color 0.15s ease-in-out; +} + +section.stage button[disabled].circle { + color: rgb(243, 43, 43); + text-shadow: 0px 0px 5px rgba(255, 104, 104, 0.3); + background-color: rgba(255, 0, 0, 0.04); +} + +section.stage :not(button[disabled]).circle:hover { + border: 1px solid rgba(255, 0, 0, 0.5); + box-shadow: inset 0px 0px 32px rgba(255, 0, 0, 0.3); +} + +section.stage button[disabled].cross { + color: rgb(43, 146, 255); + text-shadow: 0px 0px 5px rgba(174, 213, 255, 0.25); + background-color: rgba(31, 62, 163, 0.04); +} + +section.stage :not(button[disabled]).cross:hover { + border: 1px solid rgba(0, 102, 255, 0.5); + box-shadow: inset 0px 0px 32px rgba(0, 102, 255, 0.3); +} + +section.stage button[disabled].win { + color: rgb(56, 203, 220); + border: 1px solid rgba(61, 224, 162, 0.5); + background-color: rgba(27, 153, 199, 0.04); + box-shadow: inset 0px 0px 4px rgba(0, 102, 255, 0.3); +} + +/* /Stage */ diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..933c162 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,41 @@ + + + + + + + + Tic-Tac-Toe + + +
+

Tic-Tac-Toe

+
+ + + +
+
+ + + + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/dist/js/app.js b/dist/js/app.js new file mode 100644 index 0000000..a156e25 --- /dev/null +++ b/dist/js/app.js @@ -0,0 +1,195 @@ +var PLAYER_CIRCLE = 'circle'; +var PLAYER_CROSS = 'cross'; +var PLAYER_WIN = 'win'; + +var stage = document.getElementById('stage'); +var buttonCircle = document.getElementById('buttonCircle'); +var buttonCross = document.getElementById('buttonCross'); +var buttonReset = document.getElementById('buttonReset'); +var stageButtons = document.querySelectorAll('#stage button'); + +var currentPlayer = null; +var state = Object.assign({}, Array(9).fill(null)); + +function checkWin() { + var patterns = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], + [0, 3, 6], [1, 4, 7], [2, 5, 8], + [0, 4, 8], [2, 4, 6], + ]; + for (var i = 0; i < patterns.length; ++i) { + var p = patterns[i]; + if (state[p[0]] === null || state[p[1]] === null || state[p[2]] === null) { + continue; + } + if (state[p[0]] === state[p[1]] && state[p[1]] === state[p[2]]) { + return p; + } + } + return false; +} + +function disablePlayerButtons() { + buttonCross.disabled = true; + buttonCircle.disabled = true; +} + +function enablePlayerButtons() { + buttonCross.disabled = false; + buttonCircle.disabled = false; + setCrossLight(null); + setCircleLight(null); + setStagePlayer(null); + resetStageButtons(); +} + +function showWinningMoves(buttonIds) { + buttonIds.forEach(function (i) { + document.getElementById(i).classList.add(PLAYER_WIN); + }); + if (currentPlayer === PLAYER_CIRCLE) { + setCircleLight('green'); + } else if (currentPlayer === PLAYER_CROSS) { + setCrossLight('green'); + } +} + +function resetStageButtons() { + stageButtons.forEach(function (stageButton) { + setStageButtonValue(stageButton, null); + stageButton.classList.remove(PLAYER_WIN); + }); + setStageButtonsEnabled(false); + state = Object.assign({}, Array(9).fill(null)); +} + +function setStageButtonsEnabled(enabled = true) { + stageButtons.forEach(function (stageButton) { + stageButton.disabled = !enabled; + if (!enabled) { + stageButton.onclick = null; + return; + } + stageButton.onclick = function (evt) { + var stageButton = evt.target; + switch (currentPlayer) { + case PLAYER_CIRCLE: + if (setStageButtonValue(stageButton, PLAYER_CIRCLE)) { + setStagePlayer(PLAYER_CROSS); + } + break; + case PLAYER_CROSS: + if (setStageButtonValue(stageButton, PLAYER_CROSS)) { + setStagePlayer(PLAYER_CIRCLE); + } + break; + } + } + }); +} + +function setStagePlayer(player = PLAYER_CIRCLE) { + var remainingMoves = 0; + stageButtons.forEach(function (stageButton) { + if (stageButton.disabled) { + return; + } + stageButton.classList.remove(PLAYER_CIRCLE, PLAYER_CROSS); + if (player === null) { + return; + } + stageButton.classList.add(player); + currentPlayer = player; + switch (player) { + case PLAYER_CIRCLE: + disablePlayerButtons(); + setCircleLight(); + setCrossLight(null); + break; + case PLAYER_CROSS: + disablePlayerButtons(); + setCrossLight(); + setCircleLight(null); + break; + } + remainingMoves++; + }); + if (remainingMoves === 0) { + setCrossLight(null); + setCircleLight(null); + } +} + +function setStageButtonValue(stageButton, player = null) { + if (player === null) { + stageButton.innerHTML = ''; + stageButton.classList.remove('circle', 'cross'); + return false; + } + if (stageButton.innerHTML !== '') { + return false; + } + switch (player) { + case PLAYER_CIRCLE: + stageButton.innerHTML = '○'; + stageButton.classList.add('circle'); + state[stageButton.id] = PLAYER_CIRCLE; + break; + case PLAYER_CROSS: + stageButton.innerHTML = '×'; + stageButton.classList.add('cross'); + state[stageButton.id] = PLAYER_CROSS; + break; + } + stageButton.disabled = true; + var winningButtonIds = checkWin(); + if (winningButtonIds !== false) { + setStagePlayer(null); + setStageButtonsEnabled(false); + showWinningMoves(winningButtonIds); + return false; + } + return true; +} + +function setCrossLight(color = 'blue') { + var light = document.querySelector('#buttonCross .light'); + light.classList.remove('blue', 'green'); + if (color != null) { + light.classList.add(color); + } +} + +function setCircleLight(color = 'red') { + var light = document.querySelector('#buttonCircle .light'); + light.classList.remove('red', 'green'); + if (color != null) { + light.classList.add(color); + } +} + +function resizeStage() { + var stageWidth = stage.clientWidth; + stage.style.height = stageWidth + 'px'; + stageButtons.forEach(function (stageButton) { + stageButton.style.height = (stageWidth / 3) + 'px'; + stageButton.style.fontSize = (stageWidth / 6) + 'px'; + }); +} + +buttonCross.onclick = function () { + setStageButtonsEnabled(true); + setStagePlayer(PLAYER_CROSS); +} + +buttonCircle.onclick = function () { + setStageButtonsEnabled(true); + setStagePlayer(PLAYER_CIRCLE); +} + +buttonReset.onclick = function () { + enablePlayerButtons(); +} + +window.onresize = resizeStage; +window.onload = resizeStage; \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..8ba8680 Binary files /dev/null and b/screenshot.png differ