harmony/harmony.html
Vincent Palmer 728b742260 Batman!
Signed-off-by: Vincent Palmer <shift@someone.section.me>
2025-09-30 15:54:08 +02:00

2465 lines
No EOL
108 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>