harmony/harmony.html

2465 lines
108 KiB
HTML
Raw Normal View History

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