Game Specification: Hex Sokoban

1. Core Concept
A puzzle game on a hexagonal grid where a player pushes and merges "Crate Pieces" to form a single, complete hexagonal Crate.

2. Game Components

Grid: A fixed 2D hexagonal grid using axial coordinates (q, r).

Directions: Six directions indexed 0-5, corresponding to changes in (q, r):

0 (E): {q: +1, r: 0}

1 (NE): {q: +1, r: -1}

2 (NW): {q: 0, r: -1}

3 (W): {q: -1, r: 0}

4 (SW): {q: -1, r: +1}

5 (SE): {q: 0, r: +1}

Player: Occupies a single (q, r) tile. Can co-exist on a tile with a Crate Piece.

Crate Piece: An object located on a grid tile. A Crate Piece is defined by a set of "Edges" it possesses. Each Edge corresponds to one of the six direction indices (0-5). A tile can either be empty or contain one Crate Piece.

3. Objective
To merge Crate Pieces until one single Crate Piece on the grid possesses all six Edges, {0, 1, 2, 3, 4, 5}.

4. Player Actions & Movement Logic
All actions stem from a player's attempt to move in one of the six directions. Let the player's current tile be playerTile and the target destination tile be destTile.

The game logic must follow this precise order of operations:

Step 1: Determine if the action is a "Push Attempt".
A Push Attempt occurs if, and only if, one of these two conditions is met:

Inside Push: The playerTile contains a Crate Piece, and that Crate has an Edge matching the direction of movement.

Outside Push: The destTile contains a Crate Piece, and that Crate has an Edge matching the opposite direction of movement ((moveDirection + 3) % 6).

Step 2: Resolve the Action.

If the action IS a Push Attempt:

Check for Validity: A push is valid only if the entire chain of Crate Pieces in the push direction can move. A Crate can push the next one in the chain only if their contacting Edges both exist. A push is blocked if it leads off the grid or if any merge in the chain is invalid (i.e., attempting to merge two pieces that have conflicting Edges).

Execute:

If the push is valid, move the entire Crate chain by one tile. The player then moves to destTile.

If the push is invalid, the move is blocked. Nothing moves. The player remains on playerTile.

If the action IS NOT a Push Attempt:

This is a Simple Move. It covers two cases:

Moving to an empty destTile.

Moving to a destTile that contains a Crate Piece, but not in a way that qualifies as an "Outside Push" (i.e., walking into a "solid" side of a Crate).

Execute: The player moves to destTile.


create this game as a simple one file html.
here is the basic framework for the hexagons:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Hex Grid Path Drawer</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 0;
            background-color: #f0f2f5;
        }
        h1 {
            color: #333;
        }
        p {
            color: #555;
            margin-bottom: 20px;
        }
        #grid-container {
            width: 90vw;
            max-width: 900px;
            height: 80vh;
            max-height: 700px;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            background-color: #fff;
        }
        svg {
            width: 100%;
            height: 100%;
        }

        /* --- MODIFIED CSS FOR HIGH CONTRAST --- */

        /* Default state: a thin outline */
        .hex-edge {
            fill: transparent;      /* Make the inside of the shape hollow */
            stroke: #bbb;           /* Use a light gray stroke for the thin line effect */
            stroke-width: 1px;
            transition: all 0.15s ease-in-out; /* Animate all property changes */
            pointer-events: none;   /* Clicks are handled by the hitbox */
        }

        /* Active state: a thick, filled shape */
        .hex-edge.active {
            fill: #e74c3c;          /* Fill the shape with solid red */
            stroke: #c0392b;        /* Give it a darker red border */
        }
        
        /* Hover state: also a thick, filled shape */
        .hex-edge.hover-preview {
            fill: #3498db;          /* Fill the shape with solid blue */
            stroke: #2980b9;        /* Give it a darker blue border */
        }
        
        .hitbox-polygon {
            fill: transparent;
            stroke: none;
            cursor: pointer;
        }
    </style>
</head>
<body>

    <h1>Hex Grid Path Drawer</h1>
    <p>Unhighlighted edges are thin. Hover or click to make them thick and bright.</p>

    <div id="grid-container">
        <svg id="hex-grid-svg"></svg>
    </div>

    <script>
        // The JavaScript is identical to the previous version.
        // All changes were made in the CSS.
        document.addEventListener('DOMContentLoaded', function() {
            const GRID_COLS = 12;
            const GRID_ROWS = 8;
            const HEX_RADIUS = 40;
            const EDGE_INSET = 2;
            const TAPER_AMOUNT = 6;

            const svg = document.getElementById('hex-grid-svg');
            const svgEdgesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
            const svgHitboxGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
            svg.appendChild(svgEdgesGroup);
            svg.appendChild(svgHitboxGroup);

            const HEX_WIDTH = Math.sqrt(3) * HEX_RADIUS;
            const HEX_HEIGHT = 2 * HEX_RADIUS;

            function getHexVertices(centerX, centerY, radius) {
                const vertices = [];
                for (let i = 0; i < 6; i++) {
                    const angle = (Math.PI / 180) * (60 * i + 30);
                    const x = centerX + radius * Math.cos(angle);
                    const y = centerY + radius * Math.sin(angle);
                    vertices.push({ x, y });
                }
                return vertices;
            }
            
            function handlePolygonClick(event) {
                const edgeId = event.currentTarget.dataset.edgeId;
                document.getElementById(edgeId)?.classList.toggle('active');
            }

            function handlePolygonMouseOver(event) {
                const edgeId = event.currentTarget.dataset.edgeId;
                document.getElementById(edgeId)?.classList.add('hover-preview');
            }

            function handlePolygonMouseOut(event) {
                const edgeId = event.currentTarget.dataset.edgeId;
                document.getElementById(edgeId)?.classList.remove('hover-preview');
            }

            for (let row = 0; row < GRID_ROWS; row++) {
                for (let col = 0; col < GRID_COLS; col++) {
                    const xOffset = (row % 2 === 1) ? HEX_WIDTH / 2 : 0;
                    const centerX = (col * HEX_WIDTH) + xOffset;
                    const centerY = row * HEX_HEIGHT * 0.75;

                    const outerRadius = HEX_RADIUS - EDGE_INSET;
                    const innerRadius = HEX_RADIUS - EDGE_INSET - TAPER_AMOUNT;
                    
                    const outerVertices = getHexVertices(centerX, centerY, outerRadius);
                    const innerVertices = getHexVertices(centerX, centerY, innerRadius);
                    const hitboxVertices = getHexVertices(centerX, centerY, HEX_RADIUS);

                    for (let i = 0; i < 6; i++) {
                        const edgeId = `edge-${row}-${col}-${i}`;
                        
                        const op1 = outerVertices[i];
                        const op2 = outerVertices[(i + 1) % 6];
                        const ip1 = innerVertices[i];
                        const ip2 = innerVertices[(i + 1) % 6];

                        const edge = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                        edge.setAttribute('points', `${op1.x},${op1.y} ${op2.x},${op2.y} ${ip2.x},${ip2.y} ${ip1.x},${ip1.y}`);
                        edge.setAttribute('id', edgeId);
                        edge.setAttribute('class', 'hex-edge');
                        svgEdgesGroup.appendChild(edge);

                        const hp1 = hitboxVertices[i];
                        const hp2 = hitboxVertices[(i + 1) % 6];
                        const hitbox = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                        hitbox.setAttribute('points', `${centerX},${centerY} ${hp1.x},${hp1.y} ${hp2.x},${hp2.y}`);
                        hitbox.dataset.edgeId = edgeId;
                        hitbox.setAttribute('class', 'hitbox-polygon');
                        
                        hitbox.addEventListener('click', handlePolygonClick);
                        hitbox.addEventListener('mouseover', handlePolygonMouseOver);
                        hitbox.addEventListener('mouseout', handlePolygonMouseOut);
                        svgHitboxGroup.appendChild(hitbox);
                    }
                }
            }
            
            const svgWidth = (GRID_COLS * HEX_WIDTH) + (HEX_WIDTH / 2);
            const svgHeight = ((GRID_ROWS - 1) * HEX_HEIGHT * 0.75) + HEX_HEIGHT;
            const padding = HEX_RADIUS;
            svg.setAttribute('viewBox', `${-padding} ${-padding} ${svgWidth + padding * 2} ${svgHeight + padding * 2}`);
        });
    </script>

</body>
</html>