2465 lines
108 KiB
HTML
2465 lines
108 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>🎯 Harmony Board Game 🎯</title>
|
|||
|
|
<style>
|
|||
|
|
body {
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
background: #000;
|
|||
|
|
color: #0f0;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 20px;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-container {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
max-width: 1200px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.board-container {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.board {
|
|||
|
|
position: relative;
|
|||
|
|
width: 560px;
|
|||
|
|
height: 560px;
|
|||
|
|
background: #111;
|
|||
|
|
border: 2px solid #0f0;
|
|||
|
|
margin: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-lines {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-line {
|
|||
|
|
position: absolute;
|
|||
|
|
background: #444;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-line.horizontal {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-line.vertical {
|
|||
|
|
height: 100%;
|
|||
|
|
width: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
border: 2px solid #666;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #222;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 12px;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.unplayable {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.heart-only {
|
|||
|
|
border-color: #ff0 !important;
|
|||
|
|
background: #330 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.heart-only:hover {
|
|||
|
|
background: #660 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.highlight {
|
|||
|
|
background: #ff0 !important;
|
|||
|
|
animation: pulse 1s infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.winning {
|
|||
|
|
background: #ff0 !important;
|
|||
|
|
animation: winning-pulse 2s infinite;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.exploding {
|
|||
|
|
background: #f00 !important;
|
|||
|
|
animation: explode 1s ease-out forwards;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.imploding {
|
|||
|
|
background: #800 !important;
|
|||
|
|
animation: implode 1s ease-in forwards;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes explode {
|
|||
|
|
0% {
|
|||
|
|
transform: translate(-50%, -50%) scale(1);
|
|||
|
|
background: #ff0;
|
|||
|
|
box-shadow: 0 0 5px #ff0;
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
transform: translate(-50%, -50%) scale(2);
|
|||
|
|
background: #f80;
|
|||
|
|
box-shadow: 0 0 20px #f80;
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
transform: translate(-50%, -50%) scale(3);
|
|||
|
|
background: #f00;
|
|||
|
|
box-shadow: 0 0 40px #f00;
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes implode {
|
|||
|
|
0% {
|
|||
|
|
transform: translate(-50%, -50%) scale(1);
|
|||
|
|
background: #800;
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
transform: translate(-50%, -50%) scale(0.5);
|
|||
|
|
background: #400;
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
transform: translate(-50%, -50%) scale(0);
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0%, 100% { opacity: 0.5; }
|
|||
|
|
50% { opacity: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes winning-pulse {
|
|||
|
|
0%, 100% {
|
|||
|
|
background: #ff0 !important;
|
|||
|
|
transform: scale(1);
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
background: #f80 !important;
|
|||
|
|
transform: scale(1.2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.controls {
|
|||
|
|
background: #111;
|
|||
|
|
border: 2px solid #0f0;
|
|||
|
|
padding: 20px;
|
|||
|
|
width: 300px;
|
|||
|
|
height: fit-content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-info {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
padding: 10px;
|
|||
|
|
border: 1px solid #444;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.element-buttons {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.element-btn, .action-btn {
|
|||
|
|
padding: 10px;
|
|||
|
|
background: #222;
|
|||
|
|
border: 2px solid #0f0;
|
|||
|
|
color: #0f0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-controls {
|
|||
|
|
margin: 15px 0;
|
|||
|
|
padding: 10px;
|
|||
|
|
border: 1px solid #444;
|
|||
|
|
background: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-selector {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-group {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-group label {
|
|||
|
|
color: #0f0;
|
|||
|
|
font-weight: bold;
|
|||
|
|
min-width: 30px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-buttons {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-btn {
|
|||
|
|
padding: 5px 12px;
|
|||
|
|
background: #222;
|
|||
|
|
border: 1px solid #555;
|
|||
|
|
color: #ccc;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-btn.active {
|
|||
|
|
border-color: #0f0;
|
|||
|
|
color: #0f0;
|
|||
|
|
background: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-type-btn:hover {
|
|||
|
|
border-color: #0f0;
|
|||
|
|
color: #0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.element-btn:hover, .action-btn:hover {
|
|||
|
|
background: #0f0;
|
|||
|
|
color: #000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.element-btn:disabled, .action-btn:disabled {
|
|||
|
|
opacity: 0.3;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.element-btn.selected {
|
|||
|
|
background: #0f0;
|
|||
|
|
color: #000;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-log {
|
|||
|
|
height: 200px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
background: #000;
|
|||
|
|
border: 1px solid #444;
|
|||
|
|
padding: 10px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
display: none; /* Hidden by default */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-log.visible {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.missile-line {
|
|||
|
|
position: absolute;
|
|||
|
|
background: #ff0;
|
|||
|
|
height: 2px;
|
|||
|
|
transform-origin: left center;
|
|||
|
|
animation: missile-launch 0.5s ease-out;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes missile-launch {
|
|||
|
|
0% { width: 0; opacity: 1; }
|
|||
|
|
100% { width: var(--line-length); opacity: 0.8; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.simulation-controls {
|
|||
|
|
margin: 20px 0;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.score {
|
|||
|
|
font-size: 18px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Fullscreen Presentation Mode */
|
|||
|
|
.fullscreen-mode {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: #000;
|
|||
|
|
z-index: 9999;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-mode .board-container {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-mode .board {
|
|||
|
|
width: min(90vw, 90vh);
|
|||
|
|
height: min(90vw, 90vh);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-mode .controls {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-toggle {
|
|||
|
|
background: #333;
|
|||
|
|
color: #0f0;
|
|||
|
|
border: 2px solid #0f0;
|
|||
|
|
padding: 10px 15px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
width: 100%;
|
|||
|
|
font-family: inherit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-toggle:hover {
|
|||
|
|
background: #0f0;
|
|||
|
|
color: #000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Player Stone Differentiation */
|
|||
|
|
.intersection.player-1 {
|
|||
|
|
background-color: rgba(0, 150, 255, 0.3) !important; /* Blue background for P1 */
|
|||
|
|
border-color: #0096ff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.player-2 {
|
|||
|
|
background-color: rgba(255, 100, 0, 0.3) !important; /* Orange background for P2 */
|
|||
|
|
border-color: #ff6400 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.player-1:hover {
|
|||
|
|
background-color: rgba(0, 150, 255, 0.5) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.player-2:hover {
|
|||
|
|
background-color: rgba(255, 100, 0, 0.5) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.intersection.harmony-piece {
|
|||
|
|
border-color: #0ff !important; /* Cyan border for locked pieces */
|
|||
|
|
box-shadow: 0 0 8px #0ff;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Presentation Mode Styles */
|
|||
|
|
.presentation-mode .controls {
|
|||
|
|
display: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.presentation-mode body {
|
|||
|
|
padding: 0 !important;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
height: 100vh;
|
|||
|
|
width: 100vw;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.presentation-mode .game-container {
|
|||
|
|
max-width: none !important;
|
|||
|
|
gap: 0 !important;
|
|||
|
|
width: 100vw !important;
|
|||
|
|
height: 100vh !important;
|
|||
|
|
display: flex !important;
|
|||
|
|
justify-content: center !important;
|
|||
|
|
align-items: center !important;
|
|||
|
|
position: fixed !important;
|
|||
|
|
top: 0 !important;
|
|||
|
|
left: 0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.presentation-mode .board-container {
|
|||
|
|
margin: 0 !important;
|
|||
|
|
width: 100vw !important;
|
|||
|
|
height: 100vh !important;
|
|||
|
|
display: flex !important;
|
|||
|
|
justify-content: center !important;
|
|||
|
|
align-items: center !important;
|
|||
|
|
position: fixed !important;
|
|||
|
|
top: 0 !important;
|
|||
|
|
left: 0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.presentation-mode .board {
|
|||
|
|
margin: 0 !important;
|
|||
|
|
border: 3px solid #0f0 !important;
|
|||
|
|
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3) !important;
|
|||
|
|
max-width: none !important;
|
|||
|
|
max-height: none !important;
|
|||
|
|
/* Size will be set by JavaScript */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Scale intersections in presentation mode - sizes set by JavaScript */
|
|||
|
|
.presentation-mode .intersection {
|
|||
|
|
/* Sizes will be calculated and set by JavaScript */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="game-container">
|
|||
|
|
<div class="board-container">
|
|||
|
|
<div class="board" id="board"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="controls">
|
|||
|
|
<h2>🎯 Harmony Game 🎯</h2>
|
|||
|
|
|
|||
|
|
|
|||
|
|
<div class="simulation-controls">
|
|||
|
|
<button class="action-btn" id="resetGame">🔄 New Game</button>
|
|||
|
|
<button class="action-btn" id="undoMove" disabled">↩️ Undo</button>
|
|||
|
|
<button class="fullscreen-toggle" id="fullscreenToggle">📺 Presentation Mode</button>
|
|||
|
|
<button class="action-btn" id="consoleToggle">📝 Show Console</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="player-controls">
|
|||
|
|
<div class="player-type-selector">
|
|||
|
|
<div class="player-type-group">
|
|||
|
|
<label>P1:</label>
|
|||
|
|
<div class="player-buttons">
|
|||
|
|
<button class="player-type-btn active" id="p1-ai" data-player="1" data-type="ai">🤖 AI</button>
|
|||
|
|
<button class="player-type-btn" id="p1-human" data-player="1" data-type="human">👤 Human</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="player-type-group">
|
|||
|
|
<label>P2:</label>
|
|||
|
|
<div class="player-buttons">
|
|||
|
|
<button class="player-type-btn active" id="p2-ai" data-player="2" data-type="ai">🤖 AI</button>
|
|||
|
|
<button class="player-type-btn" id="p2-human" data-player="2" data-type="human">👤 Human</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="player-info" id="playerInfo">
|
|||
|
|
<div id="currentPlayer">🤖 AI 1's Turn</div>
|
|||
|
|
<div id="gameStatus">AI thinking...</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="element-buttons">
|
|||
|
|
<button class="element-btn" id="fire-btn" data-element="fire">🔥 Fire (12)</button>
|
|||
|
|
<button class="element-btn" id="water-btn" data-element="water">💧 Water (12)</button>
|
|||
|
|
<button class="element-btn" id="earth-btn" data-element="earth">🌍 Earth (12)</button>
|
|||
|
|
<button class="element-btn" id="air-btn" data-element="air">💨 Air (12)</button>
|
|||
|
|
<button class="element-btn" id="love-btn" data-element="love">❤️ Love (1)</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="player-info">
|
|||
|
|
<div><strong>Victory Conditions:</strong></div>
|
|||
|
|
<div id="victoryStatus">
|
|||
|
|
Win by achieving:<br>
|
|||
|
|
• 3 completed harmonies<br>
|
|||
|
|
OR<br>
|
|||
|
|
• 2 harmonies + 5-in-a-row
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="player-info">
|
|||
|
|
<div><strong>Game Stage:</strong></div>
|
|||
|
|
<div id="gameStageP1">P1 Stage: Corner Harmony</div>
|
|||
|
|
<div id="gameStageP2">P2 Stage: Corner Harmony</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="game-log" id="gameLog"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
class HarmonyGame {
|
|||
|
|
constructor() {
|
|||
|
|
this.board = Array(13).fill().map(() => Array(13).fill(null));
|
|||
|
|
this.currentPlayer = 1;
|
|||
|
|
this.gamePhase = 'playing';
|
|||
|
|
this.gameStage = { 1: 'corner_harmony', 2: 'corner_harmony' };
|
|||
|
|
this.selectedPieceType = null; // Currently selected piece for human player
|
|||
|
|
this.aiMoveDelay = 500; // Initial AI move delay in ms
|
|||
|
|
this.elements = {
|
|||
|
|
fire: '🔥', water: '💧', earth: '🌍', air: '💨', love: '❤️'
|
|||
|
|
};
|
|||
|
|
this.dominance = { fire: 'earth', earth: 'water', water: 'air', air: 'fire' };
|
|||
|
|
this.inventory = {
|
|||
|
|
1: { fire: 12, water: 12, earth: 12, air: 12, love: 1 },
|
|||
|
|
2: { fire: 12, water: 12, earth: 12, air: 12, love: 1 }
|
|||
|
|
};
|
|||
|
|
this.heartPositions = { 1: null, 2: null };
|
|||
|
|
this.weights = {
|
|||
|
|
fiveInARow: 10000,
|
|||
|
|
harmony: 10000,
|
|||
|
|
fourInARow: 750,
|
|||
|
|
threeInARow: 150,
|
|||
|
|
twoInARow: 15,
|
|||
|
|
harmonyProgress: 250, // Increased from 50
|
|||
|
|
blockHarmony: 200,
|
|||
|
|
material: 5,
|
|||
|
|
capture: 0, // Decreased from 1
|
|||
|
|
positional: 3,
|
|||
|
|
scarcityPenalty: 15,
|
|||
|
|
captureRiskPenalty: 45,
|
|||
|
|
noStonesAndNoHarmonyPenalty: 5000,
|
|||
|
|
strategicMoveBonus: 50, // Increased from 25
|
|||
|
|
secondHarmonyBonus: 500, // Bonus for pursuing a win after first harmony
|
|||
|
|
heartMoveBonus: 30,
|
|||
|
|
winningHeartPositionBonus: 2000, // NEW: Big bonus for moving heart to winning position
|
|||
|
|
secondHarmonyProgress: 800, // NEW: Higher bonus for progress toward second harmony
|
|||
|
|
lockedPiecePenalty: 200 // NEW: Penalty for trying to attack locked pieces
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Player types: 'human' or 'ai'
|
|||
|
|
this.playerTypes = {
|
|||
|
|
1: 'ai', // Player 1 is AI by default
|
|||
|
|
2: 'ai' // Player 2 is AI by default
|
|||
|
|
};
|
|||
|
|
this.completedHarmonies = { 1: new Set(), 2: new Set() };
|
|||
|
|
this.harmoniesCount = { 1: 0, 2: 0 }; // Track number of completed harmonies
|
|||
|
|
this.lockedPieces = new Set();
|
|||
|
|
|
|||
|
|
// Heart-only positions from ASCII diagram (marked with 'h')
|
|||
|
|
this.cornerHeartPositions = [
|
|||
|
|
{r: 1, c: 1}, {r: 1, c: 11}, {r: 11, c: 1}, {r: 11, c: 11}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Middle heart positions for middle harmony
|
|||
|
|
this.middleHeartPositions = [
|
|||
|
|
{r: 3, c: 3}, {r: 3, c: 9}, {r: 9, c: 3}, {r: 9, c: 9}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Center position (center heart)
|
|||
|
|
this.centerPosition = {r: 6, c: 6};
|
|||
|
|
|
|||
|
|
this.gameOver = false;
|
|||
|
|
this.winner = null;
|
|||
|
|
|
|||
|
|
// Check for presentation mode
|
|||
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|||
|
|
this.presentationMode = urlParams.get('mode') === 'presentation';
|
|||
|
|
|
|||
|
|
// Apply presentation mode styles immediately if in presentation mode
|
|||
|
|
if (this.presentationMode) {
|
|||
|
|
document.body.classList.add('presentation-mode');
|
|||
|
|
console.log('🎯 Presentation mode activated');
|
|||
|
|
console.log('🎯 Window size:', window.innerWidth, 'x', window.innerHeight);
|
|||
|
|
|
|||
|
|
// Try multiple times to ensure board is resized after it's created
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 50);
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 200);
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 500);
|
|||
|
|
|
|||
|
|
window.addEventListener('resize', () => this.resizeBoardForPresentation());
|
|||
|
|
|
|||
|
|
// Listen for messages from parent window
|
|||
|
|
window.addEventListener('message', (event) => {
|
|||
|
|
if (event.data && event.data.type === 'resize') {
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 100);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.playablePositions = new Set();
|
|||
|
|
this.heartOnlyPositions = new Set();
|
|||
|
|
this.initializeBoardLayout();
|
|||
|
|
|
|||
|
|
this.initBoard();
|
|||
|
|
this.setupEventListeners();
|
|||
|
|
this.updateDisplay();
|
|||
|
|
this.startGame();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initializeBoardLayout() {
|
|||
|
|
// Based on ASCII diagram, define positions directly
|
|||
|
|
// Corner extended positions
|
|||
|
|
this.playablePositions.add('0,0');
|
|||
|
|
this.playablePositions.add('0,1');
|
|||
|
|
this.playablePositions.add('0,11');
|
|||
|
|
this.playablePositions.add('0,12');
|
|||
|
|
this.playablePositions.add('12,0');
|
|||
|
|
this.playablePositions.add('12,1');
|
|||
|
|
this.playablePositions.add('12,11');
|
|||
|
|
this.playablePositions.add('12,12');
|
|||
|
|
|
|||
|
|
// Main playable area (rows 1-11, cols 1-11)
|
|||
|
|
for (let r = 1; r <= 11; r++) {
|
|||
|
|
for (let c = 1; c <= 11; c++) {
|
|||
|
|
this.playablePositions.add(`${r},${c}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Add missing side positions for rows 1 and 11
|
|||
|
|
this.playablePositions.add('1,0');
|
|||
|
|
this.playablePositions.add('1,12');
|
|||
|
|
this.playablePositions.add('11,0');
|
|||
|
|
this.playablePositions.add('11,12');
|
|||
|
|
|
|||
|
|
// Heart-only positions based on ASCII pattern
|
|||
|
|
// Corner hearts (just inside the corners)
|
|||
|
|
this.heartOnlyPositions.add('1,1'); // top-left corner heart
|
|||
|
|
this.heartOnlyPositions.add('1,11'); // top-right corner heart
|
|||
|
|
this.heartOnlyPositions.add('11,1'); // bottom-left corner heart
|
|||
|
|
this.heartOnlyPositions.add('11,11'); // bottom-right corner heart
|
|||
|
|
|
|||
|
|
// Middle hearts (on the edges toward middle)
|
|||
|
|
this.heartOnlyPositions.add('3,3'); // left middle heart (3 in from both sides)
|
|||
|
|
this.heartOnlyPositions.add('3,9'); // right middle heart (3 in from both sides)
|
|||
|
|
this.heartOnlyPositions.add('9,3'); // left middle heart (3 in from both sides)
|
|||
|
|
this.heartOnlyPositions.add('9,9'); // right middle heart (3 in from both sides)
|
|||
|
|
|
|||
|
|
// Center heart
|
|||
|
|
this.heartOnlyPositions.add('6,6'); // center heart
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initBoard() {
|
|||
|
|
const board = document.getElementById('board');
|
|||
|
|
board.innerHTML = '';
|
|||
|
|
|
|||
|
|
// Create grid lines
|
|||
|
|
const gridLines = document.createElement('div');
|
|||
|
|
gridLines.className = 'grid-lines';
|
|||
|
|
|
|||
|
|
// Horizontal lines
|
|||
|
|
for (let i = 0; i <= 12; i++) {
|
|||
|
|
const line = document.createElement('div');
|
|||
|
|
line.className = 'grid-line horizontal';
|
|||
|
|
line.style.top = `${40 + (i * 400 / 12)}px`;
|
|||
|
|
gridLines.appendChild(line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Vertical lines
|
|||
|
|
for (let i = 0; i <= 12; i++) {
|
|||
|
|
const line = document.createElement('div');
|
|||
|
|
line.className = 'grid-line vertical';
|
|||
|
|
line.style.left = `${40 + (i * 400 / 12)}px`;
|
|||
|
|
gridLines.appendChild(line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
board.appendChild(gridLines);
|
|||
|
|
|
|||
|
|
// Create intersections for 13x13 board
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
const intersection = document.createElement('div');
|
|||
|
|
intersection.className = 'intersection';
|
|||
|
|
intersection.dataset.row = r;
|
|||
|
|
intersection.dataset.col = c;
|
|||
|
|
|
|||
|
|
// Position intersection on grid
|
|||
|
|
const left = 40 + (c * 480 / 12);
|
|||
|
|
const top = 40 + (r * 480 / 12);
|
|||
|
|
|
|||
|
|
intersection.style.left = `${left}px`;
|
|||
|
|
intersection.style.top = `${top}px`;
|
|||
|
|
|
|||
|
|
// Check if this position is playable based on ASCII layout
|
|||
|
|
const posKey = `${r},${c}`;
|
|||
|
|
if (!this.playablePositions.has(posKey)) {
|
|||
|
|
intersection.classList.add('unplayable');
|
|||
|
|
} else if (this.heartOnlyPositions.has(posKey)) {
|
|||
|
|
intersection.classList.add('heart-only');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
board.appendChild(intersection);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If in presentation mode, resize board after it's created
|
|||
|
|
if (this.presentationMode) {
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 10);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isRegularPlayablePosition(r, c) {
|
|||
|
|
// Check if position is within bounds and marked as playable
|
|||
|
|
if (r < 0 || r >= 13 || c < 0 || c >= 13) return false;
|
|||
|
|
return this.playablePositions.has(`${r},${c}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isPlayable(r, c) {
|
|||
|
|
if (r < 0 || r >= 13 || c < 0 || c >= 13) return false;
|
|||
|
|
return this.playablePositions.has(`${r},${c}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setPlayerType(player, type) {
|
|||
|
|
this.playerTypes[player] = type;
|
|||
|
|
|
|||
|
|
// Update button visuals
|
|||
|
|
document.querySelectorAll(`[data-player="${player}"]`).forEach(btn => {
|
|||
|
|
btn.classList.remove('active');
|
|||
|
|
});
|
|||
|
|
document.querySelector(`[data-player="${player}"][data-type="${type}"]`).classList.add('active');
|
|||
|
|
|
|||
|
|
this.log(`Player ${player} set to ${type === 'ai' ? '🤖 AI' : '👤 Human'}`);
|
|||
|
|
this.updateDisplay();
|
|||
|
|
|
|||
|
|
// If it's currently this player's turn and they became AI, trigger AI move
|
|||
|
|
if (this.currentPlayer === player && type === 'ai' && !this.gameOver) {
|
|||
|
|
setTimeout(() => this.makeAIMove(), this.aiMoveDelay);
|
|||
|
|
} else if (this.playerTypes[this.currentPlayer] === 'human') {
|
|||
|
|
// If the current player is human, ensure the element buttons are correctly displayed
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resizeBoardForPresentation() {
|
|||
|
|
if (!this.presentationMode) return;
|
|||
|
|
|
|||
|
|
const board = document.querySelector('.board');
|
|||
|
|
if (!board) return;
|
|||
|
|
|
|||
|
|
// Calculate size based on window dimensions
|
|||
|
|
const containerWidth = window.innerWidth;
|
|||
|
|
const containerHeight = window.innerHeight;
|
|||
|
|
const size = Math.min(containerWidth, containerHeight) * 0.95;
|
|||
|
|
|
|||
|
|
console.log(`🔍 Window dimensions: ${containerWidth}x${containerHeight}, calculated size: ${size}px`);
|
|||
|
|
|
|||
|
|
// Set board size directly
|
|||
|
|
board.style.width = size + 'px';
|
|||
|
|
board.style.height = size + 'px';
|
|||
|
|
|
|||
|
|
// Scale intersections proportionally
|
|||
|
|
const scaleFactor = size / 560; // 560 is original board size
|
|||
|
|
const intersectionSize = Math.max(8, 20 * scaleFactor);
|
|||
|
|
const fontSize = Math.max(6, 12 * scaleFactor);
|
|||
|
|
const borderWidth = Math.max(1, 2 * scaleFactor);
|
|||
|
|
|
|||
|
|
const intersections = document.querySelectorAll('.intersection');
|
|||
|
|
intersections.forEach(intersection => {
|
|||
|
|
intersection.style.width = intersectionSize + 'px';
|
|||
|
|
intersection.style.height = intersectionSize + 'px';
|
|||
|
|
intersection.style.fontSize = fontSize + 'px';
|
|||
|
|
intersection.style.borderWidth = borderWidth + 'px';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log(`🎯 Board resized to ${size}px (scale: ${scaleFactor.toFixed(2)}x, intersections: ${intersectionSize}px)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
startGame() {
|
|||
|
|
// Apply presentation mode styles if in presentation mode
|
|||
|
|
if (this.presentationMode) {
|
|||
|
|
document.body.classList.add('presentation-mode');
|
|||
|
|
// Speed up AI moves for presentation
|
|||
|
|
this.aiMoveDelay = 100;
|
|||
|
|
// Auto-restart games when they finish for continuous play
|
|||
|
|
this.autoRestart = true;
|
|||
|
|
|
|||
|
|
// Resize board after game starts
|
|||
|
|
setTimeout(() => this.resizeBoardForPresentation(), 100);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start AI vs AI game automatically
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|||
|
|
setTimeout(() => this.makeAIMove(), this.aiMoveDelay);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupEventListeners() {
|
|||
|
|
// Element selection buttons
|
|||
|
|
document.querySelectorAll('.element-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', (e) => {
|
|||
|
|
const element = e.target.dataset.element;
|
|||
|
|
this.selectPieceType(element);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Board interaction
|
|||
|
|
document.addEventListener('click', (e) => {
|
|||
|
|
if (e.target.classList.contains('intersection') &&
|
|||
|
|
!e.target.classList.contains('unplayable') &&
|
|||
|
|
!this.gameOver && this.playerTypes[this.currentPlayer] === 'human') {
|
|||
|
|
const r = parseInt(e.target.dataset.row);
|
|||
|
|
const c = parseInt(e.target.dataset.col);
|
|||
|
|
this.handleBoardClick(r, c);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Control buttons
|
|||
|
|
document.getElementById('resetGame').addEventListener('click', () => {
|
|||
|
|
this.resetGame();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('undoMove').addEventListener('click', () => {
|
|||
|
|
this.undo();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Fullscreen presentation mode toggle
|
|||
|
|
document.getElementById('fullscreenToggle').addEventListener('click', () => {
|
|||
|
|
this.toggleFullscreen();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Console toggle
|
|||
|
|
document.getElementById('consoleToggle').addEventListener('click', () => {
|
|||
|
|
this.toggleConsole();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Handle window resize for fullscreen mode
|
|||
|
|
window.addEventListener('resize', () => {
|
|||
|
|
if (document.body.classList.contains('fullscreen-mode')) {
|
|||
|
|
this.resizeForFullscreen();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Player type selection buttons
|
|||
|
|
document.querySelectorAll('.player-type-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', (e) => {
|
|||
|
|
const player = parseInt(e.target.dataset.player);
|
|||
|
|
const type = e.target.dataset.type;
|
|||
|
|
this.setPlayerType(player, type);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('undoMove').addEventListener('click', () => {
|
|||
|
|
this.undoMove();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
selectPieceType(elementType) {
|
|||
|
|
if (this.gameOver || this.playerTypes[this.currentPlayer] !== 'human') return;
|
|||
|
|
|
|||
|
|
// Check if player has this piece
|
|||
|
|
if (this.inventory[this.currentPlayer][elementType] <= 0) {
|
|||
|
|
this.log(`❌ No ${elementType} pieces remaining`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Clear previous selection
|
|||
|
|
document.querySelectorAll('.element-btn').forEach(btn => {
|
|||
|
|
btn.classList.remove('selected');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Select new piece
|
|||
|
|
this.selectedPieceType = elementType;
|
|||
|
|
document.getElementById(`${elementType}-btn`).classList.add('selected');
|
|||
|
|
|
|||
|
|
this.log(`🎯 Selected ${this.elements[elementType]} ${elementType}`);
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
simulateMove(board, r, c, elementType, player) {
|
|||
|
|
const tempBoard = board.map(row => [...row]);
|
|||
|
|
tempBoard[r][c] = this.elements[elementType] + player;
|
|||
|
|
let captures = 0;
|
|||
|
|
|
|||
|
|
// Simulate captures
|
|||
|
|
if (elementType !== 'love') {
|
|||
|
|
const adjacentPositions = [
|
|||
|
|
{r: r-1, c: c}, {r: r+1, c: c},
|
|||
|
|
{r: r, c: c-1}, {r: r, c: c+1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const pos of adjacentPositions) {
|
|||
|
|
if (pos.r < 0 || pos.r >= 13 || pos.c < 0 || pos.c >= 13) continue;
|
|||
|
|
|
|||
|
|
const targetCell = tempBoard[pos.r][pos.c];
|
|||
|
|
if (!targetCell) continue;
|
|||
|
|
|
|||
|
|
const targetPlayer = parseInt(targetCell.slice(-1));
|
|||
|
|
if (targetPlayer === player) continue;
|
|||
|
|
|
|||
|
|
const targetElement = this.getElementTypeFromSymbol(targetCell.slice(0, -1));
|
|||
|
|
if (targetElement === 'love') continue;
|
|||
|
|
|
|||
|
|
// Don't count captures of locked pieces (they can't actually be captured)
|
|||
|
|
if (this.lockedPieces.has(`${pos.r},${pos.c}`)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.dominance[elementType] === targetElement) {
|
|||
|
|
tempBoard[pos.r][pos.c] = null;
|
|||
|
|
captures++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { tempBoard, captures };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
findHarmonyBlockingSpot(heartPos) {
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c}, {r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1}, {r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
if (this.isPlayable(pos.r, pos.c) && this.board[pos.r][pos.c] === null) {
|
|||
|
|
return pos;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async moveHeart(player, r, c) {
|
|||
|
|
const oldPos = this.heartPositions[player];
|
|||
|
|
if (oldPos) {
|
|||
|
|
this.board[oldPos.r][oldPos.c] = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.board[r][c] = this.elements.love + player;
|
|||
|
|
this.heartPositions[player] = {r, c};
|
|||
|
|
|
|||
|
|
this.log(`🤖 Player ${player} moved ❤️ to [${r},${c}]`);
|
|||
|
|
|
|||
|
|
await this.checkVictoryConditions();
|
|||
|
|
|
|||
|
|
if (!this.gameOver) {
|
|||
|
|
this.nextPlayer();
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|||
|
|
if (this.aiMoveDelay > 50) this.aiMoveDelay -= 50;
|
|||
|
|
setTimeout(() => this.makeAIMove(), this.aiMoveDelay);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleBoardClick(r, c) {
|
|||
|
|
const player = this.currentPlayer;
|
|||
|
|
if (this.selectedPieceType === 'love' && this.gameStage[player] !== 'corner_harmony' && this.heartPositions[player]) {
|
|||
|
|
// Logic to move the heart for a human player
|
|||
|
|
if (this.heartOnlyPositions.has(`${r},${c}`) && this.board[r][c] === null) {
|
|||
|
|
this.log(`👤 Player ${player} is moving ❤️ to [${r},${c}]`);
|
|||
|
|
await this.moveHeart(player, r, c);
|
|||
|
|
} else {
|
|||
|
|
this.log("❌ Can only move heart to another empty, valid heart position.");
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.selectedPieceType) {
|
|||
|
|
this.log("❌ Please select a piece type first");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.board[r][c] !== null) {
|
|||
|
|
this.log("❌ Position already occupied");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isHeartOnlyPosition = this.heartOnlyPositions.has(`${r},${c}`);
|
|||
|
|
|
|||
|
|
if (isHeartOnlyPosition && this.selectedPieceType !== 'love') {
|
|||
|
|
this.log("❌ Only heart pieces can be placed at corner, middle, or center positions");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.selectedPieceType === 'love' && !this.canPlaceHeart(player, r, c)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await this.makeMove(r, c, this.selectedPieceType, player);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async makeMove(r, c, elementType, player) {
|
|||
|
|
if (elementType === 'love' && !this.canPlaceHeart(player, r, c)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.board[r][c] = this.elements[elementType] + player;
|
|||
|
|
this.inventory[player][elementType]--;
|
|||
|
|
|
|||
|
|
if (elementType === 'love') {
|
|||
|
|
this.heartPositions[player] = {r, c};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log(`${player === 1 ? '👤' : '🤖'} Player ${player} placed ${this.elements[elementType]} at [${r},${c}]`);
|
|||
|
|
|
|||
|
|
this.processElementalReactions(r, c, elementType, player);
|
|||
|
|
|
|||
|
|
await this.checkVictoryConditions();
|
|||
|
|
|
|||
|
|
if (!this.gameOver) {
|
|||
|
|
this.nextPlayer();
|
|||
|
|
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|||
|
|
if (this.aiMoveDelay > 50) {
|
|||
|
|
this.aiMoveDelay -= 50;
|
|||
|
|
}
|
|||
|
|
setTimeout(() => this.makeAIMove(), this.aiMoveDelay);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
processElementalReactions(r, c, elementType, player) {
|
|||
|
|
if (elementType === 'love') return; // Love pieces don't trigger reactions
|
|||
|
|
|
|||
|
|
const adjacentPositions = [
|
|||
|
|
{r: r-1, c: c}, // up
|
|||
|
|
{r: r+1, c: c}, // down
|
|||
|
|
{r: r, c: c-1}, // left
|
|||
|
|
{r: r, c: c+1} // right
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const removedPieces = [];
|
|||
|
|
|
|||
|
|
for (const pos of adjacentPositions) {
|
|||
|
|
if (pos.r < 0 || pos.r >= 13 || pos.c < 0 || pos.c >= 13) continue;
|
|||
|
|
|
|||
|
|
const targetCell = this.board[pos.r][pos.c];
|
|||
|
|
if (!targetCell) continue;
|
|||
|
|
|
|||
|
|
// Extract element and player from cell (format: "🔥1" or "❤️2")
|
|||
|
|
const targetPlayer = parseInt(targetCell.slice(-1));
|
|||
|
|
if (targetPlayer === player) continue; // Can't attack own pieces
|
|||
|
|
|
|||
|
|
const targetElement = this.getElementTypeFromSymbol(targetCell.slice(0, -1));
|
|||
|
|
if (targetElement === 'love') continue; // Love pieces are immune
|
|||
|
|
|
|||
|
|
// Check if the piece is locked
|
|||
|
|
if (this.lockedPieces.has(`${pos.r},${pos.c}`)) {
|
|||
|
|
this.log(`🛡️ Piece at [${pos.r},${pos.c}] is part of a locked Harmony and cannot be captured.`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check if our element dominates the target
|
|||
|
|
if (this.dominance[elementType] === targetElement) {
|
|||
|
|
this.board[pos.r][pos.c] = null;
|
|||
|
|
removedPieces.push({element: targetElement, pos: pos});
|
|||
|
|
|
|||
|
|
// Animate removal
|
|||
|
|
const targetIntersection = document.querySelector(`[data-row="${pos.r}"][data-col="${pos.c}"]`);
|
|||
|
|
if (targetIntersection) {
|
|||
|
|
targetIntersection.classList.add('exploding');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
targetIntersection.classList.remove('exploding');
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log(`⚔️ ${this.elements[elementType]} destroys ${this.elements[targetElement]} at [${pos.r},${pos.c}]`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return removedPieces;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getElementTypeFromSymbol(symbol) {
|
|||
|
|
for (const [type, emoji] of Object.entries(this.elements)) {
|
|||
|
|
if (emoji === symbol) return type;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async checkVictoryConditions() {
|
|||
|
|
for (let player = 1; player <= 2; player++) {
|
|||
|
|
// Check for completed harmonies at current heart position
|
|||
|
|
const heartPos = this.heartPositions[player];
|
|||
|
|
if (heartPos && this.checkHarmonyAtPosition(heartPos, player)) {
|
|||
|
|
const harmonyId = `${heartPos.r},${heartPos.c}`;
|
|||
|
|
// Check if this is a new harmony
|
|||
|
|
if (!this.completedHarmonies[player].has(harmonyId)) {
|
|||
|
|
this.completedHarmonies[player].add(harmonyId);
|
|||
|
|
this.harmoniesCount[player]++;
|
|||
|
|
this.log(`Player ${player} completed harmony #${this.harmoniesCount[player]}! Total: ${this.harmoniesCount[player]}`);
|
|||
|
|
|
|||
|
|
// Lock the elemental pieces of the new harmony
|
|||
|
|
this.lockHarmonyPieces(heartPos);
|
|||
|
|
|
|||
|
|
// Update game stage for UI purposes (corner->middle->center)
|
|||
|
|
if (this.harmoniesCount[player] === 1) {
|
|||
|
|
this.gameStage[player] = 'middle_harmony';
|
|||
|
|
this.log(`Player ${player} has advanced to the Middle Harmony stage!`);
|
|||
|
|
} else if (this.harmoniesCount[player] >= 2) {
|
|||
|
|
this.gameStage[player] = 'center_harmony';
|
|||
|
|
this.log(`Player ${player} has advanced to the Center Harmony stage!`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check for victory: 3 harmonies = automatic win
|
|||
|
|
if (this.harmoniesCount[player] >= 3) {
|
|||
|
|
this.log(`🏆 Player ${player} wins with 3 harmonies!`);
|
|||
|
|
await this.handleVictory(player);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Victory Condition: 2 harmonies + 5-in-a-row = win
|
|||
|
|
if (this.harmoniesCount[player] >= 2 && this.checkFiveInRow(player)) {
|
|||
|
|
this.log(`🏆 Player ${player} wins with 2 harmonies + 5-in-a-row!`);
|
|||
|
|
await this.handleVictory(player);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkCornerHarmony(player) {
|
|||
|
|
const heartPos = this.heartPositions[player];
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// Check if heart is in a corner heart position
|
|||
|
|
const isCornerHeartPosition = this.cornerHeartPositions.some(pos =>
|
|||
|
|
pos.r === heartPos.r && pos.c === heartPos.c
|
|||
|
|
);
|
|||
|
|
if (!isCornerHeartPosition) return false;
|
|||
|
|
|
|||
|
|
return this.checkHarmonyAtPosition(heartPos, player);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkMiddleHarmony(player) {
|
|||
|
|
const heartPos = this.heartPositions[player];
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// Check if heart is in a middle heart position
|
|||
|
|
const isMiddleHeartPosition = this.middleHeartPositions.some(pos =>
|
|||
|
|
pos.r === heartPos.r && pos.c === heartPos.c
|
|||
|
|
);
|
|||
|
|
if (!isMiddleHeartPosition) return false;
|
|||
|
|
|
|||
|
|
return this.checkHarmonyAtPosition(heartPos, player);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkCenterHarmony(player) {
|
|||
|
|
const heartPos = this.heartPositions[player];
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// Check if heart is in center position
|
|||
|
|
if (heartPos.r !== this.centerPosition.r || heartPos.c !== this.centerPosition.c) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return this.checkHarmonyAtPosition(heartPos, player);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
canPlaceHeart(player, r, c, silent = false) {
|
|||
|
|
const posKey = `${r},${c}`;
|
|||
|
|
// Love pieces can only be placed at designated heart positions.
|
|||
|
|
if (!this.heartOnlyPositions.has(posKey)) {
|
|||
|
|
if (!silent) this.log("❌ Love pieces can only be placed on designated heart spots.");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Enforce stage progression: Corner -> Middle -> Center
|
|||
|
|
const heartPos = {r, c};
|
|||
|
|
const isCorner = this.cornerHeartPositions.some(p => p.r === r && p.c === c);
|
|||
|
|
const isMiddle = this.middleHeartPositions.some(p => p.r === r && p.c === c);
|
|||
|
|
const isCenter = this.centerPosition.r === r && this.centerPosition.c === c;
|
|||
|
|
|
|||
|
|
switch (this.gameStage[player]) {
|
|||
|
|
case 'corner_harmony':
|
|||
|
|
if (!isCorner) {
|
|||
|
|
if (!silent) this.log("❌ Must complete Corner Harmony first!");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case 'middle_harmony':
|
|||
|
|
if (!isMiddle) {
|
|||
|
|
if (!silent) this.log("❌ Must complete Middle Harmony next!");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case 'center_harmony':
|
|||
|
|
if (!isCenter) {
|
|||
|
|
if (!silent) this.log("❌ Must complete Center Harmony last!");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkHarmonyAtPosition(heartPos, player) {
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// Use harmoniesCount to determine if this is first harmony or subsequent
|
|||
|
|
const completedCount = this.harmoniesCount[player];
|
|||
|
|
|
|||
|
|
if (completedCount === 0) {
|
|||
|
|
// First harmony: must be cross pattern
|
|||
|
|
return this.checkFirstHarmony(heartPos, player);
|
|||
|
|
} else {
|
|||
|
|
// Second/third harmony: connected chain pattern
|
|||
|
|
return this.checkSubsequentHarmony(heartPos, player);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkFirstHarmony(heartPos, player) {
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// First harmony MUST be in cross pattern (heart + 4 adjacent elements)
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c}, // up
|
|||
|
|
{r: heartPos.r + 1, c: heartPos.c}, // down
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1}, // left
|
|||
|
|
{r: heartPos.r, c: heartPos.c + 1} // right
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const elementsFound = new Set();
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = this.board[pos.r]?.[pos.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const elementSymbol = cell.slice(0, -1);
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(elementSymbol);
|
|||
|
|
if (elementType && elementType !== 'love') {
|
|||
|
|
elementsFound.add(elementType);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return elementsFound.size === 4; // All 4 different elements in cross pattern
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkSubsequentHarmony(heartPos, player) {
|
|||
|
|
if (!heartPos) return false;
|
|||
|
|
|
|||
|
|
// For 2nd/3rd harmony: heart connects to 1 element, then chain of 4 total elements
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1},
|
|||
|
|
{r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Find elements directly connected to heart
|
|||
|
|
const directElements = [];
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = this.board[pos.r]?.[pos.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const elementSymbol = cell.slice(0, -1);
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(elementSymbol);
|
|||
|
|
if (elementType && elementType !== 'love') {
|
|||
|
|
directElements.push({pos, type: elementType});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Heart should connect to exactly 1 element for subsequent harmonies
|
|||
|
|
if (directElements.length !== 1) return false;
|
|||
|
|
|
|||
|
|
const startElement = directElements[0];
|
|||
|
|
const visited = new Set();
|
|||
|
|
const elementsInChain = new Set();
|
|||
|
|
|
|||
|
|
// Find all connected elements starting from the one connected to heart
|
|||
|
|
const findConnectedElements = (pos, elementType) => {
|
|||
|
|
const key = `${pos.r},${pos.c}`;
|
|||
|
|
if (visited.has(key)) return;
|
|||
|
|
visited.add(key);
|
|||
|
|
elementsInChain.add(elementType);
|
|||
|
|
|
|||
|
|
const neighbors = [
|
|||
|
|
{r: pos.r - 1, c: pos.c},
|
|||
|
|
{r: pos.r + 1, c: pos.c},
|
|||
|
|
{r: pos.r, c: pos.c - 1},
|
|||
|
|
{r: pos.r, c: pos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const neighbor of neighbors) {
|
|||
|
|
const cell = this.board[neighbor.r]?.[neighbor.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const neighborSymbol = cell.slice(0, -1);
|
|||
|
|
const neighborType = this.getElementTypeFromSymbol(neighborSymbol);
|
|||
|
|
if (neighborType && neighborType !== 'love') {
|
|||
|
|
findConnectedElements(neighbor, neighborType);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
findConnectedElements(startElement.pos, startElement.type);
|
|||
|
|
|
|||
|
|
return elementsInChain.size === 4; // Need 4 different elements in connected chain
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isCorrectPositionForStage(heartPos, player) {
|
|||
|
|
const isCorner = this.cornerHeartPositions.some(p => p.r === heartPos.r && p.c === heartPos.c);
|
|||
|
|
const isMiddle = this.middleHeartPositions.some(p => p.r === heartPos.r && p.c === heartPos.c);
|
|||
|
|
const isCenter = this.centerPosition.r === heartPos.r && this.centerPosition.c === heartPos.c;
|
|||
|
|
|
|||
|
|
switch (this.gameStage[player]) {
|
|||
|
|
case 'corner_harmony': return isCorner;
|
|||
|
|
case 'middle_harmony': return isMiddle;
|
|||
|
|
case 'center_harmony': return isCenter;
|
|||
|
|
default: return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkFiveInRow(player) {
|
|||
|
|
const directions = [
|
|||
|
|
{r: 0, c: 1}, // horizontal
|
|||
|
|
{r: 1, c: 0}, // vertical
|
|||
|
|
{r: 1, c: 1}, // diagonal \
|
|||
|
|
{r: 1, c: -1} // diagonal /
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
for (const dir of directions) {
|
|||
|
|
if (this.checkLineFromPosition(r, c, dir, player, 5)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkLineFromPosition(startR, startC, direction, player, length) {
|
|||
|
|
let firstElement = null;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < length; i++) {
|
|||
|
|
const r = startR + direction.r * i;
|
|||
|
|
const c = startC + direction.c * i;
|
|||
|
|
|
|||
|
|
if (r < 0 || r >= 13 || c < 0 || c >= 13) return false;
|
|||
|
|
|
|||
|
|
const cell = this.board[r][c];
|
|||
|
|
if (!cell || !cell.endsWith(player.toString())) return false;
|
|||
|
|
|
|||
|
|
const elementSymbol = cell.slice(0, -1);
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(elementSymbol);
|
|||
|
|
|
|||
|
|
if (elementType === 'love') return false; // Love pieces don't count for 5-in-a-row
|
|||
|
|
|
|||
|
|
if (firstElement === null) {
|
|||
|
|
firstElement = elementType;
|
|||
|
|
} else if (firstElement !== elementType) {
|
|||
|
|
return false; // Must be same element type
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
evaluateBoard(board, player) {
|
|||
|
|
let score = 0;
|
|||
|
|
const opponent = player === 1 ? 2 : 1;
|
|||
|
|
|
|||
|
|
score += this.checkLineThreats(board, player, 5) * this.weights.fiveInARow;
|
|||
|
|
score -= this.checkLineThreats(board, opponent, 5) * this.weights.fiveInARow;
|
|||
|
|
score += this.checkLineThreats(board, player, 4) * this.weights.fourInARow;
|
|||
|
|
score -= this.checkLineThreats(board, opponent, 4) * this.weights.fourInARow;
|
|||
|
|
score += this.checkLineThreats(board, player, 3) * this.weights.threeInARow;
|
|||
|
|
score -= this.checkLineThreats(board, opponent, 3) * this.weights.threeInARow;
|
|||
|
|
score += this.checkLineThreats(board, player, 2) * this.weights.twoInARow;
|
|||
|
|
score -= this.checkLineThreats(board, opponent, 2) * this.weights.twoInARow;
|
|||
|
|
|
|||
|
|
// --- Harmony Victory Progress ---
|
|||
|
|
const playerHeartPos = this.getHeartPosition(board, player);
|
|||
|
|
if (playerHeartPos) {
|
|||
|
|
const harmonyProgress = this.getHarmonyProgress(board, playerHeartPos, player);
|
|||
|
|
if (harmonyProgress === 4) { // A full harmony is found
|
|||
|
|
score += this.weights.harmony;
|
|||
|
|
} else {
|
|||
|
|
// Score harmony progress at any valid heart position
|
|||
|
|
score += harmonyProgress * this.weights.harmonyProgress;
|
|||
|
|
|
|||
|
|
// Extra bonus for building harmony after first harmony completed
|
|||
|
|
if (this.completedHarmonies[player].size > 0) {
|
|||
|
|
score += harmonyProgress * this.weights.harmonyProgress;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const opponentHeartPos = this.getHeartPosition(board, opponent);
|
|||
|
|
if (opponentHeartPos) {
|
|||
|
|
const opponentHarmonyProgress = this.getHarmonyProgress(board, opponentHeartPos, opponent);
|
|||
|
|
if (opponentHarmonyProgress === 4) {
|
|||
|
|
score -= this.weights.harmony;
|
|||
|
|
}
|
|||
|
|
if (opponentHarmonyProgress === 3) { // Only block when opponent is one step away
|
|||
|
|
score -= this.weights.blockHarmony;
|
|||
|
|
} else if (opponentHarmonyProgress === 2) {
|
|||
|
|
score -= this.weights.blockHarmony / 2; // Less penalty for 2 pieces
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Material Advantage & Positional ---
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
const cell = board[r][c];
|
|||
|
|
if (cell) {
|
|||
|
|
const cellPlayer = parseInt(cell.slice(-1));
|
|||
|
|
if (cellPlayer === player) {
|
|||
|
|
score += this.weights.material;
|
|||
|
|
// Positional bonus for being in the center
|
|||
|
|
if (r >= 4 && r <= 8 && c >= 4 && c <= 8) {
|
|||
|
|
score += this.weights.positional;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
score -= this.weights.material;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Penalty for running out of stones without achieving harmony
|
|||
|
|
const totalStones = Object.values(this.inventory[player]).reduce((a, b) => a + b, 0);
|
|||
|
|
if (totalStones === 0 && this.gameStage[player] === 'corner_harmony') {
|
|||
|
|
score -= this.weights.noStonesAndNoHarmonyPenalty;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Additional Harmony Bonus ---
|
|||
|
|
if (this.completedHarmonies[player].size > 0) {
|
|||
|
|
// Player has completed at least one harmony, give bonus for pursuing more
|
|||
|
|
const winningFiveInARowProgress = this.checkLineThreats(board, player, 4);
|
|||
|
|
if (winningFiveInARowProgress > 0) {
|
|||
|
|
score += winningFiveInARowProgress * this.weights.secondHarmonyBonus;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Bonus for additional harmony progress at any valid position
|
|||
|
|
if (playerHeartPos) {
|
|||
|
|
const harmonyProgress = this.getHarmonyProgress(board, playerHeartPos, player);
|
|||
|
|
if (harmonyProgress > 0) {
|
|||
|
|
score += harmonyProgress * this.weights.secondHarmonyProgress;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Evaluate potential harmony positions even if heart isn't there yet
|
|||
|
|
const allHeartPositions = [
|
|||
|
|
...this.cornerHeartPositions,
|
|||
|
|
...this.middleHeartPositions,
|
|||
|
|
this.centerPosition
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const pos of allHeartPositions) {
|
|||
|
|
if (board[pos.r]?.[pos.c] === null) { // Position is available
|
|||
|
|
const potentialHarmonyProgress = this.getHarmonyProgress(board, pos, player);
|
|||
|
|
if (potentialHarmonyProgress >= 2) { // We have some elements that could work
|
|||
|
|
score += potentialHarmonyProgress * (this.weights.secondHarmonyProgress / 3);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return score;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getHeartPosition(board, player) {
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
if (board[r][c] === this.elements.love + player) {
|
|||
|
|
return { r, c };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getHarmonyProgress(board, heartPos, player) {
|
|||
|
|
const completedCount = this.harmoniesCount[player];
|
|||
|
|
|
|||
|
|
if (completedCount === 0) {
|
|||
|
|
// First harmony: cross pattern only
|
|||
|
|
return this.getFirstHarmonyProgress(board, heartPos, player);
|
|||
|
|
} else {
|
|||
|
|
// Subsequent harmony: chain pattern
|
|||
|
|
return this.getSubsequentHarmonyProgress(board, heartPos, player);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getFirstHarmonyProgress(board, heartPos, player) {
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c}, {r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1}, {r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
const elementsFound = new Set();
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = board[pos.r]?.[pos.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(cell.slice(0, -1));
|
|||
|
|
if (elementType && elementType !== 'love') {
|
|||
|
|
elementsFound.add(elementType);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return elementsFound.size;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getSubsequentHarmonyProgress(board, heartPos, player) {
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1},
|
|||
|
|
{r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Find elements directly connected to heart
|
|||
|
|
const directElements = [];
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = board[pos.r]?.[pos.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(cell.slice(0, -1));
|
|||
|
|
if (elementType && elementType !== 'love') {
|
|||
|
|
directElements.push({pos, type: elementType});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// For subsequent harmonies, heart should connect to at most 1 element
|
|||
|
|
if (directElements.length > 1) {
|
|||
|
|
return 0; // Invalid pattern for subsequent harmony
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (directElements.length === 0) {
|
|||
|
|
return 0; // No elements connected yet
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Find all connected elements in the chain
|
|||
|
|
const startElement = directElements[0];
|
|||
|
|
const visited = new Set();
|
|||
|
|
const elementsInChain = new Set();
|
|||
|
|
|
|||
|
|
const findConnectedElements = (pos, elementType) => {
|
|||
|
|
const key = `${pos.r},${pos.c}`;
|
|||
|
|
if (visited.has(key)) return;
|
|||
|
|
visited.add(key);
|
|||
|
|
elementsInChain.add(elementType);
|
|||
|
|
|
|||
|
|
const neighbors = [
|
|||
|
|
{r: pos.r - 1, c: pos.c},
|
|||
|
|
{r: pos.r + 1, c: pos.c},
|
|||
|
|
{r: pos.r, c: pos.c - 1},
|
|||
|
|
{r: pos.r, c: pos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const neighbor of neighbors) {
|
|||
|
|
const cell = board[neighbor.r]?.[neighbor.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const neighborType = this.getElementTypeFromSymbol(cell.slice(0, -1));
|
|||
|
|
if (neighborType && neighborType !== 'love') {
|
|||
|
|
findConnectedElements(neighbor, neighborType);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
findConnectedElements(startElement.pos, startElement.type);
|
|||
|
|
|
|||
|
|
return elementsInChain.size;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkLineThreats(board, player, length) {
|
|||
|
|
let threatCount = 0;
|
|||
|
|
const directions = [
|
|||
|
|
{r: 0, c: 1}, {r: 1, c: 0},
|
|||
|
|
{r: 1, c: 1}, {r: 1, c: -1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
for (const dir of directions) {
|
|||
|
|
if (this.checkLine(board, r, c, dir, player, length)) {
|
|||
|
|
threatCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return threatCount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkLine(board, startR, startC, direction, player, length) {
|
|||
|
|
let firstElement = null;
|
|||
|
|
let openEnds = 0;
|
|||
|
|
|
|||
|
|
// Check for open end before the line
|
|||
|
|
const beforeR = startR - direction.r;
|
|||
|
|
const beforeC = startC - direction.c;
|
|||
|
|
if (this.isPlayable(beforeR, beforeC) && board[beforeR][beforeC] === null) {
|
|||
|
|
openEnds++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (let i = 0; i < length; i++) {
|
|||
|
|
const r = startR + direction.r * i;
|
|||
|
|
const c = startC + direction.c * i;
|
|||
|
|
|
|||
|
|
if (!this.isPlayable(r, c)) return false;
|
|||
|
|
|
|||
|
|
const cell = board[r][c];
|
|||
|
|
if (!cell || !cell.endsWith(player.toString())) return false;
|
|||
|
|
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(cell.slice(0, -1));
|
|||
|
|
if (elementType === 'love') return false;
|
|||
|
|
|
|||
|
|
if (firstElement === null) {
|
|||
|
|
firstElement = elementType;
|
|||
|
|
} else if (firstElement !== elementType) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check for open end after the line
|
|||
|
|
const afterR = startR + direction.r * length;
|
|||
|
|
const afterC = startC + direction.c * length;
|
|||
|
|
if (this.isPlayable(afterR, afterC) && board[afterR][afterC] === null) {
|
|||
|
|
openEnds++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (length === 5) return true; // 5 in a row is a win regardless of open ends
|
|||
|
|
if (length === 4 && openEnds >= 1) return true;
|
|||
|
|
if (length === 3 && openEnds >= 2) return true;
|
|||
|
|
if (length === 2 && openEnds >= 2) return true;
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async makeAIMove() {
|
|||
|
|
const currentAI = this.currentPlayer;
|
|||
|
|
this.log(`🤖 AI ${currentAI} is thinking...`);
|
|||
|
|
|
|||
|
|
// --- Opening Move Override ---
|
|||
|
|
if (this.gameStage[currentAI] === 'corner_harmony' && this.heartPositions[currentAI] === null && this.inventory[currentAI].love > 0) {
|
|||
|
|
this.log(`🤖 AI ${currentAI} is executing its opening move: placing the heart stone.`);
|
|||
|
|
|
|||
|
|
const opponent = currentAI === 1 ? 2 : 1;
|
|||
|
|
const opponentHeartPos = this.heartPositions[opponent];
|
|||
|
|
let bestCorner = null;
|
|||
|
|
|
|||
|
|
// Check if opponent has a heart in a corner
|
|||
|
|
if (opponentHeartPos && this.cornerHeartPositions.some(p => p.r === opponentHeartPos.r && p.c === opponentHeartPos.c)) {
|
|||
|
|
const oppositeCorner = { r: 12 - opponentHeartPos.r, c: 12 - opponentHeartPos.c };
|
|||
|
|
if (this.board[oppositeCorner.r][oppositeCorner.c] === null) {
|
|||
|
|
bestCorner = oppositeCorner;
|
|||
|
|
this.log(`🤖 AI ${currentAI} strategically chose the opposite corner.`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!bestCorner) {
|
|||
|
|
// If no strategic choice is made, pick a random available corner
|
|||
|
|
const availableCorners = this.cornerHeartPositions.filter(pos => this.board[pos.r][pos.c] === null);
|
|||
|
|
if (availableCorners.length > 0) {
|
|||
|
|
bestCorner = availableCorners[Math.floor(Math.random() * availableCorners.length)];
|
|||
|
|
this.log(`🤖 AI ${currentAI} chose a random corner.`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (bestCorner) {
|
|||
|
|
await this.makeMove(bestCorner.r, bestCorner.c, 'love', currentAI);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Strategic Heart Movement Override ---
|
|||
|
|
// If AI has completed harmonies, look for strategic heart movement opportunities
|
|||
|
|
if (this.completedHarmonies[currentAI].size > 0 && this.heartPositions[currentAI]) {
|
|||
|
|
const currentHeartPos = this.heartPositions[currentAI];
|
|||
|
|
const allHeartPositions = [
|
|||
|
|
...this.cornerHeartPositions,
|
|||
|
|
...this.middleHeartPositions,
|
|||
|
|
this.centerPosition
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Check if we should move heart to a better position
|
|||
|
|
for (const winPos of allHeartPositions) {
|
|||
|
|
if (this.board[winPos.r][winPos.c] === null && this.canPlaceHeart(currentAI, winPos.r, winPos.c, true)) {
|
|||
|
|
const potentialProgress = this.getHarmonyProgress(this.board, winPos, currentAI);
|
|||
|
|
const currentProgress = currentHeartPos ? this.getHarmonyProgress(this.board, currentHeartPos, currentAI) : 0;
|
|||
|
|
|
|||
|
|
// Only move if new position has significantly better progress (+2 or more)
|
|||
|
|
const shouldMove = potentialProgress > currentProgress + 1;
|
|||
|
|
|
|||
|
|
if (shouldMove) {
|
|||
|
|
this.log(`🤖 AI ${currentAI} moving heart to better position [${winPos.r},${winPos.c}] (progress: ${potentialProgress} vs current: ${currentProgress})`);
|
|||
|
|
await this.moveHeart(currentAI, winPos.r, winPos.c);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
let bestMove = { score: -Infinity, r: -1, c: -1, element: null, isHeartMove: false };
|
|||
|
|
const availableElements = Object.entries(this.inventory[currentAI])
|
|||
|
|
.filter(([_, count]) => count > 0)
|
|||
|
|
.map(([element, _]) => element);
|
|||
|
|
|
|||
|
|
// 1. Evaluate placing new pieces
|
|||
|
|
const emptyPositions = [];
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
if (this.board[r][c] === null && this.isPlayable(r, c)) {
|
|||
|
|
emptyPositions.push({ r, c });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const pos of emptyPositions) {
|
|||
|
|
for (const element of availableElements) {
|
|||
|
|
if (element === 'love' && !this.canPlaceHeart(currentAI, pos.r, pos.c)) continue;
|
|||
|
|
if (element !== 'love' && this.heartOnlyPositions.has(`${pos.r},${pos.c}`)) continue;
|
|||
|
|
|
|||
|
|
// Simulate the move and its consequences (captures)
|
|||
|
|
const { tempBoard, captures } = this.simulateMove(this.board, pos.r, pos.c, element, currentAI);
|
|||
|
|
let score = this.evaluateBoard(tempBoard, currentAI);
|
|||
|
|
|
|||
|
|
// Add bonus for captures
|
|||
|
|
score += captures * this.weights.capture;
|
|||
|
|
|
|||
|
|
// Extra bonus for placing elements adjacent to correctly positioned heart
|
|||
|
|
const currentHeartPos = this.heartPositions[currentAI];
|
|||
|
|
if (currentHeartPos && this.isCorrectPositionForStage(currentHeartPos, currentAI) && element !== 'love') {
|
|||
|
|
const isAdjacentToHeart = Math.abs(pos.r - currentHeartPos.r) + Math.abs(pos.c - currentHeartPos.c) === 1;
|
|||
|
|
if (isAdjacentToHeart) {
|
|||
|
|
score += this.weights.harmonyProgress * 3; // Big bonus for building harmony
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Apply penalty if the placed piece is immediately vulnerable
|
|||
|
|
const opponent = currentAI === 1 ? 2 : 1;
|
|||
|
|
const opponentAvailableElements = Object.entries(this.inventory[opponent])
|
|||
|
|
.filter(([_, count]) => count > 0)
|
|||
|
|
.map(([element, _]) => element);
|
|||
|
|
|
|||
|
|
for (const opponentElement of opponentAvailableElements) {
|
|||
|
|
if (this.dominance[opponentElement] === element) {
|
|||
|
|
const adjacentPositions = [
|
|||
|
|
{r: pos.r - 1, c: pos.c}, {r: pos.r + 1, c: pos.c},
|
|||
|
|
{r: pos.r, c: pos.c - 1}, {r: pos.r, c: pos.c + 1}
|
|||
|
|
];
|
|||
|
|
for (const adjPos of adjacentPositions) {
|
|||
|
|
if (this.isPlayable(adjPos.r, adjPos.c) && this.board[adjPos.r][adjPos.c] === null) {
|
|||
|
|
score -= this.weights.captureRiskPenalty;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Apply scarcity penalty
|
|||
|
|
const remainingPieces = this.inventory[currentAI][element];
|
|||
|
|
if (remainingPieces < 4) { // Apply penalty when pieces are running low
|
|||
|
|
const penalty = (4 - remainingPieces) * this.weights.scarcityPenalty;
|
|||
|
|
score -= penalty;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NEW: Penalty for moves that try to attack locked pieces (waste of time)
|
|||
|
|
if (element !== 'love') {
|
|||
|
|
const adjacentPositions = [
|
|||
|
|
{r: pos.r - 1, c: pos.c}, {r: pos.r + 1, c: pos.c},
|
|||
|
|
{r: pos.r, c: pos.c - 1}, {r: pos.r, c: pos.c + 1}
|
|||
|
|
];
|
|||
|
|
let lockedPiecesNearby = 0;
|
|||
|
|
for (const adjPos of adjacentPositions) {
|
|||
|
|
if (adjPos.r >= 0 && adjPos.r < 13 && adjPos.c >= 0 && adjPos.c < 13) {
|
|||
|
|
const targetCell = this.board[adjPos.r][adjPos.c];
|
|||
|
|
if (targetCell && this.lockedPieces.has(`${adjPos.r},${adjPos.c}`)) {
|
|||
|
|
const targetPlayer = parseInt(targetCell.slice(-1));
|
|||
|
|
if (targetPlayer !== currentAI) {
|
|||
|
|
const targetElement = this.getElementTypeFromSymbol(targetCell.slice(0, -1));
|
|||
|
|
if (this.dominance[element] === targetElement) {
|
|||
|
|
lockedPiecesNearby++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
score -= lockedPiecesNearby * this.weights.lockedPiecePenalty;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (element !== 'love' && this.heartPositions[currentAI]) {
|
|||
|
|
const heartPos = this.heartPositions[currentAI];
|
|||
|
|
const dist = Math.abs(pos.r - heartPos.r) + Math.abs(pos.c - heartPos.c);
|
|||
|
|
if (dist === 1) {
|
|||
|
|
score += this.weights.strategicMoveBonus;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (score > bestMove.score) {
|
|||
|
|
bestMove = { score, r: pos.r, c: pos.c, element, isHeartMove: false };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Evaluate moving the heart stone
|
|||
|
|
if (this.heartPositions[currentAI]) {
|
|||
|
|
const currentHeartPos = this.heartPositions[currentAI];
|
|||
|
|
|
|||
|
|
// Consider all valid heart positions (no stage restrictions)
|
|||
|
|
const validHeartPositions = [
|
|||
|
|
...this.cornerHeartPositions,
|
|||
|
|
...this.middleHeartPositions,
|
|||
|
|
this.centerPosition
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const pos of validHeartPositions) {
|
|||
|
|
const r = pos.r;
|
|||
|
|
const c = pos.c;
|
|||
|
|
|
|||
|
|
// Check if the move is valid and the spot is empty
|
|||
|
|
if (this.board[r][c] === null && this.canPlaceHeart(currentAI, r, c, true)) {
|
|||
|
|
const tempBoard = this.board.map(row => [...row]);
|
|||
|
|
tempBoard[currentHeartPos.r][currentHeartPos.c] = null; // Remove old heart
|
|||
|
|
tempBoard[r][c] = this.elements.love + currentAI; // Place new heart
|
|||
|
|
|
|||
|
|
let score = this.evaluateBoard(tempBoard, currentAI);
|
|||
|
|
|
|||
|
|
// Add basic bonus for moving the heart
|
|||
|
|
score += this.weights.heartMoveBonus;
|
|||
|
|
|
|||
|
|
// Bonus for moving to positions with good harmony progress
|
|||
|
|
if (this.completedHarmonies[currentAI].size > 0) {
|
|||
|
|
const harmonyProgress = this.getHarmonyProgress(tempBoard, {r, c}, currentAI);
|
|||
|
|
|
|||
|
|
// Bonus for moving to position with good harmony potential
|
|||
|
|
if (harmonyProgress >= 3) {
|
|||
|
|
score += this.weights.winningHeartPositionBonus; // Big bonus for near completion
|
|||
|
|
} else if (harmonyProgress >= 2) {
|
|||
|
|
score += this.weights.winningHeartPositionBonus / 2; // Bonus for decent progress
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (score > bestMove.score) {
|
|||
|
|
bestMove = { score, r, c, element: 'love', isHeartMove: true };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (bestMove.element) {
|
|||
|
|
if (bestMove.isHeartMove) {
|
|||
|
|
this.log(`🤖 AI ${currentAI} chose to MOVE its heart to [${bestMove.r},${bestMove.c}] with score ${bestMove.score.toFixed(2)}`);
|
|||
|
|
await this.moveHeart(currentAI, bestMove.r, bestMove.c);
|
|||
|
|
} else {
|
|||
|
|
this.log(`🤖 AI ${currentAI} chose to PLACE ${this.elements[bestMove.element]} at [${bestMove.r},${bestMove.c}] with score ${bestMove.score.toFixed(2)}`);
|
|||
|
|
await this.makeMove(bestMove.r, bestMove.c, bestMove.element, currentAI);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
this.log(`🤖 AI ${currentAI} has no strategic moves, passing turn.`);
|
|||
|
|
this.nextPlayer();
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleVictory(winner) {
|
|||
|
|
this.gameOver = true;
|
|||
|
|
this.winner = winner;
|
|||
|
|
|
|||
|
|
let playerName;
|
|||
|
|
if (winner === null) {
|
|||
|
|
playerName = "Nobody - it's a tie!";
|
|||
|
|
this.log("🤝 Game ended in a tie!");
|
|||
|
|
} else {
|
|||
|
|
const playerType = this.playerTypes[winner] === 'ai' ? 'AI' : 'Player';
|
|||
|
|
playerName = `${playerType} ${winner}`;
|
|||
|
|
this.log(`🏆 ${playerName} wins the game!`);
|
|||
|
|
|
|||
|
|
// Explode losing player's pieces
|
|||
|
|
await this.explodeLosersStones(winner);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.updateDisplay();
|
|||
|
|
|
|||
|
|
// If both players are AI, automatically restart after a short delay
|
|||
|
|
if (this.playerTypes[1] === 'ai' && this.playerTypes[2] === 'ai') {
|
|||
|
|
const restartDelay = this.presentationMode ? 500 : 2000; // Faster restart in presentation mode
|
|||
|
|
this.log(`🤖 Both players are AI - auto-restarting in ${restartDelay/1000} seconds...`);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.resetGame();
|
|||
|
|
}, restartDelay);
|
|||
|
|
} else {
|
|||
|
|
// Show victory announcement for human players
|
|||
|
|
setTimeout(() => {
|
|||
|
|
alert(`🏆 ${playerName}`);
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetGame() {
|
|||
|
|
this.board = Array(13).fill().map(() => Array(13).fill(null));
|
|||
|
|
this.currentPlayer = 1;
|
|||
|
|
this.gamePhase = 'playing';
|
|||
|
|
this.selectedPieceType = null;
|
|||
|
|
this.aiMoveDelay = 500;
|
|||
|
|
this.completedHarmonies = { 1: new Set(), 2: new Set() };
|
|||
|
|
this.harmoniesCount = { 1: 0, 2: 0 };
|
|||
|
|
this.lockedPieces = new Set();
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Reset inventories
|
|||
|
|
this.inventory = {
|
|||
|
|
1: { fire: 12, water: 12, earth: 12, air: 12, love: 1 },
|
|||
|
|
2: { fire: 12, water: 12, earth: 12, air: 12, love: 1 }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Reset heart positions
|
|||
|
|
this.heartPositions = { 1: null, 2: null };
|
|||
|
|
|
|||
|
|
this.gameOver = false;
|
|||
|
|
this.winner = null;
|
|||
|
|
|
|||
|
|
// Clear UI selections
|
|||
|
|
document.querySelectorAll('.element-btn').forEach(btn => {
|
|||
|
|
btn.classList.remove('selected');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.initBoard();
|
|||
|
|
this.updateDisplay();
|
|||
|
|
this.startGame();
|
|||
|
|
this.log("🔄 New game started");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
undoMove() {
|
|||
|
|
// TODO: Implement undo functionality
|
|||
|
|
this.log("↩️ Undo not implemented yet");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toggleFullscreen() {
|
|||
|
|
const body = document.body;
|
|||
|
|
const gameContainer = document.querySelector('.game-container');
|
|||
|
|
const fullscreenBtn = document.getElementById('fullscreenToggle');
|
|||
|
|
|
|||
|
|
if (!document.fullscreenElement) {
|
|||
|
|
// Enter fullscreen
|
|||
|
|
document.documentElement.requestFullscreen().then(() => {
|
|||
|
|
body.classList.add('fullscreen-mode');
|
|||
|
|
fullscreenBtn.textContent = '❌ Exit Presentation';
|
|||
|
|
this.log("📺 Entered presentation mode");
|
|||
|
|
this.resizeForFullscreen();
|
|||
|
|
}).catch(err => {
|
|||
|
|
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// Exit fullscreen
|
|||
|
|
if (document.exitFullscreen) {
|
|||
|
|
document.exitFullscreen().then(() => {
|
|||
|
|
body.classList.remove('fullscreen-mode');
|
|||
|
|
fullscreenBtn.textContent = '📺 Presentation Mode';
|
|||
|
|
this.log("📺 Exited presentation mode");
|
|||
|
|
this.resizeForFullscreen(); // Resize back to normal
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toggleConsole() {
|
|||
|
|
const gameLog = document.getElementById('gameLog');
|
|||
|
|
const consoleBtn = document.getElementById('consoleToggle');
|
|||
|
|
|
|||
|
|
if (gameLog.classList.contains('visible')) {
|
|||
|
|
// Hide console
|
|||
|
|
gameLog.classList.remove('visible');
|
|||
|
|
consoleBtn.textContent = '📝 Show Console';
|
|||
|
|
this.log("📝 Console hidden");
|
|||
|
|
} else {
|
|||
|
|
// Show console
|
|||
|
|
gameLog.classList.add('visible');
|
|||
|
|
consoleBtn.textContent = '❌ Hide Console';
|
|||
|
|
this.log("📝 Console visible");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resizeForFullscreen() {
|
|||
|
|
const board = document.getElementById('board');
|
|||
|
|
const isFullscreen = document.body.classList.contains('fullscreen-mode');
|
|||
|
|
|
|||
|
|
let size, gridSize, stoneSize;
|
|||
|
|
|
|||
|
|
if (isFullscreen) {
|
|||
|
|
size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
|
|||
|
|
stoneSize = size / 20;
|
|||
|
|
} else {
|
|||
|
|
size = 560; // Original board size
|
|||
|
|
stoneSize = 12; // Original stone font size
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
board.style.width = size + 'px';
|
|||
|
|
board.style.height = size + 'px';
|
|||
|
|
|
|||
|
|
gridSize = size - 80; // Account for margins
|
|||
|
|
|
|||
|
|
// Recalculate grid line positions
|
|||
|
|
const horizontalLines = board.querySelectorAll('.grid-line.horizontal');
|
|||
|
|
horizontalLines.forEach((line, i) => {
|
|||
|
|
line.style.top = `${40 + (i * gridSize / 12)}px`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const verticalLines = board.querySelectorAll('.grid-line.vertical');
|
|||
|
|
verticalLines.forEach((line, i) => {
|
|||
|
|
line.style.left = `${40 + (i * gridSize / 12)}px`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Recalculate intersection positions and sizes
|
|||
|
|
const intersections = document.querySelectorAll('.intersection');
|
|||
|
|
intersections.forEach(intersection => {
|
|||
|
|
const r = parseInt(intersection.dataset.row);
|
|||
|
|
const c = parseInt(intersection.dataset.col);
|
|||
|
|
const left = 40 + (c * gridSize / 12);
|
|||
|
|
const top = 40 + (r * gridSize / 12);
|
|||
|
|
|
|||
|
|
intersection.style.left = left + 'px';
|
|||
|
|
intersection.style.top = top + 'px';
|
|||
|
|
|
|||
|
|
if (isFullscreen) {
|
|||
|
|
intersection.style.width = stoneSize + 'px';
|
|||
|
|
intersection.style.height = stoneSize + 'px';
|
|||
|
|
intersection.style.fontSize = (stoneSize * 0.6) + 'px';
|
|||
|
|
} else {
|
|||
|
|
// Reset to original CSS values
|
|||
|
|
intersection.style.width = '';
|
|||
|
|
intersection.style.height = '';
|
|||
|
|
intersection.style.fontSize = '';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nextPlayer() {
|
|||
|
|
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
|
|||
|
|
|
|||
|
|
// Clear piece selection when switching away from human
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|||
|
|
this.selectedPieceType = null;
|
|||
|
|
document.querySelectorAll('.element-btn').forEach(btn => {
|
|||
|
|
btn.classList.remove('selected');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateBoard() {
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
const intersection = document.querySelector(`[data-row="${r}"][data-col="${c}"]`);
|
|||
|
|
if (intersection) {
|
|||
|
|
const cellContent = this.board[r][c];
|
|||
|
|
if (cellContent) {
|
|||
|
|
// Remove player number from display
|
|||
|
|
intersection.textContent = cellContent.slice(0, -1);
|
|||
|
|
|
|||
|
|
// Add player background color
|
|||
|
|
const playerNum = cellContent.slice(-1);
|
|||
|
|
intersection.classList.remove('player-1', 'player-2');
|
|||
|
|
intersection.classList.add(`player-${playerNum}`);
|
|||
|
|
|
|||
|
|
// Add harmony piece class if locked
|
|||
|
|
if (this.lockedPieces.has(`${r},${c}`)) {
|
|||
|
|
intersection.classList.add('harmony-piece');
|
|||
|
|
} else {
|
|||
|
|
intersection.classList.remove('harmony-piece');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
intersection.textContent = '';
|
|||
|
|
intersection.classList.remove('player-1', 'player-2', 'harmony-piece');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lockHarmonyPieces(heartPos) {
|
|||
|
|
const player = this.currentPlayer;
|
|||
|
|
const completedCount = this.harmoniesCount[player];
|
|||
|
|
|
|||
|
|
if (completedCount === 1) {
|
|||
|
|
// First harmony: lock the 4 adjacent cross pieces
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c}, {r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1}, {r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
if (this.board[pos.r]?.[pos.c]) {
|
|||
|
|
this.lockedPieces.add(`${pos.r},${pos.c}`);
|
|||
|
|
this.log(`🔒 Piece at [${pos.r},${pos.c}] is now locked.`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// Subsequent harmonies: lock all pieces in the connected chain
|
|||
|
|
this.lockConnectedChainPieces(heartPos, player);
|
|||
|
|
}
|
|||
|
|
this.updateBoard();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lockConnectedChainPieces(heartPos, player) {
|
|||
|
|
// Find the element directly connected to heart
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heartPos.r - 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r + 1, c: heartPos.c},
|
|||
|
|
{r: heartPos.r, c: heartPos.c - 1},
|
|||
|
|
{r: heartPos.r, c: heartPos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const directElements = [];
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = this.board[pos.r]?.[pos.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const elementSymbol = cell.slice(0, -1);
|
|||
|
|
const elementType = this.getElementTypeFromSymbol(elementSymbol);
|
|||
|
|
if (elementType && elementType !== 'love') {
|
|||
|
|
directElements.push(pos);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (directElements.length !== 1) return; // Should be exactly 1 for valid subsequent harmony
|
|||
|
|
|
|||
|
|
const startPos = directElements[0];
|
|||
|
|
const visited = new Set();
|
|||
|
|
const chainPositions = [];
|
|||
|
|
|
|||
|
|
// Find all connected elements starting from the one connected to heart
|
|||
|
|
const findConnectedPositions = (pos) => {
|
|||
|
|
const key = `${pos.r},${pos.c}`;
|
|||
|
|
if (visited.has(key)) return;
|
|||
|
|
visited.add(key);
|
|||
|
|
chainPositions.push(pos);
|
|||
|
|
|
|||
|
|
const neighbors = [
|
|||
|
|
{r: pos.r - 1, c: pos.c},
|
|||
|
|
{r: pos.r + 1, c: pos.c},
|
|||
|
|
{r: pos.r, c: pos.c - 1},
|
|||
|
|
{r: pos.r, c: pos.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const neighbor of neighbors) {
|
|||
|
|
const cell = this.board[neighbor.r]?.[neighbor.c];
|
|||
|
|
if (cell && cell.endsWith(player.toString())) {
|
|||
|
|
const neighborSymbol = cell.slice(0, -1);
|
|||
|
|
const neighborType = this.getElementTypeFromSymbol(neighborSymbol);
|
|||
|
|
if (neighborType && neighborType !== 'love') {
|
|||
|
|
findConnectedPositions(neighbor);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
findConnectedPositions(startPos);
|
|||
|
|
|
|||
|
|
// Lock all pieces in the connected chain
|
|||
|
|
for (const pos of chainPositions) {
|
|||
|
|
this.lockedPieces.add(`${pos.r},${pos.c}`);
|
|||
|
|
this.log(`🔒 Piece at [${pos.r},${pos.c}] is now locked.`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log(message) {
|
|||
|
|
const gameLog = document.getElementById('gameLog');
|
|||
|
|
const timestamp = new Date().toLocaleTimeString();
|
|||
|
|
gameLog.innerHTML += `[${timestamp}] ${message}<br>`;
|
|||
|
|
gameLog.scrollTop = gameLog.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async animateWinningLine() {
|
|||
|
|
const boardContainer = document.querySelector('.board-container');
|
|||
|
|
const boardRect = boardContainer.getBoundingClientRect();
|
|||
|
|
const centerX = boardRect.width / 2;
|
|||
|
|
const centerY = boardRect.height / 2;
|
|||
|
|
|
|||
|
|
for (const cell of this.winningLine) {
|
|||
|
|
const intersectionElement = document.querySelector(`[data-row="${cell.r}"][data-col="${cell.c}"]`);
|
|||
|
|
const intersectionRect = intersectionElement.getBoundingClientRect();
|
|||
|
|
const intersectionCenterX = intersectionRect.left + intersectionRect.width / 2 - boardRect.left;
|
|||
|
|
const intersectionCenterY = intersectionRect.top + intersectionRect.height / 2 - boardRect.top;
|
|||
|
|
|
|||
|
|
const deltaX = intersectionCenterX - centerX;
|
|||
|
|
const deltaY = intersectionCenterY - centerY;
|
|||
|
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|||
|
|
const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
|
|||
|
|
|
|||
|
|
const missile = document.createElement('div');
|
|||
|
|
missile.className = 'missile-line';
|
|||
|
|
missile.style.left = centerX + 'px';
|
|||
|
|
missile.style.top = centerY + 'px';
|
|||
|
|
missile.style.transform = `rotate(${angle}deg)`;
|
|||
|
|
missile.style.setProperty('--line-length', distance + 'px');
|
|||
|
|
|
|||
|
|
boardContainer.appendChild(missile);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
intersectionElement.classList.add('winning');
|
|||
|
|
setTimeout(() => missile.remove(), 500);
|
|||
|
|
}, 500);
|
|||
|
|
|
|||
|
|
await this.sleep(200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for all missiles to complete
|
|||
|
|
await this.sleep(800);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleDraw() {
|
|||
|
|
// Apply tie-breaker rule: Elemental Supremacy
|
|||
|
|
const winner = this.checkElementalSupremacy();
|
|||
|
|
if (winner) {
|
|||
|
|
this.wins[winner]++;
|
|||
|
|
this.log(`⚔️ AI ${winner} wins by Elemental Supremacy! Total wins: AI 1: ${this.wins[1]}, AI 2: ${this.wins[2]} ⚔️`);
|
|||
|
|
await this.explodeLosersStones(winner);
|
|||
|
|
} else {
|
|||
|
|
this.log("🤝 True draw - MUTUAL DESTRUCTION!");
|
|||
|
|
await this.mutualDestruction();
|
|||
|
|
}
|
|||
|
|
this.updateDisplay();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkElementalSupremacy() {
|
|||
|
|
const scores = {};
|
|||
|
|
|
|||
|
|
for (let player = 1; player <= 2; player++) {
|
|||
|
|
// Only players who achieved Harmony can win by Elemental Supremacy
|
|||
|
|
if (!this.harmonyAchieved[player]) {
|
|||
|
|
scores[player] = -1;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const heart = this.heartPlacements[player];
|
|||
|
|
if (!heart) {
|
|||
|
|
scores[player] = -1;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Count element stones adjacent to heart
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heart.r - 1, c: heart.c},
|
|||
|
|
{r: heart.r + 1, c: heart.c},
|
|||
|
|
{r: heart.r, c: heart.c - 1},
|
|||
|
|
{r: heart.r, c: heart.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let elementCount = 0;
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const cell = this.board[pos.r]?.[pos.c];
|
|||
|
|
if (Object.values(this.elements).some(element => cell?.includes(element))) {
|
|||
|
|
elementCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
scores[player] = elementCount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log(`💎 Elemental Supremacy scores - AI 1: ${scores[1]}, AI 2: ${scores[2]}`);
|
|||
|
|
|
|||
|
|
if (scores[1] > scores[2] && scores[1] >= 0) return 1;
|
|||
|
|
if (scores[2] > scores[1] && scores[2] >= 0) return 2;
|
|||
|
|
return null; // True tie
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
cleanupBoardAnimations() {
|
|||
|
|
// Remove all CSS animation classes from intersections
|
|||
|
|
const intersections = document.querySelectorAll('.intersection');
|
|||
|
|
intersections.forEach(intersection => {
|
|||
|
|
intersection.classList.remove('exploding', 'imploding', 'winning', 'highlight', 'player-1', 'player-2');
|
|||
|
|
// Clear any inline styles that might persist
|
|||
|
|
intersection.style.animation = '';
|
|||
|
|
intersection.style.transform = '';
|
|||
|
|
intersection.style.opacity = '';
|
|||
|
|
intersection.style.background = '';
|
|||
|
|
intersection.style.boxShadow = '';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Remove any missile elements that might still be in the DOM
|
|||
|
|
const missiles = document.querySelectorAll('.missile-line');
|
|||
|
|
missiles.forEach(missile => missile.remove());
|
|||
|
|
|
|||
|
|
// Force browser repaint to ensure animations are cleared
|
|||
|
|
const board = document.getElementById('board');
|
|||
|
|
void board.offsetHeight; // Force repaint
|
|||
|
|
|
|||
|
|
this.log("🧹 Animation cleanup complete");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nextPlayer() {
|
|||
|
|
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
|
|||
|
|
|
|||
|
|
// Clear piece selection when switching away from human
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'ai') {
|
|||
|
|
this.selectedPieceType = null;
|
|||
|
|
document.querySelectorAll('.element-btn').forEach(btn => {
|
|||
|
|
btn.classList.remove('selected');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateDisplay() {
|
|||
|
|
// Update current player indicator
|
|||
|
|
if (this.gameOver) {
|
|||
|
|
const winnerName = this.playerTypes[this.winner] === 'human'
|
|||
|
|
? (this.winner === 1 ? 'Player 1' : 'Player 2')
|
|||
|
|
: `AI ${this.winner}`;
|
|||
|
|
document.getElementById('currentPlayer').textContent = `🏆 ${winnerName} Won!`;
|
|||
|
|
} else {
|
|||
|
|
const playerIcon = this.playerTypes[this.currentPlayer] === 'human' ? '👤' : '🤖';
|
|||
|
|
const playerName = this.playerTypes[this.currentPlayer] === 'human'
|
|||
|
|
? `Player ${this.currentPlayer}`
|
|||
|
|
: `AI ${this.currentPlayer}`;
|
|||
|
|
document.getElementById('currentPlayer').textContent = `${playerIcon} ${playerName}'s Turn`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update game status
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'human' && !this.gameOver) {
|
|||
|
|
if (this.selectedPieceType) {
|
|||
|
|
document.getElementById('gameStatus').textContent =
|
|||
|
|
`Selected: ${this.elements[this.selectedPieceType]} - Click board to place`;
|
|||
|
|
} else {
|
|||
|
|
document.getElementById('gameStatus').textContent = "Select a piece type below";
|
|||
|
|
}
|
|||
|
|
} else if (this.playerTypes[this.currentPlayer] === 'ai' && !this.gameOver) {
|
|||
|
|
document.getElementById('gameStatus').textContent = `AI ${this.currentPlayer} is thinking...`;
|
|||
|
|
} else {
|
|||
|
|
document.getElementById('gameStatus').textContent = "Game Over";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update piece counts and button visibility
|
|||
|
|
const humanPlayer = this.playerTypes[this.currentPlayer] === 'human' ? this.currentPlayer :
|
|||
|
|
(this.playerTypes[1] === 'human' ? 1 : (this.playerTypes[2] === 'human' ? 2 : 1));
|
|||
|
|
|
|||
|
|
for (const element of ['fire', 'water', 'earth', 'air', 'love']) {
|
|||
|
|
const btn = document.getElementById(`${element}-btn`);
|
|||
|
|
const count = this.inventory[humanPlayer][element];
|
|||
|
|
btn.textContent = `${this.elements[element]} ${element.charAt(0).toUpperCase() + element.slice(1)} (${count})`;
|
|||
|
|
btn.disabled = count === 0 || this.gameOver || this.playerTypes[this.currentPlayer] === 'ai';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Show/hide element buttons based on current player
|
|||
|
|
const elementButtons = document.querySelector('.element-buttons');
|
|||
|
|
if (this.playerTypes[this.currentPlayer] === 'human') {
|
|||
|
|
elementButtons.style.display = 'grid';
|
|||
|
|
} else {
|
|||
|
|
elementButtons.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update victory status
|
|||
|
|
document.getElementById('gameStageP1').textContent = `P1 Stage: ${this.gameStage[1].replace('_', ' ')}`;
|
|||
|
|
document.getElementById('gameStageP2').textContent = `P2 Stage: ${this.gameStage[2].replace('_', ' ')}`;
|
|||
|
|
|
|||
|
|
this.updateBoard();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
hasValidMovesForBothPlayers() {
|
|||
|
|
// Check if either player has valid moves
|
|||
|
|
for (let player = 1; player <= 2; player++) {
|
|||
|
|
// Can place element stones?
|
|||
|
|
if (this.elementStones[player] > 0) {
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
if (this.isPlayable(r, c) && this.board[r][c] === null) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Can move heart stone? (only if not the last mover)
|
|||
|
|
if (this.lastHeartMover !== player && this.elementStones[player] === 0) {
|
|||
|
|
const hasHeartMoveAvailable = this.validHeartSpots.some(spot =>
|
|||
|
|
this.board[spot.r][spot.c] === null
|
|||
|
|
);
|
|||
|
|
if (hasHeartMoveAvailable) return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async animateHarmonyWin(winner) {
|
|||
|
|
const heart = this.heartPlacements[winner];
|
|||
|
|
if (!heart) return;
|
|||
|
|
|
|||
|
|
const adjacent = [
|
|||
|
|
{r: heart.r - 1, c: heart.c},
|
|||
|
|
{r: heart.r + 1, c: heart.c},
|
|||
|
|
{r: heart.r, c: heart.c - 1},
|
|||
|
|
{r: heart.r, c: heart.c + 1}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Highlight heart stone first
|
|||
|
|
const heartElement = document.querySelector(`[data-row="${heart.r}"][data-col="${heart.c}"]`);
|
|||
|
|
if (heartElement) {
|
|||
|
|
heartElement.classList.add('winning');
|
|||
|
|
await this.sleep(200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Then highlight adjacent elements
|
|||
|
|
for (const pos of adjacent) {
|
|||
|
|
const element = document.querySelector(`[data-row="${pos.r}"][data-col="${pos.c}"]`);
|
|||
|
|
if (element && Object.values(this.elements).some(elem => this.board[pos.r]?.[pos.c]?.includes(elem))) {
|
|||
|
|
element.classList.add('winning');
|
|||
|
|
await this.sleep(200);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for harmony animation to complete
|
|||
|
|
await this.sleep(500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async explodeLosersStones(winner) {
|
|||
|
|
const loser = winner === 1 ? 2 : 1;
|
|||
|
|
const loserStones = [];
|
|||
|
|
|
|||
|
|
// Find all loser's stones
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
const cell = this.board[r][c];
|
|||
|
|
if (cell === `❤️${loser}` || (Object.values(this.elements).some(element => cell?.includes(element)) && Math.random() < 0.5)) {
|
|||
|
|
loserStones.push({r, c});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Implode loser's stones randomly
|
|||
|
|
const shuffled = loserStones.sort(() => Math.random() - 0.5);
|
|||
|
|
for (const stone of shuffled) {
|
|||
|
|
const element = document.querySelector(`[data-row="${stone.r}"][data-col="${stone.c}"]`);
|
|||
|
|
if (element) {
|
|||
|
|
element.classList.add('imploding');
|
|||
|
|
await this.sleep(100);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for animations to complete
|
|||
|
|
await this.sleep(1200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async mutualDestruction() {
|
|||
|
|
const allStones = [];
|
|||
|
|
|
|||
|
|
// Find all stones on the board
|
|||
|
|
for (let r = 0; r < 13; r++) {
|
|||
|
|
for (let c = 0; c < 13; c++) {
|
|||
|
|
if (this.board[r][c] !== null) {
|
|||
|
|
allStones.push({r, c});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Explode everything randomly
|
|||
|
|
const shuffled = allStones.sort(() => Math.random() - 0.5);
|
|||
|
|
for (const stone of shuffled) {
|
|||
|
|
const element = document.querySelector(`[data-row="${stone.r}"][data-col="${stone.c}"]`);
|
|||
|
|
if (element) {
|
|||
|
|
element.classList.add('exploding');
|
|||
|
|
await this.sleep(50); // Faster for chaos effect
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log("💥💥💥 TOTAL ANNIHILATION! 💥💥💥");
|
|||
|
|
|
|||
|
|
// Wait for all explosions to complete
|
|||
|
|
await this.sleep(1200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sleep(ms) {
|
|||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|||
|
|
const game = new HarmonyGame();
|
|||
|
|
window.game = game; // Make game accessible for debugging
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|