🏰 Blocade 🗡️
Current Player:
Player 1
🤖 AI
Last Roll:
🎲
Blocade
PHP
A blocade board game. Be the first to reach the top
<?php
/**
* Plugin Name: Blocade
* Plugin URI: https://it-breeze.info/
* Description: For a blocade board game with AI players
* Version: 2.3.0
* Author: Mike Vahldieck
* Author URI: https://it-breeze.info/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
<?php
/**
* Plugin Name: Blocade
* Plugin URI: https://it-breeze.info/
* Description: For a blocade board game with AI players
* Version: 2.3.0
* Author: Mike Vahldieck
* Author URI: https://it-breeze.info/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: blocade
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
class ExactBlockadeGame {
public function __construct() {
add_action('init', array($this, 'init'));
add_action('plugins_loaded', array($this, 'load_textdomain'));
add_shortcode('blocade', array($this, 'display_game'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('wp_ajax_blockade_action', array($this, 'handle_ajax'));
add_action('wp_ajax_nopriv_blockade_action', array($this, 'handle_ajax'));
}
public function init() {
}
public function load_textdomain() {
load_plugin_textdomain('blocade', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
public function enqueue_scripts() {
wp_enqueue_script('jquery');
wp_localize_script('jquery', 'blocadeL10n', array(
'currentPlayer' => __('Current Player:', 'blocade'),
'lastRoll' => __('Last Roll:', 'blocade'),
'rollDice' => __('🎲 Roll Dice', 'blocade'),
'startGame' => __('🎮 Start Game', 'blocade'),
'newGame' => __('🔄 New Game', 'blocade'),
'aiIndicator' => __('🤖 AI', 'blocade'),
'playerRolled' => __('Player %d rolled %d', 'blocade'),
'playerRolledSix' => __('Player %d rolled a 6! Gets another turn!', 'blocade'),
'noValidMoves' => __('Player %d has no valid moves with roll %d! Turn ends.', 'blocade'),
'playerWins' => __('🎉 Player %d (%s) wins! Reached position 112! 🏆', 'blocade'),
'aiThinking' => __('AI Player %d is thinking...', 'blocade'),
'cannotPlaceHere' => __('❌ Cannot place barricade here! Choose an empty space.', 'blocade'),
'cannotPlaceStartOrTarget' => __('❌ Cannot place barricade in starting areas or target!', 'blocade'),
'invalidPlacement' => __('❌ Invalid placement location!', 'blocade'),
'humanText' => __('Human', 'blocade'),
'aiText' => __('AI', 'blocade'),
'playerNames' => array(
__('🚒 Red Fire Truck', 'blocade'),
__('🚚 Green Delivery', 'blocade'),
__('🚕 Yellow Taxi', 'blocade'),
__('🚐 Blue Family Van', 'blocade')
),
'playerOptions' => array(
__('1 Human + 3 AI', 'blocade'),
__('2 Human + 2 AI', 'blocade'),
__('3 Human + 1 AI', 'blocade'),
__('4 Human Players', 'blocade')
),
'rollDiceTitle' => __('Roll the dice', 'blocade'),
'aiPlayerTurnTitle' => __('AI Player Turn', 'blocade'),
'notYourTurnTitle' => __('Not your turn to roll', 'blocade'),
'truckAlt' => __('Player %d Truck %d', 'blocade'),
'stopAlt' => __('Stop', 'blocade')
));
}
public function display_game($atts) {
$atts = shortcode_atts(array(
'players' => 1,
), $atts);
$plugin_url = plugin_dir_url(__FILE__);
$stop_image_url = $plugin_url . 'img/stop.png';
$fire_truck_url = $plugin_url . 'img/fire-truck.png';
$deliver_truck_url = $plugin_url . 'img/deliver-truck.png';
$taxi_url = $plugin_url . 'img/taxi.png';
$family_van_url = $plugin_url . 'img/family-van.png';
ob_start();
?>
<div id="exact-blockade-container">
<style>
#exact-blockade-container {
max-width: 1050px;
margin: 20px auto;
font-family: 'Trebuchet MS', Arial, sans-serif;
background: linear-gradient(135deg, #B8860B, #DAA520);
padding: 20px;
border-radius: 15px;
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
}
#blockade-board-container {
position: relative;
width: 950px;
height: 850px;
margin: 20px auto;
background: linear-gradient(45deg, #F0E68C 0%, #BDB76B 50%, #B8860B 100%);
border: 8px solid #654321;
border-radius: 20px;
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
}
.path-space {
position: absolute;
width: 32px;
height: 32px;
background: radial-gradient(circle, #2F2F2F 60%, #000000 100%);
border: 3px solid #654321;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 2px 2px 4px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
}
.path-space:hover {
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255,215,0,0.6);
}
.connection-line {
position: absolute;
z-index: 1;
border-radius: 2px;
opacity: 0.8;
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
.path-space.barricade {
color: transparent !important;
font-size: 0 !important;
font-weight: bold;
background: transparent !important;
border: none !important;
box-shadow: none !important;
animation: barricade-pulse 2s ease-in-out infinite alternate;
}
.barricade-image {
display: inline-block;
max-width: 58px;
object-fit: contain;
vertical-align: middle;
}
.path-space.barricade.highlight-move {
box-shadow: 0 0 15px rgba(255,165,0,0.8) !important;
border: 3px solid #FFA500 !important;
animation: barricade-highlight 1s ease-in-out infinite alternate !important;
}
@keyframes barricade-highlight {
from {
box-shadow: 0 0 15px rgba(255,165,0,0.8) !important;
border-color: #FFA500 !important;
}
to {
box-shadow: 0 0 25px rgba(255,165,0,1.0) !important;
border-color: #FF8C00 !important;
}
}
@keyframes barricade-pulse {
from { box-shadow: 0 0 10px rgba(220, 20, 60, 0.8); }
to { box-shadow: 0 0 20px rgba(220, 20, 60, 1.0); }
}
.barricade-placement-mode {
background: linear-gradient(145deg, #FFD700, #FFA500) !important;
border: 3px solid #FF4500 !important;
animation: placement-highlight 0.5s ease-in-out infinite alternate !important;
}
@keyframes placement-highlight {
from { box-shadow: 0 0 8px rgba(255, 215, 0, 0.8); }
to { box-shadow: 0 0 16px rgba(255, 215, 0, 1.0); }
}
.starting-area {
position: absolute;
width: 120px;
height: 60px;
border-radius: 15px;
border: 4px solid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 6px;
background: linear-gradient(145deg, rgba(255,255,255,0.8), rgba(255,255,255,0.4));
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.starting-area.player-1 {
border-color: #DC143C;
background: linear-gradient(145deg, rgba(220,20,60,0.3), rgba(220,20,60,0.1));
}
.starting-area.player-2 {
border-color: #228B22;
background: linear-gradient(145deg, rgba(34,139,34,0.3), rgba(34,139,34,0.1));
}
.starting-area.player-3 {
border-color: #FFD700;
background: linear-gradient(145deg, rgba(255,215,0,0.3), rgba(255,215,0,0.1));
}
.starting-area.player-4 {
border-color: #4169E1;
background: linear-gradient(145deg, rgba(65,105,225,0.3), rgba(65,105,225,0.1));
}
.player-character {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #000;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
margin-bottom: 3px;
box-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.character-1 { background: linear-gradient(145deg, #FF6B6B, #DC143C); }
.character-2 { background: linear-gradient(145deg, #51CF66, #228B22); }
.character-3 { background: linear-gradient(145deg, #FFE066, #FFD700); }
.character-4 { background: linear-gradient(145deg, #74C0FC, #4169E1); }
.starting-spaces {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
}
.start-space {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(101, 67, 33, 0.3);
cursor: pointer;
opacity: 0.1;
}
.finish-target {
position: absolute;
top: 60px;
left: 414px;
width: 50px;
height: 50px;
background: radial-gradient(circle, #FFD700 0%, #DC143C 25%, #FFFFFF 35%, #DC143C 45%, #FFFFFF 55%, #DC143C 65%, #FFFFFF 75%, #8B0000 100%);
border: 4px solid #654321;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 15px rgba(255,215,0,0.6);
animation: target-glow 2s ease-in-out infinite alternate;
cursor: pointer;
}
@keyframes target-glow {
from { box-shadow: 0 0 15px rgba(255,215,0,0.6); }
to { box-shadow: 0 0 25px rgba(255,215,0,0.9); }
}
.game-piece {
position: absolute;
width: 32px;
height: 32px;
border-radius: 50%;
border: 3px solid #000;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 2px 2px 4px rgba(0,0,0,0.4);
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
overflow: hidden;
}
.game-piece img {
width: 26px;
height: 26px;
object-fit: contain;
border-radius: 50%;
}
.game-piece:hover {
transform: translate(-50%, -50%) scale(1.15);
}
.game-piece.selected {
border-color: #FFD700;
border-width: 4px;
box-shadow: 0 0 12px rgba(255,215,0,0.8);
animation: piece-pulse 1s ease-in-out infinite alternate;
}
@keyframes piece-pulse {
from { transform: translate(-50%, -50%) scale(1); }
to { transform: translate(-50%, -50%) scale(1.1); }
}
.piece-1 { border-color: #DC143C; }
.piece-2 { border-color: #228B22; }
.piece-3 { border-color: #FFD700; }
.piece-4 { border-color: #4169E1; }
#game-controls {
text-align: center;
margin: 20px 0;
background: rgba(255,255,255,0.9);
padding: 20px;
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
#game-status {
padding: 15px;
border-radius: 10px;
margin: 10px 0;
border: 2px solid #000000;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dice {
font-size: 32px;
background: linear-gradient(145deg, #FFFFFF, #F0F0F0);
border: 3px solid #654321;
padding: 15px;
border-radius: 10px;
margin: 0 10px;
box-shadow: 3px 3px 6px rgba(0,0,0,0.3);
display: inline-block;
min-width: 60px;
text-align: center;
}
button {
background: linear-gradient(145deg, #4CAF50, #45a049);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
margin: 5px;
font-size: 16px;
font-weight: bold;
box-shadow: 3px 3px 6px rgba(0,0,0,0.2);
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 3px 5px 8px rgba(0,0,0,0.3);
}
button:disabled {
background: linear-gradient(145deg, #cccccc, #999999);
cursor: not-allowed;
transform: none;
}
.highlight-move {
animation: move-highlight 1s ease-in-out infinite alternate;
}
@keyframes move-highlight {
from {
box-shadow: 0 0 8px rgba(0,255,0,0.6);
border-color: #00FF00;
}
to {
box-shadow: 0 0 15px rgba(0,255,0,0.9);
border-color: #32CD32;
}
}
.game-title {
text-align: center;
font-size: 32px;
font-weight: bold;
color: #000000;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
margin-bottom: 20px;
}
.player-setup {
background: rgba(255,255,255,0.9);
padding: 15px;
border-radius: 10px;
margin: 10px 0;
text-align: center;
}
.player-setup select {
padding: 8px 12px;
font-size: 16px;
border-radius: 5px;
border: 2px solid #4169E1;
margin: 0 10px;
}
.ai-indicator {
display: inline-block;
background: linear-gradient(145deg, #FF6B6B, #DC143C);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-left: 5px;
}
.ai-thinking {
animation: ai-pulse 1s ease-in-out infinite alternate;
}
@keyframes ai-pulse {
from { opacity: 0.7; }
to { opacity: 1.0; }
}
#game-status {
width: 100%;
}
.game-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.left-controls {
display: flex;
align-items: center;
gap: 10px;
}
.center-controls {
flex-grow: 1;
display: flex;
justify-content: center;
}
.right-controls {
display: flex;
gap: 10px;
}
</style>
<h2 class="game-title"><?php echo esc_html__('🏰 Blocade 🗡️', 'blocade'); ?></h2>
<div id="game-status">
<div><strong><?php echo esc_html__('Current Player:', 'blocade'); ?></strong>
<span id="current-player"><?php echo esc_html__('Player 1', 'blocade'); ?></span>
<span id="ai-indicator" class="ai-indicator" style="display: none;"><?php echo esc_html__('🤖 AI', 'blocade'); ?></span>
</div>
<div class="game-controls">
<div class="left-controls">
<strong><?php echo esc_html__('Last Roll:', 'blocade'); ?></strong>
<span id="dice-result" class="dice">🎲</span>
<button id="roll-dice" onclick="rollDice()"><?php echo esc_html__('🎲 Roll Dice', 'blocade'); ?></button>
</div>
<div class="center-controls">
<select id="human-players" onchange="updatePlayerSetup()">
<option value="1" <?php echo $atts['players'] == 1 ? 'selected' : ''; ?>><?php echo esc_html__('1 Human + 3 AI', 'blocade'); ?></option>
<option value="2" <?php echo $atts['players'] == 2 ? 'selected' : ''; ?>><?php echo esc_html__('2 Human + 2 AI', 'blocade'); ?></option>
<option value="3" <?php echo $atts['players'] == 3 ? 'selected' : ''; ?>><?php echo esc_html__('3 Human + 1 AI', 'blocade'); ?></option>
<option value="4" <?php echo $atts['players'] == 4 ? 'selected' : ''; ?>><?php echo esc_html__('4 Human Players', 'blocade'); ?></option>
</select>
</div>
<div class="right-controls">
<button id="start-game" onclick="startNewGame()"><?php echo esc_html__('🎮 Start Game', 'blocade'); ?></button>
<button id="reset-game" onclick="resetGame()"><?php echo esc_html__('🔄 New Game', 'blocade'); ?></button>
</div>
</div>
</div>
<div id="blockade-board-container">
<div class="finish-target"></div>
</div>
<div id="game-instructions"></div>
</div>
<script>
const L10n = blocadeL10n || {};
const STOP_IMAGE_URL = '<?php echo esc_js($stop_image_url); ?>';
const TRUCK_IMAGES = {
1: '<?php echo esc_js($fire_truck_url); ?>',
2: '<?php echo esc_js($deliver_truck_url); ?>',
3: '<?php echo esc_js($taxi_url); ?>',
4: '<?php echo esc_js($family_van_url); ?>'
};
let gameState = {
currentPlayer: 1,
humanPlayers: <?php echo intval($atts['players']); ?>,
totalPlayers: 4,
diceRoll: 0,
turnPhase: 'roll',
selectedPiece: null,
boardSpaces: {},
playerPieces: {},
gameWon: false,
barricadeRelocationMode: false,
aiPlayers: [],
aiThinking: false,
gameStarted: false
};
function sprintf(str, ...args) {
return str.replace(/%[sd]/g, function() {
return args.shift();
});
}
function updatePlayerSetup() {
const humanCount = parseInt(document.getElementById('human-players').value);
gameState.humanPlayers = humanCount;
gameState.aiPlayers = [];
for (let i = humanCount + 1; i <= 4; i++) {
gameState.aiPlayers.push(i);
}
gameState.gameStarted = false;
updateStartGameButton();
}
function updateStartGameButton() {
const startButton = document.getElementById('start-game');
if (gameState.gameStarted) {
startButton.style.display = 'none';
} else {
startButton.style.display = 'inline-block';
}
}
function startNewGame() {
updatePlayerSetup();
Object.values(gameState.boardSpaces).forEach(space => {
if (space.isBarricade) {
removeBarricadeStyle(space.element);
space.isBarricade = false;
}
});
resetGame();
gameState.gameStarted = true;
updateStartGameButton();
}
function isCurrentPlayerAI() {
return gameState.aiPlayers.includes(gameState.currentPlayer);
}
function updateAIIndicator() {
const aiIndicator = document.getElementById('ai-indicator');
if (isCurrentPlayerAI()) {
aiIndicator.style.display = 'inline-block';
if (gameState.aiThinking) {
aiIndicator.classList.add('ai-thinking');
} else {
aiIndicator.classList.remove('ai-thinking');
}
} else {
aiIndicator.style.display = 'none';
aiIndicator.classList.remove('ai-thinking');
}
}
const EXACT_BOARD_LAYOUT = {
1: [
{pos: 1, x: 100, y: 620}, {pos: 2, x: 140, y: 620}, {pos: 3, x: 180, y: 620},
{pos: 4, x: 220, y: 620}, {pos: 5, x: 260, y: 620}, {pos: 6, x: 300, y: 620},
{pos: 7, x: 340, y: 620}, {pos: 8, x: 380, y: 620}, {pos: 9, x: 420, y: 620},
{pos: 10, x: 460, y: 620}, {pos: 11, x: 500, y: 620}, {pos: 12, x: 540, y: 620},
{pos: 13, x: 580, y: 620}, {pos: 14, x: 620, y: 620}, {pos: 15, x: 660, y: 620},
{pos: 16, x: 700, y: 620}, {pos: 17, x: 740, y: 620}
],
2: [
{pos: 18, x: 100, y: 580}, {pos: 19, x: 260, y: 580}, {pos: 20, x: 420, y: 580},
{pos: 21, x: 580, y: 580}, {pos: 22, x: 740, y: 580}
],
3: [
{pos: 23, x: 100, y: 540, barricade: true}, {pos: 24, x: 140, y: 540}, {pos: 25, x: 180, y: 540},
{pos: 26, x: 220, y: 540}, {pos: 27, x: 260, y: 540, barricade: true}, {pos: 28, x: 300, y: 540},
{pos: 29, x: 340, y: 540}, {pos: 30, x: 380, y: 540}, {pos: 31, x: 420, y: 540, barricade: true},
{pos: 32, x: 460, y: 540}, {pos: 33, x: 500, y: 540}, {pos: 34, x: 540, y: 540},
{pos: 35, x: 580, y: 540, barricade: true}, {pos: 36, x: 620, y: 540}, {pos: 37, x: 660, y: 540},
{pos: 38, x: 700, y: 540}, {pos: 39, x: 740, y: 540, barricade: true}
],
4: [
{pos: 40, x: 180, y: 500}, {pos: 41, x: 340, y: 500}, {pos: 42, x: 500, y: 500},
{pos: 43, x: 660, y: 500}
],
5: [
{pos: 44, x: 180, y: 460}, {pos: 45, x: 220, y: 460}, {pos: 46, x: 260, y: 460},
{pos: 47, x: 300, y: 460}, {pos: 48, x: 340, y: 460}, {pos: 49, x: 380, y: 460},
{pos: 50, x: 420, y: 460}, {pos: 51, x: 460, y: 460}, {pos: 52, x: 500, y: 460},
{pos: 53, x: 540, y: 460}, {pos: 54, x: 580, y: 460}, {pos: 55, x: 620, y: 460},
{pos: 56, x: 660, y: 460}
],
6: [
{pos: 57, x: 260, y: 420}, {pos: 58, x: 580, y: 420}
],
7: [
{pos: 59, x: 260, y: 380}, {pos: 60, x: 300, y: 380}, {pos: 61, x: 340, y: 380, barricade: true},
{pos: 62, x: 380, y: 380}, {pos: 63, x: 420, y: 380}, {pos: 64, x: 460, y: 380},
{pos: 65, x: 500, y: 380, barricade: true}, {pos: 66, x: 540, y: 380}, {pos: 67, x: 580, y: 380}
],
8: [
{pos: 68, x: 340, y: 340}, {pos: 69, x: 500, y: 340}
],
9: [
{pos: 70, x: 340, y: 300}, {pos: 71, x: 380, y: 300}, {pos: 72, x: 420, y: 300, barricade: true},
{pos: 73, x: 460, y: 300}, {pos: 74, x: 500, y: 300}
],
10: [
{pos: 75, x: 420, y: 260, barricade: true}
],
11: [
{pos: 76, x: 100, y: 220}, {pos: 77, x: 140, y: 220}, {pos: 78, x: 180, y: 220},
{pos: 79, x: 220, y: 220}, {pos: 80, x: 260, y: 220}, {pos: 81, x: 300, y: 220},
{pos: 82, x: 340, y: 220}, {pos: 83, x: 380, y: 220}, {pos: 84, x: 420, y: 220, barricade: true},
{pos: 85, x: 460, y: 220}, {pos: 86, x: 500, y: 220}, {pos: 87, x: 540, y: 220},
{pos: 88, x: 580, y: 220}, {pos: 89, x: 620, y: 220}, {pos: 90, x: 660, y: 220},
{pos: 91, x: 700, y: 220}, {pos: 92, x: 740, y: 220}
],
12: [
{pos: 93, x: 100, y: 180}, {pos: 94, x: 740, y: 180}
],
13: [
{pos: 95, x: 100, y: 140}, {pos: 96, x: 140, y: 140}, {pos: 97, x: 180, y: 140},
{pos: 98, x: 220, y: 140}, {pos: 99, x: 260, y: 140}, {pos: 100, x: 300, y: 140},
{pos: 101, x: 340, y: 140}, {pos: 102, x: 380, y: 140}, {pos: 103, x: 420, y: 140, barricade: true},
{pos: 104, x: 460, y: 140}, {pos: 105, x: 500, y: 140}, {pos: 106, x: 540, y: 140},
{pos: 107, x: 580, y: 140}, {pos: 108, x: 620, y: 140}, {pos: 109, x: 660, y: 140},
{pos: 110, x: 700, y: 140}, {pos: 111, x: 740, y: 140}
]
};
const STARTING_AREAS = {
1: {x: 130, y: 720, color: 'player-1', entry: 3},
2: {x: 290, y: 720, color: 'player-2', entry: 7},
3: {x: 450, y: 720, color: 'player-3', entry: 11},
4: {x: 610, y: 720, color: 'player-4', entry: 15}
};
const BOARD_PATHS = {
'start-1-0': [3], 'start-1-1': [3], 'start-1-2': [3], 'start-1-3': [3], 'start-1-4': [3],
'start-2-0': [7], 'start-2-1': [7], 'start-2-2': [7], 'start-2-3': [7], 'start-2-4': [7],
'start-3-0': [11], 'start-3-1': [11], 'start-3-2': [11], 'start-3-3': [11], 'start-3-4': [11],
'start-4-0': [15], 'start-4-1': [15], 'start-4-2': [15], 'start-4-3': [15], 'start-4-4': [15],
1: [2, 18],
2: [1, 3],
3: [2, 4],
4: [3, 5],
5: [4, 6, 19],
6: [5, 7],
7: [6, 8],
8: [7, 9],
9: [8, 10, 20],
10: [9, 11],
11: [10, 12],
12: [11, 13],
13: [12, 14, 21],
14: [13, 15],
15: [14, 16],
16: [15, 17],
17: [16, 22],
18: [1, 23],
19: [5, 27],
20: [9, 31],
21: [13, 35],
22: [17, 39],
23: [18, 24],
24: [23, 25],
25: [24, 26, 40],
26: [25, 27],
27: [19, 26, 28],
28: [27, 29],
29: [28, 30, 41],
30: [29, 31],
31: [20, 30, 32],
32: [31, 33],
33: [32, 34, 42],
34: [33, 35],
35: [21, 34, 36],
36: [35, 37],
37: [36, 38, 43],
38: [37, 39],
39: [22, 38],
40: [25, 44],
41: [29, 48],
42: [33, 52],
43: [37, 56],
44: [40, 45],
45: [44, 46],
46: [45, 47, 57],
47: [46, 48],
48: [41, 47, 49],
49: [48, 50],
50: [49, 51],
51: [50, 52],
52: [42, 51, 53],
53: [52, 54],
54: [53, 55, 58],
55: [54, 56],
56: [43, 55],
57: [46, 59],
58: [54, 67],
59: [57, 60],
60: [59, 61],
61: [60, 62, 68],
62: [61, 63],
63: [62, 64],
64: [63, 65],
65: [64, 66, 69],
66: [65, 67],
67: [58, 66],
68: [61, 70],
69: [65, 74],
70: [68, 71],
71: [70, 72],
72: [71, 73, 75],
73: [72, 74],
74: [69, 73],
75: [72, 84],
76: [77, 93],
77: [76, 78],
78: [77, 79],
79: [78, 80],
80: [79, 81],
81: [80, 82],
82: [81, 83],
83: [82, 84],
84: [75, 83, 85],
85: [84, 86],
86: [85, 87],
87: [86, 88],
88: [87, 89],
89: [88, 90],
90: [89, 91],
91: [90, 92],
92: [91, 94],
93: [76, 95],
94: [92, 111],
95: [93, 96],
96: [95, 97],
97: [96, 98],
98: [97, 99],
99: [98, 100],
100: [99, 101],
101: [100, 102],
102: [101, 103],
103: [102, 104, 112],
104: [103, 105],
105: [104, 106],
106: [105, 107],
107: [106, 108],
108: [107, 109],
109: [108, 110],
110: [109, 111],
111: [94, 110],
112: []
};
function applyBarricadeStyle(element) {
const existingImg = element.querySelector('.barricade-image');
if (existingImg) {
existingImg.remove();
}
element.classList.add('barricade');
const stopImg = document.createElement('img');
stopImg.src = STOP_IMAGE_URL;
stopImg.className = 'barricade-image';
stopImg.alt = L10n.stopAlt || 'Stop';
element.appendChild(stopImg);
}
function removeBarricadeStyle(element) {
element.classList.remove('barricade');
const stopImgs = element.querySelectorAll('.barricade-image');
stopImgs.forEach(img => img.remove());
element.style.removeProperty('box-shadow');
element.style.removeProperty('border');
element.style.removeProperty('animation');
element.style.removeProperty('background');
element.style.removeProperty('transform');
element.style.removeProperty('z-index');
}
function makeAIMove() {
if (!isCurrentPlayerAI() || gameState.turnPhase !== 'move' || gameState.gameWon) {
return;
}
gameState.aiThinking = true;
updateAIIndicator();
setTimeout(() => {
const aiMove = chooseAIMove();
if (aiMove) {
gameState.selectedPiece = aiMove.piece;
aiMove.piece.element.classList.add('selected');
setTimeout(() => {
attemptMove(aiMove.piece, aiMove.destination);
gameState.aiThinking = false;
updateAIIndicator();
}, 500);
} else {
gameState.aiThinking = false;
updateAIIndicator();
setTimeout(() => {
endTurn();
}, 1000);
}
}, 1500);
}
function chooseAIMove() {
const aiPlayerPieces = gameState.playerPieces[gameState.currentPlayer];
const allMoves = [];
aiPlayerPieces.forEach(piece => {
if (!piece.finished && canPieceMove(piece)) {
let possibleMoves = [];
if (String(piece.position).startsWith('start-')) {
possibleMoves = getPossibleMoves(piece.position, gameState.diceRoll);
} else {
const currentPos = parseInt(piece.position);
possibleMoves = getPossibleMoves(currentPos, gameState.diceRoll);
}
possibleMoves.forEach(destination => {
const space = gameState.boardSpaces[destination];
if (space && (!space.piece || space.piece.player !== piece.player)) {
const moveData = {
piece: piece,
destination: destination,
space: space,
priority: calculateMovePriority(piece, destination, space)
};
allMoves.push(moveData);
}
});
}
});
if (allMoves.length === 0) {
return null;
}
allMoves.sort((a, b) => b.priority - a.priority);
return allMoves[0];
}
function calculateMovePriority(piece, destination, space) {
let priority = 0;
if (destination === 112) {
priority += 10000;
return priority;
}
const opponentEntryPoints = [];
for (let player = 1; player <= 4; player++) {
if (player !== gameState.currentPlayer) {
opponentEntryPoints.push(STARTING_AREAS[player].entry);
}
}
if (space.isBarricade && opponentEntryPoints.includes(destination)) {
priority -= 5000;
return priority;
}
const destPos = parseInt(destination || 0);
const currentPos = String(piece.position).startsWith('start-') ? 0 : parseInt(piece.position || 0);
if (destPos > currentPos) {
const advancement = destPos - currentPos;
priority += Math.min(advancement * 30, 1200);
}
if (destPos >= 95) {
priority += 600;
} else if (destPos >= 76) {
priority += 500;
} else if (destPos >= 44) {
priority += 400;
} else if (destPos >= 23) {
priority += 200;
}
if (destPos >= 1 && destPos <= 17 && !String(piece.position).startsWith('start-')) {
priority -= 300;
}
if (space.isBarricade && !opponentEntryPoints.includes(destination)) {
priority += 400;
}
if (space.piece && space.piece.player !== piece.player) {
if (destPos >= 1 && destPos <= 17) {
priority += 100;
} else {
priority += 400;
}
}
if (String(piece.position).startsWith('start-')) {
priority += 400;
}
const aiPlayerPieces = gameState.playerPieces[gameState.currentPlayer] || [];
const piecesInBottomTwoRows = aiPlayerPieces.filter(p => {
const pos = String(p.position).startsWith('start-') ? 0 : parseInt(p.position || 0);
return pos > 0 && pos <= 39;
}).length;
if (piecesInBottomTwoRows >= 3 && destPos > 39) {
priority += 500;
}
if (destPos >= 40 && currentPos <= 17) {
priority += 300;
}
return priority;
}
function initGame() {
const container = document.getElementById('blockade-board-container');
const existingSpaces = container.querySelectorAll('.path-space, .game-piece');
existingSpaces.forEach(el => el.remove());
gameState.boardSpaces = {};
createBoard();
setupStartingAreas();
initializePieces();
updatePlayerSetup();
updateDisplay();
}
function createBoard() {
const container = document.getElementById('blockade-board-container');
const finishTarget = document.querySelector('.finish-target');
finishTarget.onclick = () => handleSpaceClick(112);
Object.values(EXACT_BOARD_LAYOUT).forEach(row => {
row.forEach(space => {
const spaceElement = document.createElement('div');
spaceElement.className = space.barricade ? 'path-space barricade' : 'path-space';
spaceElement.style.left = space.x + 'px';
spaceElement.style.top = space.y + 'px';
spaceElement.onclick = () => handleSpaceClick(space.pos);
if (space.barricade) {
applyBarricadeStyle(spaceElement);
}
gameState.boardSpaces[space.pos] = {
element: spaceElement,
x: space.x,
y: space.y,
piece: null,
position: space.pos,
isBarricade: space.barricade || false
};
container.appendChild(spaceElement);
});
});
gameState.boardSpaces[112] = {
element: document.querySelector('.finish-target'),
x: 420, y: 85, piece: null, position: 112, isTarget: true
};
createConnectionLines();
}
function createConnectionLines() {
const container = document.getElementById('blockade-board-container');
const existingLines = container.querySelectorAll('.connection-line');
existingLines.forEach(line => line.remove());
const connections = [
{from: {x: 196, y: 720}, to: {x: 196, y: 660}, color: 'player-1'},
{from: {x: 356, y: 720}, to: {x: 356, y: 660}, color: 'player-2'},
{from: {x: 516, y: 720}, to: {x: 516, y: 660}, color: 'player-3'},
{from: {x: 676, y: 720}, to: {x: 676, y: 660}, color: 'player-4'}
];
connections.forEach(conn => {
const line = document.createElement('div');
line.className = `connection-line ${conn.color}`;
line.style.left = conn.from.x + 'px';
line.style.top = conn.to.y + 'px';
line.style.width = '4px';
line.style.height = (conn.from.y - conn.to.y) + 'px';
line.style.background = getConnectionColor(conn.color);
container.appendChild(line);
});
}
function getConnectionColor(playerColor) {
const colors = {
'player-1': 'linear-gradient(to bottom, #DC143C, #DC143C)',
'player-2': 'linear-gradient(to bottom, #228B22, #228B22)',
'player-3': 'linear-gradient(to bottom, #FFD700, #FFD700)',
'player-4': 'linear-gradient(to bottom, #4169E1, #4169E1)'
};
return colors[playerColor] || '#666666';
}
function setupStartingAreas() {
const container = document.getElementById('blockade-board-container');
const existingAreas = container.querySelectorAll('.starting-area');
existingAreas.forEach(area => area.remove());
Object.keys(STARTING_AREAS).forEach(playerNum => {
const area = STARTING_AREAS[playerNum];
const startingArea = document.createElement('div');
startingArea.className = `starting-area ${area.color}`;
startingArea.style.left = area.x + 'px';
startingArea.style.top = area.y + 'px';
const character = document.createElement('div');
character.className = `player-character character-${playerNum}`;
const spacesContainer = document.createElement('div');
spacesContainer.className = 'starting-spaces';
for (let i = 0; i < 5; i++) {
const startSpace = document.createElement('div');
startSpace.className = `start-space ${area.color}`;
startSpace.dataset.player = playerNum;
startSpace.dataset.startIndex = i;
startSpace.onclick = () => handleStartSpaceClick(playerNum, i);
spacesContainer.appendChild(startSpace);
const spaceId = `start-${playerNum}-${i}`;
const startPositions = [
{x: 20, y: 40}, {x: 35, y: 40}, {x: 50, y: 40}, {x: 65, y: 40}, {x: 80, y: 40}
];
gameState.boardSpaces[spaceId] = {
element: startSpace,
x: area.x + startPositions[i].x,
y: area.y + startPositions[i].y,
piece: null, isStart: true, player: parseInt(playerNum), startIndex: i
};
}
startingArea.appendChild(spacesContainer);
container.appendChild(startingArea);
});
}
function initializePieces() {
gameState.playerPieces = {};
for (let player = 1; player <= gameState.totalPlayers; player++) {
gameState.playerPieces[player] = [];
for (let i = 0; i < 5; i++) {
const piece = {
id: `piece-${player}-${i}`, player: player,
position: `start-${player}-${i}`, finished: false, element: null
};
const pieceElement = document.createElement('div');
pieceElement.className = `game-piece piece-${player}`;
const truckImg = document.createElement('img');
truckImg.src = TRUCK_IMAGES[player];
truckImg.alt = sprintf(L10n.truckAlt || 'Player %d Truck %d', player, i + 1);
truckImg.title = sprintf(L10n.truckAlt || 'Player %d Truck %d', player, i + 1);
pieceElement.appendChild(truckImg);
pieceElement.onclick = () => selectPiece(piece);
piece.element = pieceElement;
const startSpace = gameState.boardSpaces[`start-${player}-${i}`];
pieceElement.style.left = (startSpace.x + 8) + 'px';
pieceElement.style.top = (startSpace.y + 8) + 'px';
startSpace.piece = piece;
gameState.playerPieces[player].push(piece);
document.getElementById('blockade-board-container').appendChild(pieceElement);
}
}
}
function rollDice() {
if (gameState.turnPhase !== 'roll' || gameState.gameWon) return;
gameState.diceRoll = Math.floor(Math.random() * 6) + 1;
gameState.turnPhase = 'move';
updateDisplay();
if (isCurrentPlayerAI()) {
setTimeout(() => {
makeAIMove();
}, 1000);
} else {
highlightMovablePieces();
setTimeout(() => {
const hasValidMoves = playerHasValidMoves(gameState.currentPlayer);
if (!hasValidMoves) {
setTimeout(() => {
alert(sprintf(L10n.noValidMoves || 'Player %d has no valid moves with roll %d! Turn ends.', gameState.currentPlayer, gameState.diceRoll));
}, 100);
document.querySelectorAll('.highlight-move').forEach(el => el.classList.remove('highlight-move'));
setTimeout(() => {
endTurn();
}, 1500);
}
}, 300);
}
}
function highlightMovablePieces() {
document.querySelectorAll('.game-piece.highlight-move').forEach(el => {
el.classList.remove('highlight-move');
});
const currentPlayerPieces = gameState.playerPieces[gameState.currentPlayer];
currentPlayerPieces.forEach(piece => {
if (!piece.finished && canPieceMove(piece)) {
piece.element.classList.add('highlight-move');
}
});
}
function getPossibleMoves(startPosition, steps) {
if (steps === 0) return [startPosition];
if (String(startPosition).startsWith('start-')) {
const playerNum = startPosition.split('-')[1];
const entryPosition = STARTING_AREAS[playerNum].entry;
const entrySpace = gameState.boardSpaces[entryPosition];
let possibleDestinations = [];
if (entrySpace.isBarricade) {
if (steps === 1) {
possibleDestinations.push(entryPosition);
}
return possibleDestinations;
}
if (!(entrySpace.piece && entrySpace.piece.player == playerNum)) {
possibleDestinations.push(entryPosition);
}
const remainingSteps = steps - 1;
if (remainingSteps > 0) {
const furtherMoves = getPossibleMoves(entryPosition, remainingSteps);
possibleDestinations = [...possibleDestinations, ...furtherMoves];
}
return [...new Set(possibleDestinations)];
}
let allPaths = [];
function findPaths(currentPos, remainingSteps, currentPath) {
if (remainingSteps === 0) {
allPaths.push(currentPath[currentPath.length - 1]);
return;
}
const connections = BOARD_PATHS[currentPos] || [];
for (let nextPos of connections) {
if (currentPath.length >= 2 && nextPos === currentPath[currentPath.length - 2]) {
continue;
}
const space = gameState.boardSpaces[nextPos];
if (space) {
if (currentPath.includes(nextPos)) continue;
if (remainingSteps === 1) {
if (!space.piece || space.piece.player !== gameState.currentPlayer) {
allPaths.push(nextPos);
}
} else {
if (space.isBarricade) {
continue;
}
findPaths(nextPos, remainingSteps - 1, [...currentPath, nextPos]);
}
}
}
}
findPaths(startPosition, steps, [startPosition]);
const uniquePaths = [...new Set(allPaths)].filter(pos => pos !== startPosition);
return uniquePaths;
}
function canPieceMove(piece) {
if (String(piece.position).startsWith('start-')) {
const possibleMoves = getPossibleMoves(piece.position, gameState.diceRoll);
return possibleMoves.some(dest => {
const space = gameState.boardSpaces[dest];
return !space.piece || space.piece.player !== piece.player;
});
} else {
const currentPos = parseInt(piece.position);
const possibleDestinations = getPossibleMoves(currentPos, gameState.diceRoll);
return possibleDestinations.some(dest => {
const space = gameState.boardSpaces[dest];
return !space.piece || space.piece.player !== piece.player;
});
}
}
function selectPiece(piece) {
if (isCurrentPlayerAI()) {
return;
}
if (gameState.selectedPiece === piece) {
piece.element.classList.remove('selected');
gameState.selectedPiece = null;
clearDestinationHighlight();
return;
}
if (gameState.turnPhase === 'move' && gameState.selectedPiece && gameState.selectedPiece !== piece) {
if (String(piece.position).startsWith('start-')) return;
let targetPosition = parseInt(piece.position);
let validDestinations = [];
if (String(gameState.selectedPiece.position).startsWith('start-')) {
validDestinations = getPossibleMoves(gameState.selectedPiece.position, gameState.diceRoll);
} else {
const currentPos = parseInt(gameState.selectedPiece.position);
validDestinations = getPossibleMoves(currentPos, gameState.diceRoll);
}
if (validDestinations.includes(targetPosition)) {
attemptMove(gameState.selectedPiece, targetPosition);
return;
}
}
if (gameState.turnPhase !== 'move' || piece.player !== gameState.currentPlayer) return;
if (gameState.selectedPiece && gameState.selectedPiece.element) {
gameState.selectedPiece.element.classList.remove('selected');
}
clearDestinationHighlight();
gameState.selectedPiece = piece;
piece.element.classList.add('selected');
highlightValidDestination(piece);
updateDisplay();
}
function highlightValidDestination(piece) {
let validDestinations = [];
if (String(piece.position).startsWith('start-')) {
validDestinations = getPossibleMoves(piece.position, gameState.diceRoll);
} else {
const currentPos = parseInt(piece.position);
validDestinations = getPossibleMoves(currentPos, gameState.diceRoll);
}
validDestinations.forEach(destination => {
const space = gameState.boardSpaces[destination];
if (space) {
space.element.classList.add('highlight-move');
if (space.isBarricade) {
space.element.style.setProperty('box-shadow', '0 0 30px #FF4500, 0 0 60px #FF4500, 0 0 90px #FF4500', 'important');
space.element.style.setProperty('border', '5px solid #FF4500', 'important');
space.element.style.setProperty('background', 'radial-gradient(circle, #FFD700, #FF4500)', 'important');
space.element.style.setProperty('transform', 'scale(1.3)', 'important');
space.element.style.setProperty('z-index', '999', 'important');
space.element.style.setProperty('animation', 'none', 'important');
let flashCount = 0;
const flashInterval = setInterval(() => {
if (flashCount < 6) {
space.element.style.setProperty('background',
flashCount % 2 === 0 ? 'radial-gradient(circle, #FFD700, #FF4500)' : 'radial-gradient(circle, #FF0000, #FFD700)',
'important');
flashCount++;
} else {
clearInterval(flashInterval);
}
}, 200);
}
if (space.piece && space.piece.player !== piece.player && !space.isBarricade) {
space.element.style.boxShadow = '0 0 15px rgba(255,0,0,0.8)';
}
}
});
}
function clearDestinationHighlight() {
document.querySelectorAll('.path-space.highlight-move, .finish-target.highlight-move').forEach(el => {
el.classList.remove('highlight-move');
el.style.removeProperty('box-shadow');
el.style.removeProperty('border');
el.style.removeProperty('animation');
el.style.removeProperty('background');
el.style.removeProperty('transform');
el.style.removeProperty('z-index');
if (!el.classList.contains('barricade')) {
el.style.boxShadow = '';
}
});
document.querySelectorAll('.path-space').forEach(el => {
if (!el.classList.contains('highlight-move') && !el.classList.contains('barricade')) {
el.style.removeProperty('box-shadow');
el.style.removeProperty('border');
el.style.removeProperty('animation');
el.style.removeProperty('background');
el.style.removeProperty('transform');
el.style.removeProperty('z-index');
}
});
}
function highlightBarricadePlacementSpaces() {
clearDestinationHighlight();
Object.keys(gameState.boardSpaces).forEach(position => {
const space = gameState.boardSpaces[position];
if (space.isStart || space.isTarget || space.piece || space.isBarricade) {
return;
}
if (String(position).startsWith('start-') || position == 112) {
return;
}
const pos = parseInt(position);
if (pos >= 1 && pos <= 111) {
space.element.classList.add('barricade-placement-mode');
}
});
}
function clearBarricadePlacementHighlights() {
document.querySelectorAll('.barricade-placement-mode').forEach(el => {
el.classList.remove('barricade-placement-mode');
});
}
function handleSpaceClick(position) {
if (isCurrentPlayerAI() && !gameState.barricadeRelocationMode) {
return;
}
if (gameState.barricadeRelocationMode) {
const space = gameState.boardSpaces[position];
if (space.isStart || space.isTarget || space.piece || space.isBarricade) {
if (isCurrentPlayerAI()) {
const validSpaces = Object.keys(gameState.boardSpaces).filter(pos => {
const s = gameState.boardSpaces[pos];
const p = parseInt(pos);
return !s.isStart && !s.isTarget && !s.piece && !s.isBarricade &&
!String(pos).startsWith('start-') && pos != 112 && p >= 1 && p <= 111;
});
if (validSpaces.length > 0) {
const randomSpace = validSpaces[Math.floor(Math.random() * validSpaces.length)];
setTimeout(() => handleSpaceClick(parseInt(randomSpace)), 500);
}
return;
} else {
alert(L10n.cannotPlaceHere || '❌ Cannot place barricade here! Choose an empty space.');
return;
}
}
if (String(position).startsWith('start-') || position == 112) {
if (isCurrentPlayerAI()) {
return;
} else {
alert(L10n.cannotPlaceStartOrTarget || '❌ Cannot place barricade in starting areas or target!');
return;
}
}
const pos = parseInt(position);
if (pos < 1 || pos > 111) {
if (!isCurrentPlayerAI()) {
alert(L10n.invalidPlacement || '❌ Invalid placement location!');
}
return;
}
applyBarricadeStyle(space.element);
space.isBarricade = true;
clearBarricadePlacementHighlights();
clearDestinationHighlight();
gameState.barricadeRelocationMode = false;
setTimeout(() => {
clearDestinationHighlight();
document.querySelectorAll('.highlight-move').forEach(el => el.classList.remove('highlight-move'));
}, 100);
checkWinCondition();
if (!gameState.gameWon) {
setTimeout(() => { endTurn(); }, 500);
}
return;
}
if (gameState.turnPhase === 'move' && gameState.selectedPiece) {
const targetSpace = gameState.boardSpaces[position];
let validDestinations = [];
if (String(gameState.selectedPiece.position).startsWith('start-')) {
validDestinations = getPossibleMoves(gameState.selectedPiece.position, gameState.diceRoll);
} else {
const currentPos = parseInt(gameState.selectedPiece.position);
validDestinations = getPossibleMoves(currentPos, gameState.diceRoll);
}
if (validDestinations.includes(position)) {
attemptMove(gameState.selectedPiece, position);
}
}
}
function handleStartSpaceClick(player, startIndex) {
if (isCurrentPlayerAI()) {
return;
}
const spaceId = `start-${player}-${startIndex}`;
const space = gameState.boardSpaces[spaceId];
if (space.piece && space.piece.player === gameState.currentPlayer) {
selectPiece(space.piece);
}
}
function playerHasValidMoves(playerNum) {
const playerPieces = gameState.playerPieces[playerNum];
if (!playerPieces) {
return false;
}
for (let piece of playerPieces) {
if (piece.finished) {
continue;
}
let possibleMoves = [];
try {
possibleMoves = getPossibleMoves(piece.position, gameState.diceRoll);
} catch (error) {
continue;
}
for (let dest of possibleMoves) {
const space = gameState.boardSpaces[dest];
if (!space) {
continue;
}
const blocked = space.piece && space.piece.player === piece.player;
if (!blocked) {
return true;
}
}
}
return false;
}
function attemptMove(piece, destinationPosition) {
let validMove = false;
if (String(piece.position).startsWith('start-')) {
const possibleMoves = getPossibleMoves(piece.position, gameState.diceRoll);
if (possibleMoves.includes(destinationPosition)) {
validMove = true;
}
} else {
const currentPos = parseInt(piece.position);
const possibleMoves = getPossibleMoves(currentPos, gameState.diceRoll);
if (possibleMoves.includes(destinationPosition)) {
validMove = true;
}
}
if (validMove) {
movePiece(piece, destinationPosition);
if (gameState.selectedPiece) {
gameState.selectedPiece.element.classList.remove('selected');
gameState.selectedPiece = null;
}
clearDestinationHighlight();
}
}
function movePiece(piece, destinationPosition) {
const currentSpace = gameState.boardSpaces[piece.position];
if (currentSpace) currentSpace.piece = null;
const destSpace = gameState.boardSpaces[destinationPosition];
if (destSpace.piece && destSpace.piece.player !== piece.player) {
returnPieceToStart(destSpace.piece);
}
piece.position = String(destinationPosition);
destSpace.piece = piece;
const pieceX = destSpace.x + 16;
const pieceY = destSpace.y + 16;
if (destSpace.isBarricade) {
piece.element.style.position = 'absolute';
piece.element.style.left = pieceX + 'px';
piece.element.style.top = pieceY + 'px';
piece.element.style.zIndex = '1000';
piece.element.style.display = 'flex';
piece.element.style.visibility = 'visible';
piece.element.style.opacity = '1';
piece.element.style.pointerEvents = 'auto';
removeBarricadeStyle(destSpace.element);
destSpace.isBarricade = false;
setTimeout(() => {
piece.element.style.position = 'absolute';
piece.element.style.left = pieceX + 'px';
piece.element.style.top = pieceY + 'px';
piece.element.style.zIndex = '1000';
piece.element.style.display = 'flex';
piece.element.style.visibility = 'visible';
piece.element.style.opacity = '1';
}, 100);
gameState.barricadeRelocationMode = true;
if (isCurrentPlayerAI()) {
setTimeout(() => {
const opponentEntryPoints = [];
for (let player = 1; player <= 4; player++) {
if (player !== gameState.currentPlayer) {
opponentEntryPoints.push(STARTING_AREAS[player].entry);
}
}
const validSpaces = Object.keys(gameState.boardSpaces).filter(pos => {
const space = gameState.boardSpaces[pos];
const p = parseInt(pos);
return !space.isStart && !space.isTarget && !space.piece && !space.isBarricade &&
!String(pos).startsWith('start-') && pos != 112 && p >= 1 && p <= 111;
});
let chosenSpace = null;
const availableEntryPoints = opponentEntryPoints.filter(entry => validSpaces.includes(String(entry)));
if (availableEntryPoints.length > 0) {
chosenSpace = availableEntryPoints[Math.floor(Math.random() * availableEntryPoints.length)];
}
else {
const strategicSpaces = validSpaces.filter(pos => {
const p = parseInt(pos);
return p >= 40 && p <= 100;
});
if (strategicSpaces.length > 0) {
chosenSpace = strategicSpaces[Math.floor(Math.random() * strategicSpaces.length)];
} else if (validSpaces.length > 0) {
chosenSpace = validSpaces[Math.floor(Math.random() * validSpaces.length)];
}
}
if (chosenSpace) {
handleSpaceClick(parseInt(chosenSpace));
}
}, 1000);
} else {
highlightBarricadePlacementSpaces();
}
} else {
piece.element.style.left = pieceX + 'px';
piece.element.style.top = pieceY + 'px';
piece.element.style.zIndex = '10';
}
if (destinationPosition === 112) {
piece.finished = true;
}
if (!gameState.barricadeRelocationMode) {
checkWinCondition();
if (!gameState.gameWon) {
setTimeout(() => { endTurn(); }, 500);
}
}
}
function returnPieceToStart(piece) {
for (let i = 0; i < 5; i++) {
const startSpaceId = `start-${piece.player}-${i}`;
const startSpace = gameState.boardSpaces[startSpaceId];
if (!startSpace.piece) {
const currentSpace = gameState.boardSpaces[piece.position];
if (currentSpace) currentSpace.piece = null;
piece.position = startSpaceId;
piece.finished = false;
startSpace.piece = piece;
piece.element.style.left = (startSpace.x + 8) + 'px';
piece.element.style.top = (startSpace.y + 8) + 'px';
break;
}
}
}
function checkWinCondition() {
const currentPlayerPieces = gameState.playerPieces[gameState.currentPlayer];
if (currentPlayerPieces.some(piece => piece.finished)) {
gameState.gameWon = true;
setTimeout(() => {
const playerType = isCurrentPlayerAI() ? (L10n.aiText || 'AI') : (L10n.humanText || 'Human');
alert(sprintf(L10n.playerWins || '🎉 Player %d (%s) wins! Reached position 112! 🏆', gameState.currentPlayer, playerType));
}, 500);
}
}
function endTurn() {
const rolledSix = gameState.diceRoll === 6;
if (!rolledSix) {
gameState.currentPlayer = (gameState.currentPlayer % gameState.totalPlayers) + 1;
}
gameState.turnPhase = 'roll';
gameState.barricadeRelocationMode = false;
gameState.aiThinking = false;
if (gameState.selectedPiece) {
gameState.selectedPiece.element.classList.remove('selected');
gameState.selectedPiece = null;
}
gameState.diceRoll = 0;
document.querySelectorAll('.highlight-move').forEach(el => el.classList.remove('highlight-move'));
clearDestinationHighlight();
clearBarricadePlacementHighlights();
updateDisplay();
if (isCurrentPlayerAI() && !gameState.gameWon) {
setTimeout(() => {
rollDice();
}, 1500);
}
}
function resetGame() {
document.querySelectorAll('.game-piece').forEach(el => el.remove());
gameState = {
currentPlayer: 1,
humanPlayers: gameState.humanPlayers,
totalPlayers: 4,
diceRoll: 0,
turnPhase: 'roll',
selectedPiece: null,
boardSpaces: gameState.boardSpaces,
playerPieces: {},
gameWon: false,
barricadeRelocationMode: false,
aiPlayers: gameState.aiPlayers,
aiThinking: false,
gameStarted: false
};
Object.values(gameState.boardSpaces).forEach(space => { space.piece = null; });
clearBarricadePlacementHighlights();
clearDestinationHighlight();
Object.values(gameState.boardSpaces).forEach(space => {
if (space.isBarricade) {
removeBarricadeStyle(space.element);
space.isBarricade = false;
}
});
[23, 27, 31, 35, 39, 61, 65, 72, 75, 84, 103].forEach(pos => {
const space = gameState.boardSpaces[pos];
if (space) {
applyBarricadeStyle(space.element);
space.isBarricade = true;
}
});
document.getElementById('roll-dice').disabled = false;
initializePieces();
updateDisplay();
updateStartGameButton();
if (isCurrentPlayerAI()) {
setTimeout(() => {
rollDice();
}, 2000);
}
}
function updateDisplay() {
const currentPlayerElement = document.getElementById('current-player');
currentPlayerElement.textContent = L10n.playerNames ? L10n.playerNames[gameState.currentPlayer - 1] : `Player ${gameState.currentPlayer}`;
const diceDisplay = gameState.diceRoll ? gameState.diceRoll : '🎲';
document.getElementById('dice-result').textContent = diceDisplay;
const rollButton = document.getElementById('roll-dice');
const isAITurn = isCurrentPlayerAI();
const canRoll = gameState.turnPhase === 'roll' && !gameState.gameWon;
rollButton.disabled = !canRoll || isAITurn;
if (isAITurn && canRoll) {
rollButton.style.opacity = '0.5';
rollButton.title = L10n.aiPlayerTurnTitle || 'AI Player Turn';
} else if (!canRoll) {
rollButton.style.opacity = '0.5';
rollButton.title = L10n.notYourTurnTitle || 'Not your turn to roll';
} else {
rollButton.style.opacity = '1';
rollButton.title = L10n.rollDiceTitle || 'Roll the dice';
}
updateAIIndicator();
}
document.addEventListener('DOMContentLoaded', function() {
initGame();
});
</script>
<?php
return ob_get_clean();
}
public function handle_ajax() {
wp_die();
}
}
new ExactBlockadeGame();
add_action('admin_menu', 'exact_blockade_admin_menu');
function exact_blockade_admin_menu() {
add_options_page(
__('Exact Blockade Game Settings', 'blocade'),
__('Blockade Game', 'blocade'),
'manage_options',
'exact-blockade-settings',
'exact_blockade_settings_page'
);
}
function exact_blockade_settings_page() {
?>
<div class="wrap">
<h1><?php echo esc_html__('🏰 Exact Original Blockade Game with AI', 'blocade'); ?></h1>
<div class="card">
<h2><?php echo esc_html__('Usage', 'blocade'); ?></h2>
<p><?php echo esc_html__('To display the exact original Blockade game on any post or page, use the shortcode:', 'blocade'); ?></p>
<code>[blocade players="1"]</code>
<p><?php echo esc_html__('Players: 1-4 human players (remaining slots filled with AI)', 'blocade'); ?></p>
<p><?php echo esc_html__('The game always plays with 4 total players. If you select fewer than 4 human players, the remaining positions are filled with AI players.', 'blocade'); ?></p>
</div>
<div class="card">
<h2><?php echo esc_html__('AI Player Strategy', 'blocade'); ?></h2>
<ul>
<li><strong><?php echo esc_html__('Priority 1:', 'blocade'); ?></strong> <?php echo esc_html__('Win the game - AI will ALWAYS prioritize reaching position 112 to win', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Priority 2:', 'blocade'); ?></strong> <?php echo esc_html__('Strategic advancement - AI strongly favors forward progress with heavy penalties for first-line fighting', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Priority 3:', 'blocade'); ?></strong> <?php echo esc_html__('Entry point protection - AI will NEVER remove barricades from opponent entry points (3, 7, 11, 15)', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Priority 4:', 'blocade'); ?></strong> <?php echo esc_html__('Strategic barricade placement - AI prioritizes placing barricades on opponent entry points when relocating', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Priority 5:', 'blocade'); ?></strong> <?php echo esc_html__('Smart combat - AI reduces fighting on the first line and focuses on strategic attacks', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Priority 6:', 'blocade'); ?></strong> <?php echo esc_html__('Anti-clustering - AI actively avoids keeping all pieces in the bottom sections', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Automatic Operation:', 'blocade'); ?></strong> <?php echo esc_html__('AI players automatically roll dice, make moves, and handle barricade placement strategically', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Visual Feedback:', 'blocade'); ?></strong> <?php echo esc_html__('AI players are marked with a 🤖 AI indicator', 'blocade'); ?></li>
</ul>
</div>
<div class="card">
<h2><?php echo esc_html__('Game Features', 'blocade'); ?></h2>
<ul>
<li><strong><?php echo esc_html__('Player Selection:', 'blocade'); ?></strong> <?php echo esc_html__('Choose 1-4 human players from the dropdown', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Mixed Gameplay:', 'blocade'); ?></strong> <?php echo esc_html__('Human and AI players can play together', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Smart AI:', 'blocade'); ?></strong> <?php echo esc_html__('AI follows strategic priorities for challenging gameplay', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('112 Numbered Positions:', 'blocade'); ?></strong> <?php echo esc_html__('Exactly matching the original board (1-112)', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Authentic Starting Connections:', 'blocade'); ?></strong> <?php echo esc_html__('Red→3, Green→7, Yellow→11, Blue→15', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Image Barricades:', 'blocade'); ?></strong> <?php echo esc_html__('Stop sign images at positions 23, 27, 31, 35, 39, 61, 65, 72, 75, 84, 103', 'blocade'); ?></li>
<li><strong><?php echo esc_html__('Vehicle Pieces:', 'blocade'); ?></strong> <?php echo esc_html__('Fire truck (red), delivery truck (green), taxi (yellow), family van (blue)', 'blocade'); ?></li>
</ul>
</div>
<div class="card">
<h2><?php echo esc_html__('Image Requirements', 'blocade'); ?></h2>
<p><?php echo esc_html__('Make sure to place your image files in the plugin directory:', 'blocade'); ?></p>
<ul>
<li><code>/wp-content/plugins/blocade/img/stop.png</code> - <?php echo esc_html__('Stop sign for barricades', 'blocade'); ?></li>
<li><code>/wp-content/plugins/blocade/img/fire-truck.png</code> - <?php echo esc_html__('Red player pieces', 'blocade'); ?></li>
<li><code>/wp-content/plugins/blocade/img/deliver-truck.png</code> - <?php echo esc_html__('Green player pieces', 'blocade'); ?></li>
<li><code>/wp-content/plugins/blocade/img/taxi.png</code> - <?php echo esc_html__('Yellow player pieces', 'blocade'); ?></li>
<li><code>/wp-content/plugins/blocade/img/family-van.png</code> - <?php echo esc_html__('Blue player pieces', 'blocade'); ?></li>
</ul>
</div>
<div class="card">
<h2><?php echo esc_html__('Language Support', 'blocade'); ?></h2>
<p><?php echo esc_html__('This plugin supports multiple languages. Translation files should be placed in:', 'blocade'); ?></p>
<code>/wp-content/plugins/blocade/languages/</code>
<p><?php echo esc_html__('Supported formats: .po/.mo files with the text domain "blocade"', 'blocade'); ?></p>
</div>
</div>
<?php
}
?>