diff --git a/public/data/rushhour.json b/public/data/rushhour.json new file mode 100644 index 0000000..d10c612 --- /dev/null +++ b/public/data/rushhour.json @@ -0,0 +1,3685 @@ +{ + "generatedAt": "2026-06-08T23:20:08.270Z", + "seed": 2654435769, + "count": 45, + "puzzles": [ + { + "level": 1, + "minMoves": 2, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 5, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 1, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 2, + "minMoves": 2, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 3, + "minMoves": 2, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 5, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 4, + "minMoves": 2, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 5, + "minMoves": 3, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 5, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 6, + "minMoves": 3, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "J", + "x": 5, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 7, + "minMoves": 4, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 8, + "minMoves": 4, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 9, + "minMoves": 5, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 10, + "minMoves": 5, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 2, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 3, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 11, + "minMoves": 5, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 2, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 12, + "minMoves": 5, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 2, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 3, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 1, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 13, + "minMoves": 5, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 14, + "minMoves": 6, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 5, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 15, + "minMoves": 6, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 16, + "minMoves": 7, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 4, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 17, + "minMoves": 7, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 18, + "minMoves": 7, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 3, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 1, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "J", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 19, + "minMoves": 8, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 20, + "minMoves": 8, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 21, + "minMoves": 8, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 1, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 22, + "minMoves": 8, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 0, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 23, + "minMoves": 8, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 5, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 24, + "minMoves": 9, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 25, + "minMoves": 9, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 4, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 0, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 0, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 26, + "minMoves": 9, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 5, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 27, + "minMoves": 10, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 28, + "minMoves": 11, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 29, + "minMoves": 12, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 30, + "minMoves": 13, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 2, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 1, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 31, + "minMoves": 14, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 32, + "minMoves": 14, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 3, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 33, + "minMoves": 14, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 3, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 2, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 34, + "minMoves": 15, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 1, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "J", + "x": 1, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 35, + "minMoves": 15, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 2, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 36, + "minMoves": 15, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 1, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 37, + "minMoves": 16, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 5, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 4, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 38, + "minMoves": 17, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 5, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 0, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 2, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 0, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 39, + "minMoves": 17, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 5, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 40, + "minMoves": 17, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 0, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 5, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 2, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 4, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 0, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 41, + "minMoves": 17, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 1, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 4, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 3, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 42, + "minMoves": 17, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 3, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 1, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 4, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 43, + "minMoves": 18, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 2, + "y": 4, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "C", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 3, + "y": 5, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 5, + "y": 2, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 1, + "y": 1, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "G", + "x": 3, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 0, + "y": 3, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 5, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "J", + "x": 0, + "y": 0, + "len": 3, + "orient": "h", + "isTarget": false + } + ] + }, + { + "level": 44, + "minMoves": 21, + "vehicles": [ + { + "id": "X", + "x": 1, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 0, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 3, + "y": 1, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "D", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "E", + "x": 3, + "y": 3, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "F", + "x": 2, + "y": 3, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 0, + "y": 0, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "H", + "x": 2, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "I", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "J", + "x": 1, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + }, + { + "level": 45, + "minMoves": 25, + "vehicles": [ + { + "id": "X", + "x": 0, + "y": 2, + "len": 2, + "orient": "h", + "isTarget": true + }, + { + "id": "A", + "x": 4, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "B", + "x": 3, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "C", + "x": 0, + "y": 5, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "D", + "x": 1, + "y": 1, + "len": 3, + "orient": "h", + "isTarget": false + }, + { + "id": "E", + "x": 0, + "y": 0, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "F", + "x": 3, + "y": 4, + "len": 2, + "orient": "v", + "isTarget": false + }, + { + "id": "G", + "x": 5, + "y": 0, + "len": 3, + "orient": "v", + "isTarget": false + }, + { + "id": "H", + "x": 4, + "y": 4, + "len": 2, + "orient": "h", + "isTarget": false + }, + { + "id": "I", + "x": 2, + "y": 2, + "len": 2, + "orient": "v", + "isTarget": false + } + ] + } + ] +} \ No newline at end of file diff --git a/public/src/games/rushhour/RushHourGame.js b/public/src/games/rushhour/RushHourGame.js new file mode 100644 index 0000000..38e563c --- /dev/null +++ b/public/src/games/rushhour/RushHourGame.js @@ -0,0 +1,475 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; +import { Button } from '../../ui/Button.js'; +import { MusicPlayer } from '../../ui/MusicPlayer.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; +import { api } from '../../services/api.js'; +import { + GRID, EXIT_ROW, cloneVehicles, slideRange, isSolved, solve, +} from './RushHourLogic.js'; + +const CELL = 132; +const BOARD_PX = CELL * GRID; +const BOARD_LEFT = Math.round((GAME_WIDTH - BOARD_PX) / 2); +const BOARD_TOP = 188; +const PAD = 10; // inset of a vehicle body within its cells + +const FELT = 0x0e2233; +const FRAME = 0x0a1722; +const CELLBG = 0x14304a; +const GRIDLN = 0x21466a; +const TARGET_COLOR = 0xe03131; +const CAR_COLORS = [ + 0x4a90d9, 0x2ecc71, 0xf1c40f, 0x9b59b6, 0xe67e22, 0x1abc9c, + 0x3498db, 0xe84393, 0x16a085, 0xd35400, 0x27ae60, 0x8e44ad, + 0x7f8c8d, 0x2980b9, 0xf39c12, 0x6ab04c, +]; + +const D = { felt: -2, frame: -1, grid: 0, exit: 1, vehicle: 10, ui: 30, banner: 34, overlay: 60, overlayUI: 62 }; + +export default class RushHourGame extends Phaser.Scene { + constructor() { super('RushHourGame'); } + + init(data) { + this.gameDef = data.game ?? { slug: 'rushhour', name: 'Rush Hour' }; + this.bank = []; + this.levelsCompleted = 0; // highest contiguous level cleared + this.canPersist = true; + this.view = 'select'; + + // per-level play state + this.level = 0; + this.startVehicles = null; + this.vehicles = null; + this.sprites = new Map(); // id -> Container + this.undoStack = []; + this.moves = 0; + this.overlayUp = false; + this.busy = false; + } + + async create() { + try { + const music = this.cache.json.get('music'); + if (music?.tracks) new MusicPlayer(this, music.tracks); + } catch (_) { /* optional */ } + + this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(D.felt); + + const raw = this.cache.json.get('rushhour'); + this.bank = (raw?.puzzles ?? []).slice().sort((a, b) => a.level - b.level); + + try { + const res = await api.get('/puzzles/rushhour/progress'); + this.levelsCompleted = res?.levelsCompleted ?? 0; + } catch (_) { + // Not signed in or offline — progress won't persist; play in-session. + this.canPersist = false; + this.levelsCompleted = 0; + } + + this.layer = this.add.container(0, 0); + this.showLevelSelect(); + } + + clearLayer() { + this.sprites.clear(); + this.layer.removeAll(true); + // These were just destroyed; drop the stale references so updateMoves() + // doesn't poke a dead Button (whose scene is now undefined) before drawHud + // rebuilds them for the next level. + this.undoBtn = null; + this.movesText = null; + } + + // ── Level select ──────────────────────────────────────────────────────────── + + showLevelSelect() { + this.view = 'select'; + this.overlayUp = false; + this.busy = false; + this.clearLayer(); + const cx = GAME_WIDTH / 2; + + const title = this.add.text(cx, 84, 'RUSH HOUR', { + fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex, + }).setOrigin(0.5); + const sub = this.add.text(cx, 138, 'Slide the cars aside and drive the red car out. Clear each level to unlock the next.', { + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add([title, sub]); + + if (!this.bank.length) { + const msg = this.add.text(cx, 520, 'No puzzles found.\nRun: node server/scripts/genRushHour.js', { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center', + }).setOrigin(0.5); + this.layer.add(msg); + const back = new Button(this, cx, GAME_HEIGHT - 90, 'Back', () => this.scene.start('GameMenu'), { variant: 'ghost' }); + this.layer.add(back); + return; + } + + const nextLevel = Math.min(this.levelsCompleted + 1, this.bank.length); + const prog = this.add.text(cx, 182, `Completed ${this.levelsCompleted} / ${this.bank.length}`, { + fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex, + }).setOrigin(0.5); + this.layer.add(prog); + + const COLS = 12; + const SIZE = 104; + const GAP = 14; + const gridW = COLS * SIZE + (COLS - 1) * GAP; + const left = cx - gridW / 2 + SIZE / 2; + const top = 268; + + this.bank.forEach((p, i) => { + const col = i % COLS; + const row = Math.floor(i / COLS); + const x = left + col * (SIZE + GAP); + const y = top + row * (SIZE + GAP); + const level = p.level; + const cleared = level <= this.levelsCompleted; + const playable = level <= nextLevel; + + const fill = cleared ? 0x1f5c3a : playable ? 0x1e3a52 : 0x16202b; + const stroke = cleared ? 0x2ecc71 : playable ? COLORS.gold : 0x2a3744; + const tile = this.add.rectangle(x, y, SIZE, SIZE, fill).setStrokeStyle(playable || cleared ? 3 : 2, stroke, 1); + const num = this.add.text(x, y - 8, String(level), { + fontFamily: 'Righteous', fontSize: '34px', + color: playable || cleared ? COLORS.textHex : '#54606b', + }).setOrigin(0.5); + const tag = this.add.text(x, y + 28, cleared ? '✓ cleared' : playable ? `par ${p.minMoves}` : 'locked', { + fontFamily: '"Julius Sans One"', fontSize: '14px', + color: cleared ? '#9be7b4' : playable ? COLORS.mutedHex : '#54606b', + }).setOrigin(0.5); + this.layer.add([tile, num, tag]); + + if (playable) { + tile.setInteractive({ useHandCursor: true }); + tile.on('pointerover', () => tile.setStrokeStyle(4, COLORS.gold, 1)); + tile.on('pointerout', () => tile.setStrokeStyle(cleared ? 3 : 3, stroke, 1)); + tile.on('pointerup', () => this.playLevel(level)); + } + }); + + const resume = new Button(this, cx - 150, GAME_HEIGHT - 78, `Play Level ${nextLevel}`, () => this.playLevel(nextLevel), + { width: 280, height: 58, fontSize: 24 }); + const back = new Button(this, cx + 170, GAME_HEIGHT - 78, 'Back', () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 180, height: 58, fontSize: 24 }); + this.layer.add([resume, back]); + + if (!this.canPersist) { + const note = this.add.text(cx, GAME_HEIGHT - 28, 'Sign in to save your progress across devices.', { + fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add(note); + } + } + + // ── Play a level ──────────────────────────────────────────────────────────── + + playLevel(level) { + const puzzle = this.bank.find((p) => p.level === level); + if (!puzzle) return; + this.view = 'play'; + this.level = level; + this.startVehicles = cloneVehicles(puzzle.vehicles); + this.vehicles = cloneVehicles(puzzle.vehicles); + this.par = puzzle.minMoves; + this.undoStack = []; + this.moves = 0; + this.overlayUp = false; + this.busy = false; + + this.clearLayer(); + this.drawBoardChrome(); + this.drawHud(); + this.renderVehicles(); + } + + drawBoardChrome() { + const g = this.add.graphics().setDepth(D.frame); + // Outer frame + g.fillStyle(FRAME, 1); + g.fillRoundedRect(BOARD_LEFT - 22, BOARD_TOP - 22, BOARD_PX + 44, BOARD_PX + 44, 18); + // Playfield + g.fillStyle(CELLBG, 1); + g.fillRect(BOARD_LEFT, BOARD_TOP, BOARD_PX, BOARD_PX); + this.layer.add(g); + + const grid = this.add.graphics().setDepth(D.grid); + grid.lineStyle(2, GRIDLN, 0.9); + for (let i = 0; i <= GRID; i++) { + grid.lineBetween(BOARD_LEFT + i * CELL, BOARD_TOP, BOARD_LEFT + i * CELL, BOARD_TOP + BOARD_PX); + grid.lineBetween(BOARD_LEFT, BOARD_TOP + i * CELL, BOARD_LEFT + BOARD_PX, BOARD_TOP + i * CELL); + } + this.layer.add(grid); + + // Exit gap on the right wall at EXIT_ROW + const exitY = BOARD_TOP + (EXIT_ROW + 0.5) * CELL; + const ex = this.add.graphics().setDepth(D.exit); + ex.fillStyle(FELT, 1); + ex.fillRect(BOARD_LEFT + BOARD_PX - 1, BOARD_TOP + EXIT_ROW * CELL + 6, 46, CELL - 12); + ex.fillStyle(COLORS.gold, 0.9); + ex.fillTriangle( + BOARD_LEFT + BOARD_PX + 12, exitY - 26, + BOARD_LEFT + BOARD_PX + 12, exitY + 26, + BOARD_LEFT + BOARD_PX + 44, exitY, + ); + this.layer.add(ex); + const exitLabel = this.add.text(BOARD_LEFT + BOARD_PX + 30, exitY + 50, 'EXIT', { + fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex, + }).setOrigin(0.5); + this.layer.add(exitLabel); + } + + drawHud() { + const title = this.add.text(BOARD_LEFT - 22, 92, `Level ${this.level}`, { + fontFamily: 'Righteous', fontSize: '52px', color: COLORS.goldHex, + }).setOrigin(0, 0.5).setDepth(D.ui); + this.layer.add(title); + + this.movesText = this.add.text(BOARD_LEFT + BOARD_PX + 22, 92, '', { + fontFamily: 'Righteous', fontSize: '30px', color: COLORS.textHex, + }).setOrigin(1, 0.5).setDepth(D.ui); + this.layer.add(this.movesText); + this.updateMoves(); + + const by = GAME_HEIGHT - 70; + const undo = new Button(this, BOARD_LEFT + 90, by, 'Undo', () => this.undo(), + { width: 150, height: 56, fontSize: 22 }); + const reset = new Button(this, BOARD_LEFT + 256, by, 'Reset', () => this.resetLevel(), + { width: 150, height: 56, fontSize: 22, variant: 'ghost' }); + const hint = new Button(this, BOARD_LEFT + 422, by, 'Hint', () => this.showHint(), + { width: 150, height: 56, fontSize: 22, variant: 'ghost' }); + const levels = new Button(this, BOARD_LEFT + BOARD_PX - 110, by, 'Levels', () => this.showLevelSelect(), + { width: 200, height: 56, fontSize: 22, variant: 'ghost' }); + this.undoBtn = undo; + this.layer.add([undo, reset, hint, levels]); + this.updateMoves(); + } + + updateMoves() { + if (this.movesText) this.movesText.setText(`Moves: ${this.moves} Par: ${this.par}`); + if (this.undoBtn) this.undoBtn.setEnabled(this.undoStack.length > 0); + } + + // ── Vehicle rendering + dragging ────────────────────────────────────────────── + + centerFor(v) { + const wCells = v.orient === 'h' ? v.len : 1; + const hCells = v.orient === 'v' ? v.len : 1; + return { + x: BOARD_LEFT + (v.x + wCells / 2) * CELL, + y: BOARD_TOP + (v.y + hCells / 2) * CELL, + }; + } + + renderVehicles() { + this.sprites.forEach((c) => c.destroy()); + this.sprites.clear(); + + let carIdx = 0; + for (const v of this.vehicles) { + const wCells = v.orient === 'h' ? v.len : 1; + const hCells = v.orient === 'v' ? v.len : 1; + const w = wCells * CELL - PAD * 2; + const h = hCells * CELL - PAD * 2; + const color = v.isTarget ? TARGET_COLOR : CAR_COLORS[carIdx++ % CAR_COLORS.length]; + + const { x, y } = this.centerFor(v); + const c = this.add.container(x, y).setDepth(D.vehicle); + + const body = this.add.graphics(); + body.fillStyle(color, 1); + body.fillRoundedRect(-w / 2, -h / 2, w, h, 16); + body.fillStyle(0xffffff, 0.14); + body.fillRoundedRect(-w / 2 + 8, -h / 2 + 8, w - 16, Math.max(10, h * 0.22), 10); + body.lineStyle(3, 0x000000, 0.28); + body.strokeRoundedRect(-w / 2, -h / 2, w, h, 16); + c.add(body); + + if (v.isTarget) { + c.add(this.add.text(0, 0, '★', { fontFamily: 'serif', fontSize: '40px', color: '#ffffff' }).setOrigin(0.5).setAlpha(0.9)); + } + + // The body graphics are centered on the container origin, but setSize() + // makes Phaser offset the input hit test by the display origin (w/2, h/2). + // So the hit area must be specified top-left at (0,0) — NOT (-w/2,-h/2) — + // to line up with the visible car (mirrors Battleship's draggable ships). + c.setSize(w, h); + c.setInteractive(new Phaser.Geom.Rectangle(0, 0, w, h), Phaser.Geom.Rectangle.Contains); + c.input.cursor = 'grab'; + this.input.setDraggable(c); + + c._vid = v.id; + this.attachDrag(c, v); + this.layer.add(c); + this.sprites.set(v.id, c); + } + } + + attachDrag(c, _v) { + c.on('dragstart', () => { + if (this.busy || this.overlayUp) { c._noDrag = true; return; } + c._noDrag = false; + const v = this.vehicles.find((q) => q.id === c._vid); + const r = slideRange(this.vehicles, this.vehicles.indexOf(v)); + c._drag = { v, range: r, home: this.centerFor(v) }; + c.setDepth(D.vehicle + 1); + playSound(this, SFX.PIECE_CLICK); + }); + + c.on('drag', (_pointer, dragX, dragY) => { + if (c._noDrag || !c._drag) return; + const { v, range, home } = c._drag; + if (v.orient === 'h') { + const minX = BOARD_LEFT + (range.min + v.len / 2) * CELL; + const maxX = BOARD_LEFT + (range.max + v.len / 2) * CELL; + c.x = Phaser.Math.Clamp(dragX, minX, maxX); + c.y = home.y; + } else { + const minY = BOARD_TOP + (range.min + v.len / 2) * CELL; + const maxY = BOARD_TOP + (range.max + v.len / 2) * CELL; + c.y = Phaser.Math.Clamp(dragY, minY, maxY); + c.x = home.x; + } + }); + + c.on('dragend', () => { + c.setDepth(D.vehicle); + if (c._noDrag || !c._drag) return; + const { v, range } = c._drag; + c._drag = null; + let target; + if (v.orient === 'h') { + target = Math.round((c.x - BOARD_LEFT) / CELL - v.len / 2); + } else { + target = Math.round((c.y - BOARD_TOP) / CELL - v.len / 2); + } + target = Phaser.Math.Clamp(target, range.min, range.max); + const before = v.orient === 'h' ? v.x : v.y; + if (target === before) { + this.snapSprite(c, v); + return; + } + this.commitMove(v, target); + }); + } + + snapSprite(c, v) { + const { x, y } = this.centerFor(v); + this.tweens.add({ targets: c, x, y, duration: 90, ease: 'Quad.easeOut' }); + } + + commitMove(v, target) { + this.undoStack.push({ id: v.id, x: v.x, y: v.y }); + if (v.orient === 'h') v.x = target; else v.y = target; + this.moves++; + this.updateMoves(); + const c = this.sprites.get(v.id); + this.snapSprite(c, v); + playSound(this, SFX.CARD_PLACE); + + if (isSolved(this.vehicles)) this.driveOut(v); + } + + undo() { + if (!this.undoStack.length || this.busy || this.overlayUp) return; + const last = this.undoStack.pop(); + const v = this.vehicles.find((q) => q.id === last.id); + v.x = last.x; v.y = last.y; + this.moves++; // undo counts as a move taken + this.updateMoves(); + this.snapSprite(this.sprites.get(v.id), v); + playSound(this, SFX.PIECE_CLICK); + } + + resetLevel() { + if (this.busy || this.overlayUp) return; + this.vehicles = cloneVehicles(this.startVehicles); + this.undoStack = []; + this.moves = 0; + this.updateMoves(); + this.renderVehicles(); + playSound(this, SFX.CARD_SHUFFLE); + } + + showHint() { + if (this.busy || this.overlayUp) return; + const { path } = solve(this.vehicles); + if (!path || !path.length) return; + const mv = path[0]; + const c = this.sprites.get(mv.id); + if (!c) return; + this.tweens.add({ targets: c, scale: 1.08, duration: 220, yoyo: true, repeat: 2, ease: 'Sine.easeInOut' }); + } + + // ── Solve flow ──────────────────────────────────────────────────────────────── + + driveOut(target) { + this.busy = true; + const c = this.sprites.get(target.id); + playSound(this, SFX.VICTORY_SHORT); + this.tweens.add({ + targets: c, + x: GAME_WIDTH + 200, + duration: 620, + ease: 'Back.easeIn', + onComplete: () => this.onSolved(), + }); + } + + onSolved() { + this.overlayUp = true; + // Optimistic local progression so play continues even if the request fails. + if (this.level > this.levelsCompleted) this.levelsCompleted = this.level; + + // Persist completion + record the match (best effort). + api.post('/puzzles/rushhour/complete', { level: this.level }) + .then((res) => { if (res?.levelsCompleted != null) this.levelsCompleted = Math.max(this.levelsCompleted, res.levelsCompleted); }) + .catch(() => { /* best effort */ }); + api.post('/history/single-player', { + slug: 'rushhour', score: this.moves, opponentScores: [], result: 'win', + }).catch(() => { /* best effort */ }); + + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setDepth(D.overlay).setInteractive(); + this.layer.add(dim); + + const panel = this.add.graphics().setDepth(D.overlay); + panel.fillStyle(COLORS.panel, 0.98); + panel.fillRoundedRect(cx - 320, cy - 200, 640, 400, 20); + panel.lineStyle(3, COLORS.accent, 1); + panel.strokeRoundedRect(cx - 320, cy - 200, 640, 400, 20); + this.layer.add(panel); + + const beatPar = this.moves <= this.par; + const title = this.add.text(cx, cy - 130, 'Solved!', { + fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + const stat = this.add.text(cx, cy - 50, + `Level ${this.level} cleared in ${this.moves} moves\nPar: ${this.par}${beatPar ? ' ★ par or better!' : ''}`, { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.textHex, align: 'center', lineSpacing: 8, + }).setOrigin(0.5).setDepth(D.overlayUI); + this.layer.add([title, stat]); + + const hasNext = this.level < this.bank.length; + const btns = []; + if (hasNext) { + btns.push(new Button(this, cx, cy + 60, `Next Level (${this.level + 1})`, () => this.playLevel(this.level + 1), + { width: 340, height: 60, fontSize: 26 }).setDepth(D.overlayUI)); + } else { + btns.push(this.add.text(cx, cy + 50, 'You cleared every puzzle. Bravo!', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI)); + } + const replay = new Button(this, cx - 110, cy + 140, 'Replay', () => this.playLevel(this.level), + { width: 200, height: 54, fontSize: 22, variant: 'ghost' }).setDepth(D.overlayUI); + const levels = new Button(this, cx + 120, cy + 140, 'Levels', () => this.showLevelSelect(), + { width: 200, height: 54, fontSize: 22, variant: 'ghost' }).setDepth(D.overlayUI); + btns.push(replay, levels); + this.layer.add(btns); + } +} diff --git a/public/src/games/rushhour/RushHourLogic.js b/public/src/games/rushhour/RushHourLogic.js new file mode 100644 index 0000000..1ef6efb --- /dev/null +++ b/public/src/games/rushhour/RushHourLogic.js @@ -0,0 +1,135 @@ +// Rush Hour — pure board model + BFS solver. No Phaser, no DOM. +// Shared by the client scene and the offline puzzle generator (both ESM). +// +// Grid is 6x6. x = column (0 left .. 5 right), y = row (0 top .. 5 bottom). +// The red target car is horizontal in EXIT_ROW and escapes through a gap in +// the right wall: solved when its right end reaches the rightmost column. +// +// A vehicle: { id, x, y, len, orient: 'h'|'v', isTarget } +// horizontal -> occupies (x..x+len-1, y); vertical -> occupies (x, y..y+len-1) +// +// Move metric (matches published Rush Hour difficulty): sliding one car any +// number of squares in a single direction counts as ONE move. + +export const GRID = 6; +export const EXIT_ROW = 2; +export const TARGET_ID = 'X'; + +export function vehicleCells(v) { + const cells = []; + for (let i = 0; i < v.len; i++) { + cells.push(v.orient === 'h' ? [v.x + i, v.y] : [v.x, v.y + i]); + } + return cells; +} + +// 6x6 grid of vehicle id (or null) for occupancy tests. +export function buildGrid(vehicles) { + const grid = Array.from({ length: GRID }, () => Array(GRID).fill(null)); + for (const v of vehicles) { + for (const [x, y] of vehicleCells(v)) grid[y][x] = v.id; + } + return grid; +} + +export function isSolved(vehicles) { + const t = vehicles.find((v) => v.isTarget); + return !!t && t.orient === 'h' && t.y === EXIT_ROW && t.x + t.len - 1 === GRID - 1; +} + +// Compact state key. Vehicle order / len / orient / fixed-axis never change, +// so keying on each vehicle's moving coordinate is sufficient and unique. +export function stateKey(vehicles) { + return vehicles.map((v) => (v.orient === 'h' ? v.x : v.y)).join(','); +} + +// Every distinct landing position reachable in one slide. Each entry is one +// move regardless of distance travelled. +export function legalMoves(vehicles) { + const grid = buildGrid(vehicles); + const moves = []; + vehicles.forEach((v, idx) => { + if (v.orient === 'h') { + for (let nx = v.x - 1; nx >= 0 && grid[v.y][nx] === null; nx--) { + moves.push({ idx, id: v.id, x: nx, y: v.y }); + } + for (let nx = v.x + v.len; nx < GRID && grid[v.y][nx] === null; nx++) { + moves.push({ idx, id: v.id, x: nx - v.len + 1, y: v.y }); + } + } else { + for (let ny = v.y - 1; ny >= 0 && grid[ny][v.x] === null; ny--) { + moves.push({ idx, id: v.id, x: v.x, y: ny }); + } + for (let ny = v.y + v.len; ny < GRID && grid[ny][v.x] === null; ny++) { + moves.push({ idx, id: v.id, x: v.x, y: ny - v.len + 1 }); + } + } + }); + return moves; +} + +// Contiguous range a vehicle can occupy along its axis, for drag snapping. +// Horizontal -> {min,max} are x; vertical -> {min,max} are y. +export function slideRange(vehicles, idx) { + const grid = buildGrid(vehicles); + const v = vehicles[idx]; + if (v.orient === 'h') { + let min = v.x, max = v.x; + for (let nx = v.x - 1; nx >= 0 && grid[v.y][nx] === null; nx--) min = nx; + for (let nx = v.x + v.len; nx < GRID && grid[v.y][nx] === null; nx++) max = nx - v.len + 1; + return { min, max }; + } + let min = v.y, max = v.y; + for (let ny = v.y - 1; ny >= 0 && grid[ny][v.x] === null; ny--) min = ny; + for (let ny = v.y + v.len; ny < GRID && grid[ny][v.x] === null; ny++) max = ny - v.len + 1; + return { min, max }; +} + +export function cloneVehicles(vehicles) { + return vehicles.map((v) => ({ ...v })); +} + +// Breadth-first shortest solution. Returns { moves, path }. +// moves: minimum number of slides to solve (0 if already solved, -1 if none) +// path: array of { id, x, y } optimal moves (null if unsolvable) +// The 6x6 state space is small, but packed unsolvable layouts can still have a +// large reachable space. `maxStates` bounds the search so a single solve stays +// cheap (used heavily by the generator); exceeding it returns moves:-1. +export function solve(vehicles, { maxStates = 600000 } = {}) { + const start = cloneVehicles(vehicles); + const startKey = stateKey(start); + if (isSolved(start)) return { moves: 0, path: [] }; + + const meta = new Map([[startKey, null]]); // key -> { parentKey, move } + const stateByKey = new Map([[startKey, start]]); + let frontier = [startKey]; + let depth = 0; + + while (frontier.length) { + depth++; + const next = []; + for (const key of frontier) { + const state = stateByKey.get(key); + for (const mv of legalMoves(state)) { + const ns = cloneVehicles(state); + ns[mv.idx].x = mv.x; + ns[mv.idx].y = mv.y; + const nk = stateKey(ns); + if (meta.has(nk)) continue; + meta.set(nk, { parentKey: key, move: { id: mv.id, x: mv.x, y: mv.y } }); + stateByKey.set(nk, ns); + if (isSolved(ns)) { + const path = []; + let cur = nk; + while (meta.get(cur)) { const e = meta.get(cur); path.unshift(e.move); cur = e.parentKey; } + return { moves: depth, path }; + } + next.push(nk); + } + if (meta.size > maxStates) return { moves: -1, path: null }; + } + frontier = next; + if (depth > 100) break; + } + return { moves: -1, path: null }; +} diff --git a/public/src/main.js b/public/src/main.js index 642e2a3..e3e6c97 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -61,6 +61,7 @@ import KiitosGame from './games/kiitos/KiitosGame.js'; import MonopolyGame from './games/monopoly/MonopolyGame.js'; import TriominoesGame from './games/triominoes/TriominoesGame.js'; import FreecellGame from './games/freecell/FreecellGame.js'; +import RushHourGame from './games/rushhour/RushHourGame.js'; const config = { type: Phaser.AUTO, @@ -135,6 +136,7 @@ const config = { MonopolyGame, TriominoesGame, FreecellGame, + RushHourGame, ], }; diff --git a/public/src/scenes/GameMenuScene.js b/public/src/scenes/GameMenuScene.js index 83c0628..aaae5eb 100644 --- a/public/src/scenes/GameMenuScene.js +++ b/public/src/scenes/GameMenuScene.js @@ -13,9 +13,10 @@ const CATEGORIES = [ { key: 'cards', label: 'Cards & Dice' }, { key: 'casino', label: 'Casino' }, { key: 'word', label: 'Words & Numbers' }, + { key: 'logic', label: 'Logic & Puzzle' }, ]; -const TAB_ICON_FRAMES = { tabletop: 0, cards: 1, casino: 2, word: 3 }; +const TAB_ICON_FRAMES = { tabletop: 0, cards: 1, casino: 2, word: 3, logic: 4 }; const ICON_INACTIVE = 56; const ICON_ACTIVE = 72; const ICON_OVERSHOOT = 86; diff --git a/public/src/scenes/GameRoomScene.js b/public/src/scenes/GameRoomScene.js index d67d66b..c0c29db 100644 --- a/public/src/scenes/GameRoomScene.js +++ b/public/src/scenes/GameRoomScene.js @@ -22,7 +22,7 @@ export default class GameRoomScene extends Phaser.Scene { } create() { - const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame' }; + const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame' }; if (slugDispatch[this.game.slug]) { this.scene.start(slugDispatch[this.game.slug], { game: this.game, diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index 37211a8..423e9e8 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -59,6 +59,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.json('playfields', '/data/playfields.json'); this.load.json('card-backs', '/data/card-backs.json'); this.load.json('music', '/data/music.json'); + this.load.json('rushhour', '/data/rushhour.json'); this.load.audio('sfx-water-splash', '/assets/fx/water-splash.mp3'); this.load.audio('sfx-water-sink', '/assets/fx/water-sink.mp3'); diff --git a/server/db/migrations/005_logic_category.sql b/server/db/migrations/005_logic_category.sql new file mode 100644 index 0000000..f316dd1 --- /dev/null +++ b/server/db/migrations/005_logic_category.sql @@ -0,0 +1,17 @@ +-- Extend the games.category CHECK constraint to include 'logic'. +-- SQLite requires table recreation to change a CHECK constraint. + +CREATE TABLE games_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + category TEXT NOT NULL CHECK (category IN ('tabletop', 'casino', 'word', 'cards', 'logic')), + max_players INTEGER NOT NULL DEFAULT 2, + supports_multiplayer INTEGER NOT NULL DEFAULT 1 +); + +INSERT INTO games_new SELECT * FROM games; + +DROP TABLE games; + +ALTER TABLE games_new RENAME TO games; diff --git a/server/db/migrations/006_puzzle_progress.sql b/server/db/migrations/006_puzzle_progress.sql new file mode 100644 index 0000000..65ce583 --- /dev/null +++ b/server/db/migrations/006_puzzle_progress.sql @@ -0,0 +1,14 @@ +-- Per-user, per-game level progress for ordered single-player puzzle games +-- (Rush Hour and future Logic & Puzzle titles). +-- +-- A single integer `levels_completed` records the highest contiguous level the +-- user has cleared. Because levels must be played in order, the next playable +-- level is always levels_completed + 1. + +CREATE TABLE puzzle_progress ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + slug TEXT NOT NULL, + levels_completed INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (user_id, slug) +); diff --git a/server/games/registry.js b/server/games/registry.js index e366196..61dc267 100644 --- a/server/games/registry.js +++ b/server/games/registry.js @@ -76,3 +76,4 @@ registerGame({ slug: 'kiitos', name: 'Kiitos', category: ' registerGame({ slug: 'monopoly', name: 'Monopoly', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 48 }); registerGame({ slug: 'triominoes', name: 'Tri-Ominoes', category: 'word', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 49 }); registerGame({ slug: 'freecell', name: 'Freecell', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 50 }); +registerGame({ slug: 'rushhour', name: 'Rush Hour', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 51 }); diff --git a/server/index.js b/server/index.js index cc5eb24..20d95db 100644 --- a/server/index.js +++ b/server/index.js @@ -9,6 +9,7 @@ import profileRoutes from './profile/routes.js'; import historyRoutes from './history/routes.js'; import historyRecordRoutes from './history/recordRoutes.js'; import wordRoutes from './words/wordRoutes.js'; +import puzzleRoutes from './puzzles/routes.js'; import { listGames } from './games/registry.js'; const app = express(); @@ -25,6 +26,7 @@ app.use('/api/profile', profileRoutes); app.use('/api/history', historyRoutes); app.use('/api/history', historyRecordRoutes); app.use('/api/words', wordRoutes); +app.use('/api/puzzles', puzzleRoutes); app.use(express.static(config.publicDir, { extensions: ['html'] })); diff --git a/server/puzzles/routes.js b/server/puzzles/routes.js new file mode 100644 index 0000000..c2ebf62 --- /dev/null +++ b/server/puzzles/routes.js @@ -0,0 +1,54 @@ +import { Router } from 'express'; +import db from '../db/index.js'; +import { requireAuth } from '../auth/middleware.js'; +import { getGame } from '../games/registry.js'; + +const router = Router(); + +// GET /api/puzzles/:slug/progress +// Returns the highest contiguous level the signed-in user has cleared. +router.get('/:slug/progress', requireAuth, (req, res) => { + const { slug } = req.params; + if (!getGame(slug)) return res.status(400).json({ error: 'Unknown game slug.' }); + + const row = db + .prepare('SELECT levels_completed FROM puzzle_progress WHERE user_id = ? AND slug = ?') + .get(req.user.id, slug); + + res.json({ levelsCompleted: row?.levels_completed ?? 0 }); +}); + +// POST /api/puzzles/:slug/complete body: { level } +// Records completion of `level`. Levels must be cleared in order, so this only +// advances progress when level === current + 1; replaying an earlier level is a +// no-op that still returns the current high-water mark. +router.post('/:slug/complete', requireAuth, (req, res) => { + const { slug } = req.params; + const { level } = req.body ?? {}; + + if (!getGame(slug)) return res.status(400).json({ error: 'Unknown game slug.' }); + if (!Number.isInteger(level) || level < 1) { + return res.status(400).json({ error: 'Invalid level.' }); + } + + const row = db + .prepare('SELECT levels_completed FROM puzzle_progress WHERE user_id = ? AND slug = ?') + .get(req.user.id, slug); + const current = row?.levels_completed ?? 0; + + if (level !== current + 1) { + return res.json({ levelsCompleted: current }); + } + + db.prepare( + `INSERT INTO puzzle_progress (user_id, slug, levels_completed, updated_at) + VALUES (?, ?, ?, datetime('now')) + ON CONFLICT(user_id, slug) + DO UPDATE SET levels_completed = excluded.levels_completed, + updated_at = excluded.updated_at`, + ).run(req.user.id, slug, level); + + res.json({ levelsCompleted: level }); +}); + +export default router; diff --git a/server/scripts/genRushHour.js b/server/scripts/genRushHour.js new file mode 100644 index 0000000..10c744b --- /dev/null +++ b/server/scripts/genRushHour.js @@ -0,0 +1,177 @@ +// Offline generator for Rush Hour puzzles. +// +// Random-fills a 6x6 board with a red target car plus cars (len 2) and trucks +// (len 3), runs the BFS solver to (a) reject unsolvable/trivial layouts and +// (b) label each survivor with its minimum-moves difficulty, then selects a +// smooth easy->expert curve and writes ordered levels to public/data/rushhour.json. +// +// Usage: +// node server/scripts/genRushHour.js [seed] [outFile] +// +// Deterministic: same seed -> same bank. Re-run after changing the curve. + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + GRID, EXIT_ROW, TARGET_ID, isSolved, solve, vehicleCells, +} from '../../public/src/games/rushhour/RushHourLogic.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT_FILE = process.argv[3] + ? path.resolve(process.argv[3]) + : path.join(__dirname, '../../public/data/rushhour.json'); + +const SEED = process.argv[2] ? Number(process.argv[2]) >>> 0 : 0x9e3779b9; + +// ── Seeded RNG (mulberry32) ────────────────────────────────────────────────── +function makeRng(seed) { + let a = seed >>> 0; + return () => { + a |= 0; a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} +const rng = makeRng(SEED); +const randInt = (n) => Math.floor(rng() * n); +const pick = (arr) => arr[randInt(arr.length)]; + +// Difficulty curve: count + [min,max] minMoves per tier. Sums to the bank size. +const TIERS = [ + { count: 8, min: 2, max: 4 }, + { count: 10, min: 5, max: 7 }, + { count: 10, min: 8, max: 11 }, + { count: 8, min: 12, max: 15 }, + { count: 7, min: 16, max: 20 }, + { count: 5, min: 21, max: 40 }, +]; +const MAX_ATTEMPTS = 8000000; +const MAX_SECONDS = 220; // wall-clock budget +const SOLVE_MAX_STATES = 12000; // bound per-solve cost (rejects pathological layouts fast) + +const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'; + +// Canonical, label-independent key for dedup. +function canonKey(vehicles) { + return vehicles + .map((v) => `${v.x},${v.y},${v.len},${v.orient},${v.isTarget ? 1 : 0}`) + .sort() + .join('|'); +} + +function fits(grid, v) { + for (const [x, y] of vehicleCells(v)) { + if (x < 0 || x >= GRID || y < 0 || y >= GRID) return false; + if (grid[y][x] !== null) return false; + } + return true; +} +function stamp(grid, v) { + for (const [x, y] of vehicleCells(v)) grid[y][x] = v.id; +} + +// Build one random candidate layout (target + blockers), or null if it failed +// to place the target. +function randomLayout() { + const grid = Array.from({ length: GRID }, () => Array(GRID).fill(null)); + const vehicles = []; + + // Target car: horizontal, length 2, starting far left so its escape path is + // long and likely blocked -> richer puzzles. + const target = { id: TARGET_ID, x: randInt(2), y: EXIT_ROW, len: 2, orient: 'h', isTarget: true }; + stamp(grid, target); + vehicles.push(target); + + const count = 6 + randInt(5); // 6..10 blockers + let letterIdx = 0; + let tries = 0; + while (vehicles.length < count + 1 && tries < 200) { + tries++; + // Favour vertical cars: they cross the exit row and create real blockades. + const orient = rng() < 0.6 ? 'v' : 'h'; + const len = rng() < 0.3 ? 3 : 2; + const x = orient === 'h' ? randInt(GRID - len + 1) : randInt(GRID); + const y = orient === 'h' ? randInt(GRID) : randInt(GRID - len + 1); + const v = { id: LETTERS[letterIdx], x, y, len, orient, isTarget: false }; + if (!fits(grid, v)) continue; + stamp(grid, v); + vehicles.push(v); + letterIdx++; + } + return vehicles; +} + +// ── Generate pool ──────────────────────────────────────────────────────────── +console.log(`[rushhour] generating with seed ${SEED}…`); +const target = TIERS.reduce((t, x) => t + x.count, 0); +const buckets = TIERS.map(() => []); +const seen = new Set(); +let attempts = 0; +let solved = 0; + +function tierIndexFor(moves) { + for (let i = 0; i < TIERS.length; i++) { + if (moves >= TIERS[i].min && moves <= TIERS[i].max) return i; + } + return -1; +} +const tierFull = () => buckets.every((b, i) => b.length >= TIERS[i].count); +const startedAt = Date.now(); + +while (attempts < MAX_ATTEMPTS && !tierFull()) { + attempts++; + if ((attempts & 0x3ff) === 0 && (Date.now() - startedAt) / 1000 > MAX_SECONDS) { + console.log('\n[rushhour] time budget reached, stopping early'); + break; + } + const vehicles = randomLayout(); + if (isSolved(vehicles)) continue; // already at exit -> trivial + const key = canonKey(vehicles); + if (seen.has(key)) continue; + seen.add(key); + + const { moves } = solve(vehicles, { maxStates: SOLVE_MAX_STATES }); + if (moves < 2) continue; // unsolvable or 1-move (no blockers) + solved++; + const ti = tierIndexFor(moves); + if (ti === -1) continue; + if (buckets[ti].length >= TIERS[ti].count) continue; + buckets[ti].push({ minMoves: moves, vehicles }); + + if (solved % 2000 === 0) { + process.stdout.write(`\r[rushhour] attempts=${attempts} kept=${buckets.reduce((t, b) => t + b.length, 0)}/${target} `); + } +} +process.stdout.write('\n'); + +// ── Assemble ordered levels ────────────────────────────────────────────────── +const chosen = []; +buckets.forEach((b) => { + b.sort((p, q) => p.minMoves - q.minMoves); + chosen.push(...b); +}); +// Overall ascending so levels ramp smoothly even across tier boundaries. +chosen.sort((p, q) => p.minMoves - q.minMoves); + +const puzzles = chosen.map((p, i) => ({ + level: i + 1, + minMoves: p.minMoves, + vehicles: p.vehicles, +})); + +const payload = { + generatedAt: new Date().toISOString(), + seed: SEED, + count: puzzles.length, + puzzles, +}; + +fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true }); +fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2)); + +const perTier = buckets.map((b, i) => `${b.length}/${TIERS[i].count}`).join(' '); +console.log(`[rushhour] attempts=${attempts} solvable=${solved}`); +console.log(`[rushhour] tiers filled: ${perTier}`); +console.log(`[rushhour] wrote ${puzzles.length} levels (minMoves ${puzzles[0]?.minMoves}..${puzzles[puzzles.length - 1]?.minMoves}) -> ${OUT_FILE}`);