Update 2025-04-17_20:04:08
This commit is contained in:
102
static/app.js
Normal file
102
static/app.js
Normal file
@ -0,0 +1,102 @@
|
||||
// /var/www/minitactix/static/app.js
|
||||
let board = []
|
||||
let playerSymbol = ''
|
||||
let npcSymbol = ''
|
||||
let playerEmoji = ''
|
||||
let npcEmoji = ''
|
||||
let gameOver = false
|
||||
|
||||
const EMOJIS = ['❌', '⭕', '🔴', '🟢', '🟡', '🟣', '⭐', '💠', '🧿', '🧩']
|
||||
|
||||
function startGame(symbol) {
|
||||
playerSymbol = symbol
|
||||
npcSymbol = symbol === 'X' ? 'O' : 'X'
|
||||
|
||||
const shuffled = EMOJIS.sort(() => 0.5 - Math.random())
|
||||
playerEmoji = shuffled[0]
|
||||
npcEmoji = shuffled[1]
|
||||
|
||||
document.getElementById('symbol-chooser').classList.add('hidden')
|
||||
document.getElementById('board').classList.remove('hidden')
|
||||
document.getElementById('restart').classList.remove('hidden')
|
||||
document.getElementById('info').textContent = `You are ${playerEmoji} — NPC is ${npcEmoji}`
|
||||
initBoard()
|
||||
randomNPCMoves(3 + Math.floor(Math.random() * 3))
|
||||
renderBoard()
|
||||
}
|
||||
|
||||
function initBoard() {
|
||||
board = Array(16).fill('')
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
const boardDiv = document.getElementById('board')
|
||||
boardDiv.innerHTML = ''
|
||||
board.forEach((val, idx) => {
|
||||
const tile = document.createElement('div')
|
||||
tile.className = 'tile'
|
||||
tile.textContent = val === playerSymbol ? playerEmoji : (val === npcSymbol ? npcEmoji : '')
|
||||
tile.onclick = () => playerMove(idx)
|
||||
if (val !== '' || gameOver) tile.onclick = null
|
||||
boardDiv.appendChild(tile)
|
||||
})
|
||||
}
|
||||
|
||||
function playerMove(index) {
|
||||
if (board[index] !== '' || gameOver) return
|
||||
board[index] = playerSymbol
|
||||
if (checkWin(playerSymbol)) {
|
||||
endGame(`${playerEmoji} wins!`)
|
||||
return
|
||||
}
|
||||
if (board.includes('')) {
|
||||
randomNPCMoves(1)
|
||||
if (checkWin(npcSymbol)) {
|
||||
endGame(`${npcEmoji} wins!`)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!board.includes('')) endGame("It's a draw!")
|
||||
renderBoard()
|
||||
}
|
||||
|
||||
function randomNPCMoves(count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const free = board.map((v, i) => v === '' ? i : null).filter(v => v !== null)
|
||||
if (free.length === 0) return
|
||||
const pick = free[Math.floor(Math.random() * free.length)]
|
||||
board[pick] = npcSymbol
|
||||
}
|
||||
}
|
||||
|
||||
function checkWin(sym) {
|
||||
const lines = [
|
||||
[0,1,2], [1,2,3],
|
||||
[4,5,6], [5,6,7],
|
||||
[8,9,10], [9,10,11],
|
||||
[12,13,14], [13,14,15],
|
||||
[0,4,8], [4,8,12],
|
||||
[1,5,9], [5,9,13],
|
||||
[2,6,10], [6,10,14],
|
||||
[3,7,11], [7,11,15],
|
||||
[0,5,10], [1,6,11],
|
||||
[4,9,14], [5,10,15],
|
||||
[2,5,8], [3,6,9],
|
||||
[6,9,12], [7,10,13]
|
||||
]
|
||||
return lines.some(line => line.every(i => board[i] === sym))
|
||||
}
|
||||
|
||||
function endGame(msg) {
|
||||
document.getElementById('status').textContent = msg
|
||||
gameOver = true
|
||||
renderBoard()
|
||||
}
|
||||
|
||||
// PWA service worker registration
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('service-worker.js')
|
||||
})
|
||||
}
|
||||
|
88
static/game.js
Normal file
88
static/game.js
Normal file
@ -0,0 +1,88 @@
|
||||
// /var/www/minitactix/static/game.js
|
||||
let board = []
|
||||
let playerSymbol = ''
|
||||
let npcSymbol = ''
|
||||
let gameOver = false
|
||||
|
||||
function startGame(symbol) {
|
||||
playerSymbol = symbol
|
||||
npcSymbol = symbol === 'X' ? 'O' : 'X'
|
||||
document.getElementById('symbol-chooser').classList.add('hidden')
|
||||
document.getElementById('board').classList.remove('hidden')
|
||||
document.getElementById('restart').classList.remove('hidden')
|
||||
initBoard()
|
||||
randomNPCMoves(3 + Math.floor(Math.random() * 3))
|
||||
renderBoard()
|
||||
}
|
||||
|
||||
function initBoard() {
|
||||
board = Array(16).fill('')
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
const boardDiv = document.getElementById('board')
|
||||
boardDiv.innerHTML = ''
|
||||
board.forEach((val, idx) => {
|
||||
const tile = document.createElement('div')
|
||||
tile.className = 'tile'
|
||||
tile.textContent = val
|
||||
tile.onclick = () => playerMove(idx)
|
||||
if (val !== '' || gameOver) tile.onclick = null
|
||||
boardDiv.appendChild(tile)
|
||||
})
|
||||
}
|
||||
|
||||
function playerMove(index) {
|
||||
if (board[index] !== '' || gameOver) return
|
||||
board[index] = playerSymbol
|
||||
if (checkWin(playerSymbol)) {
|
||||
endGame('You win!')
|
||||
return
|
||||
}
|
||||
if (board.includes('')) {
|
||||
randomNPCMoves(1)
|
||||
if (checkWin(npcSymbol)) {
|
||||
endGame('NPC wins!')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!board.includes('')) endGame("It's a draw!")
|
||||
renderBoard()
|
||||
}
|
||||
|
||||
function randomNPCMoves(count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const free = board.map((v, i) => v === '' ? i : null).filter(v => v !== null)
|
||||
if (free.length === 0) return
|
||||
const pick = free[Math.floor(Math.random() * free.length)]
|
||||
board[pick] = npcSymbol
|
||||
}
|
||||
}
|
||||
|
||||
function checkWin(sym) {
|
||||
const lines = [
|
||||
// rows
|
||||
[0,1,2], [1,2,3],
|
||||
[4,5,6], [5,6,7],
|
||||
[8,9,10], [9,10,11],
|
||||
[12,13,14], [13,14,15],
|
||||
// cols
|
||||
[0,4,8], [4,8,12],
|
||||
[1,5,9], [5,9,13],
|
||||
[2,6,10], [6,10,14],
|
||||
[3,7,11], [7,11,15],
|
||||
// diagonals
|
||||
[0,5,10], [1,6,11],
|
||||
[4,9,14], [5,10,15],
|
||||
[2,5,8], [3,6,9],
|
||||
[6,9,12], [7,10,13]
|
||||
]
|
||||
return lines.some(line => line.every(i => board[i] === sym))
|
||||
}
|
||||
|
||||
function endGame(msg) {
|
||||
document.getElementById('status').textContent = msg
|
||||
gameOver = true
|
||||
renderBoard()
|
||||
}
|
||||
|
36
static/index.html
Normal file
36
static/index.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!-- /var/www/minitactix/static/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MiniTactix 4x4</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>MiniTactix 4x4</h1>
|
||||
<p id="info"></p>
|
||||
<div id="symbol-chooser">
|
||||
<p>Choose your symbol:</p>
|
||||
<button onclick="startGame('X')">X</button>
|
||||
<button onclick="startGame('O')">O</button>
|
||||
</div>
|
||||
<div id="board" class="grid hidden"></div>
|
||||
<p id="status"></p>
|
||||
<button id="restart" class="hidden" onclick="location.reload()">Play Again</button>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('service-worker.js')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
23
static/service-worker.js
Normal file
23
static/service-worker.js
Normal file
@ -0,0 +1,23 @@
|
||||
// /var/www/minitactix/static/service-worker.js
|
||||
const CACHE_NAME = "minitactix-cache-v1"
|
||||
const FILES_TO_CACHE = [
|
||||
"/",
|
||||
"/index.html",
|
||||
"/style.css",
|
||||
"/app.js",
|
||||
"/manifest.json"
|
||||
]
|
||||
|
||||
self.addEventListener("install", (evt) => {
|
||||
evt.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(FILES_TO_CACHE))
|
||||
)
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", (evt) => {
|
||||
evt.respondWith(
|
||||
caches.match(evt.request).then((response) => response || fetch(evt.request))
|
||||
)
|
||||
})
|
||||
|
68
static/style.css
Normal file
68
static/style.css
Normal file
@ -0,0 +1,68 @@
|
||||
/* /var/www/minitactix/static/style.css */
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 80px);
|
||||
grid-gap: 5px;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 36px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid #888;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tile:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#status {
|
||||
font-size: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
Reference in New Issue
Block a user