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