From 762792f9f5b810721be002e1262989e63045bb38 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sat, 20 Jun 2026 08:26:23 -0600 Subject: [PATCH] feat: add Katamino single-player pentomino puzzle game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Katamino, a 5×N grid tiling puzzle using the 12 standard pentominoes. Players select a grid width (3-12) and solve puzzles by placing all chosen pieces to fill the board. Key features: - Three-screen flow: penta select → set select → play - Scrollable puzzle card grid with completion progress tracking - Piece rotation (R), flip (F), and lift-to-reposition - Pre-generated puzzle bank (katamino.json) with all valid tilings - Offline generator script (genKatamino.js) using backtracking solver - Progress saved to localStorage per puzzle - Registration in server registry and scene dispatchers --- public/data/katamino.json | 1 + .../games/geniussquare/GeniusSquareGame.js | 22 +- public/src/games/katamino/KataminoGame.js | 703 ++++++++++++++++++ public/src/games/katamino/KataminoLogic.js | 122 +++ public/src/main.js | 2 + public/src/scenes/GameRoomScene.js | 2 +- public/src/scenes/PreloadScene.js | 1 + server/games/registry.js | 1 + server/scripts/genKatamino.js | 176 +++++ 9 files changed, 1026 insertions(+), 4 deletions(-) create mode 100644 public/data/katamino.json create mode 100644 public/src/games/katamino/KataminoGame.js create mode 100644 public/src/games/katamino/KataminoLogic.js create mode 100644 server/scripts/genKatamino.js diff --git a/public/data/katamino.json b/public/data/katamino.json new file mode 100644 index 0000000..3479d97 --- /dev/null +++ b/public/data/katamino.json @@ -0,0 +1 @@ +{"generatedAt":"2026-06-20T14:05:59.593Z","totalTime":"52.6s","pentas":{"3":[{"idx":0,"pieces":["F","P","U"]},{"idx":1,"pieces":["L","N","V"]},{"idx":2,"pieces":["L","P","V"]},{"idx":3,"pieces":["L","T","Y"]},{"idx":4,"pieces":["N","P","U"]},{"idx":5,"pieces":["P","U","V"]},{"idx":6,"pieces":["P","U","Y"]}],"4":[{"idx":0,"pieces":["F","I","P","U"]},{"idx":1,"pieces":["F","L","P","U"]},{"idx":2,"pieces":["F","L","T","Y"]},{"idx":3,"pieces":["F","L","U","V"]},{"idx":4,"pieces":["F","L","U","Y"]},{"idx":5,"pieces":["F","P","U","Y"]},{"idx":6,"pieces":["I","L","N","V"]},{"idx":7,"pieces":["I","L","P","V"]},{"idx":8,"pieces":["I","L","T","Y"]},{"idx":9,"pieces":["I","N","P","U"]},{"idx":10,"pieces":["I","P","U","V"]},{"idx":11,"pieces":["I","P","U","Y"]},{"idx":12,"pieces":["L","N","P","U"]},{"idx":13,"pieces":["L","N","V","Z"]},{"idx":14,"pieces":["L","P","T","V"]},{"idx":15,"pieces":["L","P","T","Y"]},{"idx":16,"pieces":["L","P","U","Y"]},{"idx":17,"pieces":["L","P","V","Y"]},{"idx":18,"pieces":["L","P","V","Z"]},{"idx":19,"pieces":["L","P","W","Y"]},{"idx":20,"pieces":["L","P","Y","Z"]},{"idx":21,"pieces":["L","U","V","Y"]},{"idx":22,"pieces":["N","P","U","Y"]},{"idx":23,"pieces":["N","T","V","Y"]},{"idx":24,"pieces":["P","T","V","W"]},{"idx":25,"pieces":["P","U","V","Z"]}],"5":[{"idx":0,"pieces":["F","I","L","P","U"]},{"idx":1,"pieces":["F","I","L","T","Y"]},{"idx":2,"pieces":["F","I","L","U","V"]},{"idx":3,"pieces":["F","I","L","U","Y"]},{"idx":4,"pieces":["F","I","P","U","Y"]},{"idx":5,"pieces":["F","L","P","U","W"]},{"idx":6,"pieces":["F","L","P","U","X"]},{"idx":7,"pieces":["F","L","P","V","W"]},{"idx":8,"pieces":["F","L","P","V","Y"]},{"idx":9,"pieces":["F","L","U","Y","Z"]},{"idx":10,"pieces":["F","N","P","U","V"]},{"idx":11,"pieces":["F","N","T","V","Y"]},{"idx":12,"pieces":["F","P","T","U","Y"]},{"idx":13,"pieces":["I","L","N","P","U"]},{"idx":14,"pieces":["I","L","N","V","Z"]},{"idx":15,"pieces":["I","L","P","T","V"]},{"idx":16,"pieces":["I","L","P","T","Y"]},{"idx":17,"pieces":["I","L","P","U","Y"]},{"idx":18,"pieces":["I","L","P","V","Y"]},{"idx":19,"pieces":["I","L","P","V","Z"]},{"idx":20,"pieces":["I","L","P","W","Y"]},{"idx":21,"pieces":["I","L","P","Y","Z"]},{"idx":22,"pieces":["I","L","U","V","Y"]},{"idx":23,"pieces":["I","N","P","U","Y"]},{"idx":24,"pieces":["I","N","T","V","Y"]},{"idx":25,"pieces":["I","P","T","V","W"]},{"idx":26,"pieces":["I","P","U","V","Z"]},{"idx":27,"pieces":["L","N","P","T","W"]},{"idx":28,"pieces":["L","N","P","U","Z"]},{"idx":29,"pieces":["L","N","P","V","Y"]},{"idx":30,"pieces":["L","N","P","W","Y"]},{"idx":31,"pieces":["L","N","U","V","Z"]},{"idx":32,"pieces":["L","N","U","W","Y"]},{"idx":33,"pieces":["L","P","T","U","X"]},{"idx":34,"pieces":["L","P","T","U","Z"]},{"idx":35,"pieces":["L","P","T","W","Y"]},{"idx":36,"pieces":["L","P","U","V","W"]},{"idx":37,"pieces":["L","P","V","Y","Z"]},{"idx":38,"pieces":["L","P","W","Y","Z"]},{"idx":39,"pieces":["L","T","V","W","Y"]},{"idx":40,"pieces":["L","U","V","Y","Z"]},{"idx":41,"pieces":["N","P","U","V","Y"]},{"idx":42,"pieces":["N","P","U","Y","Z"]},{"idx":43,"pieces":["N","P","V","W","Y"]},{"idx":44,"pieces":["P","V","W","Y","Z"]}],"6":[{"idx":0,"pieces":["F","I","L","N","P","T"]},{"idx":1,"pieces":["F","I","L","N","P","V"]},{"idx":2,"pieces":["F","I","L","N","U","Y"]},{"idx":3,"pieces":["F","I","L","N","V","W"]},{"idx":4,"pieces":["F","I","L","P","T","V"]},{"idx":5,"pieces":["F","I","L","P","T","Y"]},{"idx":6,"pieces":["F","I","L","P","U","W"]},{"idx":7,"pieces":["F","I","L","P","U","X"]},{"idx":8,"pieces":["F","I","L","P","U","Y"]},{"idx":9,"pieces":["F","I","L","P","V","W"]},{"idx":10,"pieces":["F","I","L","P","V","Y"]},{"idx":11,"pieces":["F","I","L","P","W","Y"]},{"idx":12,"pieces":["F","I","L","P","Y","Z"]},{"idx":13,"pieces":["F","I","L","U","V","X"]},{"idx":14,"pieces":["F","I","L","U","V","Y"]},{"idx":15,"pieces":["F","I","L","U","V","Z"]},{"idx":16,"pieces":["F","I","L","U","Y","Z"]},{"idx":17,"pieces":["F","I","N","P","T","Y"]},{"idx":18,"pieces":["F","I","N","P","U","V"]},{"idx":19,"pieces":["F","I","N","P","U","Y"]},{"idx":20,"pieces":["F","I","N","P","V","Y"]},{"idx":21,"pieces":["F","I","N","P","V","Z"]},{"idx":22,"pieces":["F","I","N","T","V","Y"]},{"idx":23,"pieces":["F","I","N","U","V","Y"]},{"idx":24,"pieces":["F","I","N","V","W","Y"]},{"idx":25,"pieces":["F","I","P","T","U","V"]},{"idx":26,"pieces":["F","I","P","T","U","Y"]},{"idx":27,"pieces":["F","I","P","T","V","Y"]},{"idx":28,"pieces":["F","I","P","T","V","Z"]},{"idx":29,"pieces":["F","I","P","U","V","W"]},{"idx":30,"pieces":["F","I","P","U","V","Y"]},{"idx":31,"pieces":["F","I","P","U","Y","Z"]},{"idx":32,"pieces":["F","I","P","V","W","Y"]},{"idx":33,"pieces":["F","I","T","U","V","Y"]},{"idx":34,"pieces":["F","I","T","U","V","Z"]},{"idx":35,"pieces":["F","I","T","U","W","Y"]},{"idx":36,"pieces":["F","L","N","P","T","U"]},{"idx":37,"pieces":["F","L","N","P","T","V"]},{"idx":38,"pieces":["F","L","N","P","T","W"]},{"idx":39,"pieces":["F","L","N","P","T","Y"]},{"idx":40,"pieces":["F","L","N","P","U","V"]},{"idx":41,"pieces":["F","L","N","P","U","Y"]},{"idx":42,"pieces":["F","L","N","P","V","Y"]},{"idx":43,"pieces":["F","L","N","P","V","Z"]},{"idx":44,"pieces":["F","L","N","T","U","Y"]},{"idx":45,"pieces":["F","L","N","T","U","Z"]},{"idx":46,"pieces":["F","L","N","T","V","W"]},{"idx":47,"pieces":["F","L","N","T","V","X"]},{"idx":48,"pieces":["F","L","N","T","V","Z"]},{"idx":49,"pieces":["F","L","N","U","V","W"]},{"idx":50,"pieces":["F","L","N","U","W","Y"]},{"idx":51,"pieces":["F","L","P","T","U","V"]},{"idx":52,"pieces":["F","L","P","T","U","X"]},{"idx":53,"pieces":["F","L","P","T","U","Y"]},{"idx":54,"pieces":["F","L","P","T","U","Z"]},{"idx":55,"pieces":["F","L","P","T","W","Y"]},{"idx":56,"pieces":["F","L","P","T","Y","Z"]},{"idx":57,"pieces":["F","L","P","U","V","Y"]},{"idx":58,"pieces":["F","L","P","U","W","Y"]},{"idx":59,"pieces":["F","L","P","U","X","Y"]},{"idx":60,"pieces":["F","L","P","V","W","Y"]},{"idx":61,"pieces":["F","L","P","V","Y","Z"]},{"idx":62,"pieces":["F","L","P","W","Y","Z"]},{"idx":63,"pieces":["F","L","T","U","V","Y"]},{"idx":64,"pieces":["F","L","T","V","Y","Z"]},{"idx":65,"pieces":["F","L","U","V","Y","Z"]},{"idx":66,"pieces":["F","N","P","T","U","Y"]},{"idx":67,"pieces":["F","N","P","U","V","W"]},{"idx":68,"pieces":["F","N","P","U","V","X"]},{"idx":69,"pieces":["F","N","P","U","V","Y"]},{"idx":70,"pieces":["F","N","P","U","V","Z"]},{"idx":71,"pieces":["F","N","P","U","Y","Z"]},{"idx":72,"pieces":["F","N","P","V","W","Y"]},{"idx":73,"pieces":["F","N","U","V","Y","Z"]},{"idx":74,"pieces":["F","P","T","U","W","Y"]},{"idx":75,"pieces":["F","P","T","U","X","Y"]},{"idx":76,"pieces":["F","P","T","V","W","Y"]},{"idx":77,"pieces":["F","P","U","V","X","Y"]},{"idx":78,"pieces":["F","P","U","V","Y","Z"]},{"idx":79,"pieces":["I","L","N","P","T","V"]},{"idx":80,"pieces":["I","L","N","P","T","W"]},{"idx":81,"pieces":["I","L","N","P","T","Y"]},{"idx":82,"pieces":["I","L","N","P","T","Z"]},{"idx":83,"pieces":["I","L","N","P","U","Y"]},{"idx":84,"pieces":["I","L","N","P","U","Z"]},{"idx":85,"pieces":["I","L","N","P","V","W"]},{"idx":86,"pieces":["I","L","N","P","V","Y"]},{"idx":87,"pieces":["I","L","N","P","V","Z"]},{"idx":88,"pieces":["I","L","N","P","W","Y"]},{"idx":89,"pieces":["I","L","N","T","U","Y"]},{"idx":90,"pieces":["I","L","N","T","U","Z"]},{"idx":91,"pieces":["I","L","N","U","V","X"]},{"idx":92,"pieces":["I","L","N","U","V","Y"]},{"idx":93,"pieces":["I","L","N","U","V","Z"]},{"idx":94,"pieces":["I","L","N","U","W","Y"]},{"idx":95,"pieces":["I","L","N","U","Y","Z"]},{"idx":96,"pieces":["I","L","P","T","U","V"]},{"idx":97,"pieces":["I","L","P","T","U","X"]},{"idx":98,"pieces":["I","L","P","T","U","Z"]},{"idx":99,"pieces":["I","L","P","T","V","W"]},{"idx":100,"pieces":["I","L","P","T","V","Y"]},{"idx":101,"pieces":["I","L","P","T","V","Z"]},{"idx":102,"pieces":["I","L","P","T","W","Y"]},{"idx":103,"pieces":["I","L","P","T","Y","Z"]},{"idx":104,"pieces":["I","L","P","U","V","W"]},{"idx":105,"pieces":["I","L","P","U","V","Z"]},{"idx":106,"pieces":["I","L","P","U","W","Y"]},{"idx":107,"pieces":["I","L","P","U","Y","Z"]},{"idx":108,"pieces":["I","L","P","V","W","Y"]},{"idx":109,"pieces":["I","L","P","V","Y","Z"]},{"idx":110,"pieces":["I","L","P","W","Y","Z"]},{"idx":111,"pieces":["I","L","T","U","X","Y"]},{"idx":112,"pieces":["I","L","T","V","W","Y"]},{"idx":113,"pieces":["I","L","T","W","Y","Z"]},{"idx":114,"pieces":["I","L","U","V","Y","Z"]},{"idx":115,"pieces":["I","N","P","T","U","Y"]},{"idx":116,"pieces":["I","N","P","T","U","Z"]},{"idx":117,"pieces":["I","N","P","T","V","W"]},{"idx":118,"pieces":["I","N","P","T","V","Z"]},{"idx":119,"pieces":["I","N","P","U","V","Y"]},{"idx":120,"pieces":["I","N","P","U","V","Z"]},{"idx":121,"pieces":["I","N","P","U","Y","Z"]},{"idx":122,"pieces":["I","N","P","V","W","Y"]},{"idx":123,"pieces":["I","N","T","U","V","W"]},{"idx":124,"pieces":["I","N","T","U","W","Y"]},{"idx":125,"pieces":["I","N","T","V","Y","Z"]},{"idx":126,"pieces":["I","N","U","V","Y","Z"]},{"idx":127,"pieces":["I","P","T","U","V","Y"]},{"idx":128,"pieces":["I","P","T","U","W","Y"]},{"idx":129,"pieces":["I","P","T","V","W","Y"]},{"idx":130,"pieces":["I","P","T","V","W","Z"]},{"idx":131,"pieces":["I","P","T","V","Y","Z"]},{"idx":132,"pieces":["I","P","T","W","Y","Z"]},{"idx":133,"pieces":["I","P","U","V","Y","Z"]},{"idx":134,"pieces":["I","P","V","W","Y","Z"]},{"idx":135,"pieces":["L","N","P","T","U","V"]},{"idx":136,"pieces":["L","N","P","T","U","X"]},{"idx":137,"pieces":["L","N","P","T","U","Y"]},{"idx":138,"pieces":["L","N","P","T","V","Y"]},{"idx":139,"pieces":["L","N","P","T","V","Z"]},{"idx":140,"pieces":["L","N","P","T","W","Y"]},{"idx":141,"pieces":["L","N","P","T","W","Z"]},{"idx":142,"pieces":["L","N","P","T","Y","Z"]},{"idx":143,"pieces":["L","N","P","U","V","W"]},{"idx":144,"pieces":["L","N","P","U","V","X"]},{"idx":145,"pieces":["L","N","P","U","V","Y"]},{"idx":146,"pieces":["L","N","P","U","W","Y"]},{"idx":147,"pieces":["L","N","P","U","X","Y"]},{"idx":148,"pieces":["L","N","P","U","X","Z"]},{"idx":149,"pieces":["L","N","P","U","Y","Z"]},{"idx":150,"pieces":["L","N","P","V","W","Y"]},{"idx":151,"pieces":["L","N","P","V","Y","Z"]},{"idx":152,"pieces":["L","N","P","W","Y","Z"]},{"idx":153,"pieces":["L","N","T","U","V","Z"]},{"idx":154,"pieces":["L","P","T","U","V","Y"]},{"idx":155,"pieces":["L","P","T","U","Y","Z"]},{"idx":156,"pieces":["L","P","T","V","W","X"]},{"idx":157,"pieces":["L","P","T","V","Y","Z"]},{"idx":158,"pieces":["L","P","T","W","Y","Z"]},{"idx":159,"pieces":["L","P","U","V","W","Y"]},{"idx":160,"pieces":["L","P","U","V","X","Z"]},{"idx":161,"pieces":["L","P","V","W","Y","Z"]},{"idx":162,"pieces":["L","T","V","W","Y","Z"]},{"idx":163,"pieces":["N","P","T","U","V","X"]},{"idx":164,"pieces":["N","P","T","U","V","Z"]},{"idx":165,"pieces":["N","P","T","U","Y","Z"]},{"idx":166,"pieces":["N","P","T","V","W","Y"]},{"idx":167,"pieces":["N","P","U","V","Y","Z"]},{"idx":168,"pieces":["N","P","V","W","Y","Z"]},{"idx":169,"pieces":["N","T","U","W","Y","Z"]},{"idx":170,"pieces":["N","U","V","W","Y","Z"]},{"idx":171,"pieces":["P","T","U","V","W","Y"]}],"7":[{"idx":0,"pieces":["F","I","L","N","P","T","U"]},{"idx":1,"pieces":["F","I","L","N","P","T","V"]},{"idx":2,"pieces":["F","I","L","N","P","T","W"]},{"idx":3,"pieces":["F","I","L","N","P","T","Y"]},{"idx":4,"pieces":["F","I","L","N","P","T","Z"]},{"idx":5,"pieces":["F","I","L","N","P","U","V"]},{"idx":6,"pieces":["F","I","L","N","P","U","W"]},{"idx":7,"pieces":["F","I","L","N","P","U","Y"]},{"idx":8,"pieces":["F","I","L","N","P","V","W"]},{"idx":9,"pieces":["F","I","L","N","P","V","Y"]},{"idx":10,"pieces":["F","I","L","N","P","V","Z"]},{"idx":11,"pieces":["F","I","L","N","P","W","Y"]},{"idx":12,"pieces":["F","I","L","N","P","Y","Z"]},{"idx":13,"pieces":["F","I","L","N","T","U","Y"]},{"idx":14,"pieces":["F","I","L","N","T","U","Z"]},{"idx":15,"pieces":["F","I","L","N","T","V","W"]},{"idx":16,"pieces":["F","I","L","N","T","V","X"]},{"idx":17,"pieces":["F","I","L","N","T","V","Z"]},{"idx":18,"pieces":["F","I","L","N","U","V","W"]},{"idx":19,"pieces":["F","I","L","N","U","V","Y"]},{"idx":20,"pieces":["F","I","L","N","U","W","Y"]},{"idx":21,"pieces":["F","I","L","N","U","Y","Z"]},{"idx":22,"pieces":["F","I","L","N","V","W","Y"]},{"idx":23,"pieces":["F","I","L","N","V","W","Z"]},{"idx":24,"pieces":["F","I","L","P","T","U","V"]},{"idx":25,"pieces":["F","I","L","P","T","U","W"]},{"idx":26,"pieces":["F","I","L","P","T","U","X"]},{"idx":27,"pieces":["F","I","L","P","T","U","Y"]},{"idx":28,"pieces":["F","I","L","P","T","U","Z"]},{"idx":29,"pieces":["F","I","L","P","T","V","W"]},{"idx":30,"pieces":["F","I","L","P","T","V","Y"]},{"idx":31,"pieces":["F","I","L","P","T","W","Y"]},{"idx":32,"pieces":["F","I","L","P","T","Y","Z"]},{"idx":33,"pieces":["F","I","L","P","U","V","W"]},{"idx":34,"pieces":["F","I","L","P","U","V","Y"]},{"idx":35,"pieces":["F","I","L","P","U","V","Z"]},{"idx":36,"pieces":["F","I","L","P","U","W","Y"]},{"idx":37,"pieces":["F","I","L","P","U","W","Z"]},{"idx":38,"pieces":["F","I","L","P","U","X","Y"]},{"idx":39,"pieces":["F","I","L","P","U","Y","Z"]},{"idx":40,"pieces":["F","I","L","P","V","W","Y"]},{"idx":41,"pieces":["F","I","L","P","V","W","Z"]},{"idx":42,"pieces":["F","I","L","P","V","Y","Z"]},{"idx":43,"pieces":["F","I","L","P","W","Y","Z"]},{"idx":44,"pieces":["F","I","L","T","U","V","Y"]},{"idx":45,"pieces":["F","I","L","T","U","W","Y"]},{"idx":46,"pieces":["F","I","L","T","U","X","Y"]},{"idx":47,"pieces":["F","I","L","T","V","X","Y"]},{"idx":48,"pieces":["F","I","L","T","V","Y","Z"]},{"idx":49,"pieces":["F","I","L","U","V","W","Y"]},{"idx":50,"pieces":["F","I","L","U","V","Y","Z"]},{"idx":51,"pieces":["F","I","L","V","W","Y","Z"]},{"idx":52,"pieces":["F","I","L","V","X","Y","Z"]},{"idx":53,"pieces":["F","I","N","P","T","U","V"]},{"idx":54,"pieces":["F","I","N","P","T","U","Y"]},{"idx":55,"pieces":["F","I","N","P","T","V","Y"]},{"idx":56,"pieces":["F","I","N","P","U","V","W"]},{"idx":57,"pieces":["F","I","N","P","U","V","X"]},{"idx":58,"pieces":["F","I","N","P","U","V","Y"]},{"idx":59,"pieces":["F","I","N","P","U","V","Z"]},{"idx":60,"pieces":["F","I","N","P","U","W","Y"]},{"idx":61,"pieces":["F","I","N","P","U","Y","Z"]},{"idx":62,"pieces":["F","I","N","P","V","W","Y"]},{"idx":63,"pieces":["F","I","N","P","V","Y","Z"]},{"idx":64,"pieces":["F","I","N","T","V","W","Y"]},{"idx":65,"pieces":["F","I","N","T","V","Y","Z"]},{"idx":66,"pieces":["F","I","N","U","V","W","Y"]},{"idx":67,"pieces":["F","I","N","U","V","Y","Z"]},{"idx":68,"pieces":["F","I","P","T","U","V","Y"]},{"idx":69,"pieces":["F","I","P","T","U","W","Y"]},{"idx":70,"pieces":["F","I","P","T","U","X","Y"]},{"idx":71,"pieces":["F","I","P","T","U","Y","Z"]},{"idx":72,"pieces":["F","I","P","T","V","W","Y"]},{"idx":73,"pieces":["F","I","P","T","V","Y","Z"]},{"idx":74,"pieces":["F","I","P","U","V","W","Y"]},{"idx":75,"pieces":["F","I","P","U","V","W","Z"]},{"idx":76,"pieces":["F","I","P","U","V","X","Y"]},{"idx":77,"pieces":["F","I","P","U","V","Y","Z"]},{"idx":78,"pieces":["F","L","N","P","T","U","V"]},{"idx":79,"pieces":["F","L","N","P","T","U","X"]},{"idx":80,"pieces":["F","L","N","P","T","U","Y"]},{"idx":81,"pieces":["F","L","N","P","T","U","Z"]},{"idx":82,"pieces":["F","L","N","P","T","V","W"]},{"idx":83,"pieces":["F","L","N","P","T","V","Y"]},{"idx":84,"pieces":["F","L","N","P","T","V","Z"]},{"idx":85,"pieces":["F","L","N","P","T","W","Y"]},{"idx":86,"pieces":["F","L","N","P","T","W","Z"]},{"idx":87,"pieces":["F","L","N","P","T","Y","Z"]},{"idx":88,"pieces":["F","L","N","P","U","V","W"]},{"idx":89,"pieces":["F","L","N","P","U","V","X"]},{"idx":90,"pieces":["F","L","N","P","U","V","Y"]},{"idx":91,"pieces":["F","L","N","P","U","V","Z"]},{"idx":92,"pieces":["F","L","N","P","U","W","Y"]},{"idx":93,"pieces":["F","L","N","P","U","W","Z"]},{"idx":94,"pieces":["F","L","N","P","U","X","Y"]},{"idx":95,"pieces":["F","L","N","P","U","X","Z"]},{"idx":96,"pieces":["F","L","N","P","U","Y","Z"]},{"idx":97,"pieces":["F","L","N","P","V","W","Y"]},{"idx":98,"pieces":["F","L","N","P","V","Y","Z"]},{"idx":99,"pieces":["F","L","N","P","W","Y","Z"]},{"idx":100,"pieces":["F","L","N","T","U","V","Y"]},{"idx":101,"pieces":["F","L","N","T","U","V","Z"]},{"idx":102,"pieces":["F","L","N","T","U","Y","Z"]},{"idx":103,"pieces":["F","L","N","U","V","W","Y"]},{"idx":104,"pieces":["F","L","N","U","V","X","Y"]},{"idx":105,"pieces":["F","L","N","U","V","Y","Z"]},{"idx":106,"pieces":["F","L","N","U","W","Y","Z"]},{"idx":107,"pieces":["F","L","N","V","W","Y","Z"]},{"idx":108,"pieces":["F","L","P","T","U","V","W"]},{"idx":109,"pieces":["F","L","P","T","U","V","X"]},{"idx":110,"pieces":["F","L","P","T","U","V","Y"]},{"idx":111,"pieces":["F","L","P","T","U","V","Z"]},{"idx":112,"pieces":["F","L","P","T","U","W","Y"]},{"idx":113,"pieces":["F","L","P","T","U","X","Y"]},{"idx":114,"pieces":["F","L","P","T","U","Y","Z"]},{"idx":115,"pieces":["F","L","P","T","V","W","X"]},{"idx":116,"pieces":["F","L","P","T","V","W","Y"]},{"idx":117,"pieces":["F","L","P","T","V","W","Z"]},{"idx":118,"pieces":["F","L","P","T","V","X","Y"]},{"idx":119,"pieces":["F","L","P","T","V","Y","Z"]},{"idx":120,"pieces":["F","L","P","T","W","X","Y"]},{"idx":121,"pieces":["F","L","P","T","W","Y","Z"]},{"idx":122,"pieces":["F","L","P","U","V","W","Y"]},{"idx":123,"pieces":["F","L","P","U","V","Y","Z"]},{"idx":124,"pieces":["F","L","P","U","W","Y","Z"]},{"idx":125,"pieces":["F","L","P","U","X","Y","Z"]},{"idx":126,"pieces":["F","L","P","V","W","Y","Z"]},{"idx":127,"pieces":["F","L","T","U","V","X","Y"]},{"idx":128,"pieces":["F","L","T","V","W","Y","Z"]},{"idx":129,"pieces":["F","L","T","V","X","Y","Z"]},{"idx":130,"pieces":["F","L","U","V","W","Y","Z"]},{"idx":131,"pieces":["F","L","U","V","X","Y","Z"]},{"idx":132,"pieces":["F","N","P","T","U","V","X"]},{"idx":133,"pieces":["F","N","P","T","U","V","Y"]},{"idx":134,"pieces":["F","N","P","T","U","V","Z"]},{"idx":135,"pieces":["F","N","P","T","U","Y","Z"]},{"idx":136,"pieces":["F","N","P","T","V","W","Y"]},{"idx":137,"pieces":["F","N","P","T","V","Y","Z"]},{"idx":138,"pieces":["F","N","P","U","V","W","Y"]},{"idx":139,"pieces":["F","N","P","U","V","X","Y"]},{"idx":140,"pieces":["F","N","P","U","V","Y","Z"]},{"idx":141,"pieces":["F","N","P","V","W","Y","Z"]},{"idx":142,"pieces":["F","N","T","U","V","W","Y"]},{"idx":143,"pieces":["F","P","T","U","V","W","Y"]},{"idx":144,"pieces":["F","P","U","V","X","Y","Z"]},{"idx":145,"pieces":["I","L","N","P","T","U","V"]},{"idx":146,"pieces":["I","L","N","P","T","U","W"]},{"idx":147,"pieces":["I","L","N","P","T","U","X"]},{"idx":148,"pieces":["I","L","N","P","T","U","Y"]},{"idx":149,"pieces":["I","L","N","P","T","V","W"]},{"idx":150,"pieces":["I","L","N","P","T","V","Y"]},{"idx":151,"pieces":["I","L","N","P","T","V","Z"]},{"idx":152,"pieces":["I","L","N","P","T","W","Y"]},{"idx":153,"pieces":["I","L","N","P","T","W","Z"]},{"idx":154,"pieces":["I","L","N","P","T","Y","Z"]},{"idx":155,"pieces":["I","L","N","P","U","V","W"]},{"idx":156,"pieces":["I","L","N","P","U","V","X"]},{"idx":157,"pieces":["I","L","N","P","U","V","Y"]},{"idx":158,"pieces":["I","L","N","P","U","W","Y"]},{"idx":159,"pieces":["I","L","N","P","U","W","Z"]},{"idx":160,"pieces":["I","L","N","P","U","X","Y"]},{"idx":161,"pieces":["I","L","N","P","U","X","Z"]},{"idx":162,"pieces":["I","L","N","P","U","Y","Z"]},{"idx":163,"pieces":["I","L","N","P","V","W","Y"]},{"idx":164,"pieces":["I","L","N","P","V","Y","Z"]},{"idx":165,"pieces":["I","L","N","P","W","Y","Z"]},{"idx":166,"pieces":["I","L","N","T","U","V","Y"]},{"idx":167,"pieces":["I","L","N","T","U","V","Z"]},{"idx":168,"pieces":["I","L","N","T","U","X","Y"]},{"idx":169,"pieces":["I","L","N","U","V","X","Y"]},{"idx":170,"pieces":["I","L","N","U","V","Y","Z"]},{"idx":171,"pieces":["I","L","P","T","U","V","W"]},{"idx":172,"pieces":["I","L","P","T","U","V","Y"]},{"idx":173,"pieces":["I","L","P","T","U","V","Z"]},{"idx":174,"pieces":["I","L","P","T","U","W","Y"]},{"idx":175,"pieces":["I","L","P","T","U","Y","Z"]},{"idx":176,"pieces":["I","L","P","T","V","W","X"]},{"idx":177,"pieces":["I","L","P","T","V","W","Y"]},{"idx":178,"pieces":["I","L","P","T","V","W","Z"]},{"idx":179,"pieces":["I","L","P","T","V","X","Y"]},{"idx":180,"pieces":["I","L","P","T","V","Y","Z"]},{"idx":181,"pieces":["I","L","P","T","W","Y","Z"]},{"idx":182,"pieces":["I","L","P","U","V","W","Y"]},{"idx":183,"pieces":["I","L","P","U","V","W","Z"]},{"idx":184,"pieces":["I","L","P","U","V","X","Z"]},{"idx":185,"pieces":["I","L","P","U","V","Y","Z"]},{"idx":186,"pieces":["I","L","P","U","W","X","Y"]},{"idx":187,"pieces":["I","L","P","U","W","Y","Z"]},{"idx":188,"pieces":["I","L","P","V","W","Y","Z"]},{"idx":189,"pieces":["I","L","T","V","W","Y","Z"]},{"idx":190,"pieces":["I","N","P","T","U","V","X"]},{"idx":191,"pieces":["I","N","P","T","U","V","Y"]},{"idx":192,"pieces":["I","N","P","T","U","V","Z"]},{"idx":193,"pieces":["I","N","P","T","U","W","Y"]},{"idx":194,"pieces":["I","N","P","T","U","Y","Z"]},{"idx":195,"pieces":["I","N","P","T","V","W","Y"]},{"idx":196,"pieces":["I","N","P","U","V","W","Z"]},{"idx":197,"pieces":["I","N","P","U","V","Y","Z"]},{"idx":198,"pieces":["I","N","P","V","W","Y","Z"]},{"idx":199,"pieces":["I","N","T","U","V","W","Y"]},{"idx":200,"pieces":["I","N","T","U","V","X","Y"]},{"idx":201,"pieces":["I","N","T","U","W","Y","Z"]},{"idx":202,"pieces":["I","N","T","V","W","Y","Z"]},{"idx":203,"pieces":["I","N","U","V","W","Y","Z"]},{"idx":204,"pieces":["I","P","T","U","V","W","Y"]},{"idx":205,"pieces":["I","P","T","U","V","Y","Z"]},{"idx":206,"pieces":["I","P","T","U","W","Y","Z"]},{"idx":207,"pieces":["I","P","T","V","W","Y","Z"]},{"idx":208,"pieces":["I","P","U","V","X","Y","Z"]},{"idx":209,"pieces":["I","T","U","V","W","Y","Z"]},{"idx":210,"pieces":["L","N","P","T","U","V","W"]},{"idx":211,"pieces":["L","N","P","T","U","V","X"]},{"idx":212,"pieces":["L","N","P","T","U","V","Y"]},{"idx":213,"pieces":["L","N","P","T","U","V","Z"]},{"idx":214,"pieces":["L","N","P","T","U","W","Y"]},{"idx":215,"pieces":["L","N","P","T","U","X","Y"]},{"idx":216,"pieces":["L","N","P","T","U","Y","Z"]},{"idx":217,"pieces":["L","N","P","T","V","W","Y"]},{"idx":218,"pieces":["L","N","P","T","V","W","Z"]},{"idx":219,"pieces":["L","N","P","T","V","Y","Z"]},{"idx":220,"pieces":["L","N","P","T","W","Y","Z"]},{"idx":221,"pieces":["L","N","P","U","V","W","Y"]},{"idx":222,"pieces":["L","N","P","U","V","W","Z"]},{"idx":223,"pieces":["L","N","P","U","V","X","Y"]},{"idx":224,"pieces":["L","N","P","U","V","X","Z"]},{"idx":225,"pieces":["L","N","P","U","V","Y","Z"]},{"idx":226,"pieces":["L","N","P","U","W","Y","Z"]},{"idx":227,"pieces":["L","N","P","U","X","Y","Z"]},{"idx":228,"pieces":["L","N","P","V","W","Y","Z"]},{"idx":229,"pieces":["L","N","T","U","V","W","X"]},{"idx":230,"pieces":["L","N","T","U","V","X","Y"]},{"idx":231,"pieces":["L","N","T","U","V","Y","Z"]},{"idx":232,"pieces":["L","N","U","V","X","Y","Z"]},{"idx":233,"pieces":["L","P","T","U","V","W","Y"]},{"idx":234,"pieces":["L","P","T","U","V","X","Y"]},{"idx":235,"pieces":["L","P","T","U","V","Y","Z"]},{"idx":236,"pieces":["L","P","T","U","X","Y","Z"]},{"idx":237,"pieces":["L","P","T","V","W","Y","Z"]},{"idx":238,"pieces":["L","P","U","V","W","X","Y"]},{"idx":239,"pieces":["L","P","U","V","W","Y","Z"]},{"idx":240,"pieces":["L","P","U","V","X","Y","Z"]},{"idx":241,"pieces":["L","P","V","W","X","Y","Z"]},{"idx":242,"pieces":["N","P","T","U","V","Y","Z"]},{"idx":243,"pieces":["N","P","T","V","W","Y","Z"]},{"idx":244,"pieces":["N","P","U","V","X","Y","Z"]}],"8":[{"idx":0,"pieces":["F","I","L","N","P","T","U","V"]},{"idx":1,"pieces":["F","I","L","N","P","T","U","W"]},{"idx":2,"pieces":["F","I","L","N","P","T","U","X"]},{"idx":3,"pieces":["F","I","L","N","P","T","U","Y"]},{"idx":4,"pieces":["F","I","L","N","P","T","U","Z"]},{"idx":5,"pieces":["F","I","L","N","P","T","V","W"]},{"idx":6,"pieces":["F","I","L","N","P","T","V","X"]},{"idx":7,"pieces":["F","I","L","N","P","T","V","Y"]},{"idx":8,"pieces":["F","I","L","N","P","T","V","Z"]},{"idx":9,"pieces":["F","I","L","N","P","T","W","X"]},{"idx":10,"pieces":["F","I","L","N","P","T","W","Y"]},{"idx":11,"pieces":["F","I","L","N","P","T","W","Z"]},{"idx":12,"pieces":["F","I","L","N","P","T","X","Y"]},{"idx":13,"pieces":["F","I","L","N","P","T","X","Z"]},{"idx":14,"pieces":["F","I","L","N","P","T","Y","Z"]},{"idx":15,"pieces":["F","I","L","N","P","U","V","W"]},{"idx":16,"pieces":["F","I","L","N","P","U","V","X"]},{"idx":17,"pieces":["F","I","L","N","P","U","V","Y"]},{"idx":18,"pieces":["F","I","L","N","P","U","V","Z"]},{"idx":19,"pieces":["F","I","L","N","P","U","W","Y"]},{"idx":20,"pieces":["F","I","L","N","P","U","W","Z"]},{"idx":21,"pieces":["F","I","L","N","P","U","X","Y"]},{"idx":22,"pieces":["F","I","L","N","P","U","X","Z"]},{"idx":23,"pieces":["F","I","L","N","P","U","Y","Z"]},{"idx":24,"pieces":["F","I","L","N","P","V","W","X"]},{"idx":25,"pieces":["F","I","L","N","P","V","W","Y"]},{"idx":26,"pieces":["F","I","L","N","P","V","W","Z"]},{"idx":27,"pieces":["F","I","L","N","P","V","X","Y"]},{"idx":28,"pieces":["F","I","L","N","P","V","X","Z"]},{"idx":29,"pieces":["F","I","L","N","P","V","Y","Z"]},{"idx":30,"pieces":["F","I","L","N","P","W","Y","Z"]},{"idx":31,"pieces":["F","I","L","N","T","U","V","Y"]},{"idx":32,"pieces":["F","I","L","N","T","U","V","Z"]},{"idx":33,"pieces":["F","I","L","N","T","U","W","Y"]},{"idx":34,"pieces":["F","I","L","N","T","U","X","Y"]},{"idx":35,"pieces":["F","I","L","N","T","U","Y","Z"]},{"idx":36,"pieces":["F","I","L","N","T","V","W","Y"]},{"idx":37,"pieces":["F","I","L","N","T","V","W","Z"]},{"idx":38,"pieces":["F","I","L","N","T","V","X","Z"]},{"idx":39,"pieces":["F","I","L","N","T","V","Y","Z"]},{"idx":40,"pieces":["F","I","L","N","U","V","W","Y"]},{"idx":41,"pieces":["F","I","L","N","U","V","W","Z"]},{"idx":42,"pieces":["F","I","L","N","U","V","X","Y"]},{"idx":43,"pieces":["F","I","L","N","U","V","Y","Z"]},{"idx":44,"pieces":["F","I","L","N","U","W","Y","Z"]},{"idx":45,"pieces":["F","I","L","N","V","W","Y","Z"]},{"idx":46,"pieces":["F","I","L","P","T","U","V","W"]},{"idx":47,"pieces":["F","I","L","P","T","U","V","X"]},{"idx":48,"pieces":["F","I","L","P","T","U","V","Y"]},{"idx":49,"pieces":["F","I","L","P","T","U","V","Z"]},{"idx":50,"pieces":["F","I","L","P","T","U","W","X"]},{"idx":51,"pieces":["F","I","L","P","T","U","W","Y"]},{"idx":52,"pieces":["F","I","L","P","T","U","W","Z"]},{"idx":53,"pieces":["F","I","L","P","T","U","X","Y"]},{"idx":54,"pieces":["F","I","L","P","T","U","Y","Z"]},{"idx":55,"pieces":["F","I","L","P","T","V","W","X"]},{"idx":56,"pieces":["F","I","L","P","T","V","W","Y"]},{"idx":57,"pieces":["F","I","L","P","T","V","W","Z"]},{"idx":58,"pieces":["F","I","L","P","T","V","X","Y"]},{"idx":59,"pieces":["F","I","L","P","T","V","X","Z"]},{"idx":60,"pieces":["F","I","L","P","T","V","Y","Z"]},{"idx":61,"pieces":["F","I","L","P","T","W","X","Y"]},{"idx":62,"pieces":["F","I","L","P","T","W","Y","Z"]},{"idx":63,"pieces":["F","I","L","P","T","X","Y","Z"]},{"idx":64,"pieces":["F","I","L","P","U","V","W","X"]},{"idx":65,"pieces":["F","I","L","P","U","V","W","Y"]},{"idx":66,"pieces":["F","I","L","P","U","V","W","Z"]},{"idx":67,"pieces":["F","I","L","P","U","V","X","Y"]},{"idx":68,"pieces":["F","I","L","P","U","V","X","Z"]},{"idx":69,"pieces":["F","I","L","P","U","V","Y","Z"]},{"idx":70,"pieces":["F","I","L","P","U","W","X","Y"]},{"idx":71,"pieces":["F","I","L","P","U","W","Y","Z"]},{"idx":72,"pieces":["F","I","L","P","U","X","Y","Z"]},{"idx":73,"pieces":["F","I","L","P","V","W","X","Y"]},{"idx":74,"pieces":["F","I","L","P","V","W","Y","Z"]},{"idx":75,"pieces":["F","I","L","T","U","V","W","X"]},{"idx":76,"pieces":["F","I","L","T","U","V","W","Y"]},{"idx":77,"pieces":["F","I","L","T","U","V","X","Y"]},{"idx":78,"pieces":["F","I","L","T","U","V","Y","Z"]},{"idx":79,"pieces":["F","I","L","T","U","W","X","Y"]},{"idx":80,"pieces":["F","I","L","T","U","W","Y","Z"]},{"idx":81,"pieces":["F","I","L","T","U","X","Y","Z"]},{"idx":82,"pieces":["F","I","L","T","V","W","Y","Z"]},{"idx":83,"pieces":["F","I","L","T","V","X","Y","Z"]},{"idx":84,"pieces":["F","I","L","U","V","W","Y","Z"]},{"idx":85,"pieces":["F","I","L","U","V","X","Y","Z"]},{"idx":86,"pieces":["F","I","N","P","T","U","V","W"]},{"idx":87,"pieces":["F","I","N","P","T","U","V","X"]},{"idx":88,"pieces":["F","I","N","P","T","U","V","Y"]},{"idx":89,"pieces":["F","I","N","P","T","U","V","Z"]},{"idx":90,"pieces":["F","I","N","P","T","U","W","Y"]},{"idx":91,"pieces":["F","I","N","P","T","U","Y","Z"]},{"idx":92,"pieces":["F","I","N","P","T","V","W","Y"]},{"idx":93,"pieces":["F","I","N","P","T","V","Y","Z"]},{"idx":94,"pieces":["F","I","N","P","U","V","W","X"]},{"idx":95,"pieces":["F","I","N","P","U","V","W","Y"]},{"idx":96,"pieces":["F","I","N","P","U","V","W","Z"]},{"idx":97,"pieces":["F","I","N","P","U","V","X","Y"]},{"idx":98,"pieces":["F","I","N","P","U","V","X","Z"]},{"idx":99,"pieces":["F","I","N","P","U","V","Y","Z"]},{"idx":100,"pieces":["F","I","N","P","U","W","Y","Z"]},{"idx":101,"pieces":["F","I","N","P","V","W","X","Y"]},{"idx":102,"pieces":["F","I","N","P","V","W","Y","Z"]},{"idx":103,"pieces":["F","I","N","T","U","V","W","Y"]},{"idx":104,"pieces":["F","I","N","T","U","V","X","Y"]},{"idx":105,"pieces":["F","I","N","T","U","V","Y","Z"]},{"idx":106,"pieces":["F","I","N","T","V","W","Y","Z"]},{"idx":107,"pieces":["F","I","P","T","U","V","W","Y"]},{"idx":108,"pieces":["F","I","P","T","U","V","Y","Z"]},{"idx":109,"pieces":["F","I","P","T","U","W","Y","Z"]},{"idx":110,"pieces":["F","I","P","T","V","W","Y","Z"]},{"idx":111,"pieces":["F","I","P","U","V","W","Y","Z"]},{"idx":112,"pieces":["F","I","P","U","V","X","Y","Z"]},{"idx":113,"pieces":["F","L","N","P","T","U","V","W"]},{"idx":114,"pieces":["F","L","N","P","T","U","V","Y"]},{"idx":115,"pieces":["F","L","N","P","T","U","V","Z"]},{"idx":116,"pieces":["F","L","N","P","T","U","W","Y"]},{"idx":117,"pieces":["F","L","N","P","T","U","W","Z"]},{"idx":118,"pieces":["F","L","N","P","T","U","X","Y"]},{"idx":119,"pieces":["F","L","N","P","T","U","X","Z"]},{"idx":120,"pieces":["F","L","N","P","T","U","Y","Z"]},{"idx":121,"pieces":["F","L","N","P","T","V","W","X"]},{"idx":122,"pieces":["F","L","N","P","T","V","W","Y"]},{"idx":123,"pieces":["F","L","N","P","T","V","W","Z"]},{"idx":124,"pieces":["F","L","N","P","T","V","X","Y"]},{"idx":125,"pieces":["F","L","N","P","T","V","Y","Z"]},{"idx":126,"pieces":["F","L","N","P","T","W","X","Y"]},{"idx":127,"pieces":["F","L","N","P","T","W","Y","Z"]},{"idx":128,"pieces":["F","L","N","P","U","V","W","X"]},{"idx":129,"pieces":["F","L","N","P","U","V","W","Y"]},{"idx":130,"pieces":["F","L","N","P","U","V","W","Z"]},{"idx":131,"pieces":["F","L","N","P","U","V","X","Y"]},{"idx":132,"pieces":["F","L","N","P","U","V","X","Z"]},{"idx":133,"pieces":["F","L","N","P","U","V","Y","Z"]},{"idx":134,"pieces":["F","L","N","P","U","W","X","Y"]},{"idx":135,"pieces":["F","L","N","P","U","W","Y","Z"]},{"idx":136,"pieces":["F","L","N","P","U","X","Y","Z"]},{"idx":137,"pieces":["F","L","N","P","V","W","X","Y"]},{"idx":138,"pieces":["F","L","N","P","V","W","X","Z"]},{"idx":139,"pieces":["F","L","N","P","V","W","Y","Z"]},{"idx":140,"pieces":["F","L","N","P","V","X","Y","Z"]},{"idx":141,"pieces":["F","L","N","T","U","V","W","Y"]},{"idx":142,"pieces":["F","L","N","T","U","V","X","Y"]},{"idx":143,"pieces":["F","L","N","T","U","V","Y","Z"]},{"idx":144,"pieces":["F","L","N","T","U","W","X","Y"]},{"idx":145,"pieces":["F","L","N","U","V","W","X","Y"]},{"idx":146,"pieces":["F","L","N","U","V","W","Y","Z"]},{"idx":147,"pieces":["F","L","P","T","U","V","W","X"]},{"idx":148,"pieces":["F","L","P","T","U","V","W","Y"]},{"idx":149,"pieces":["F","L","P","T","U","V","X","Y"]},{"idx":150,"pieces":["F","L","P","T","U","V","Y","Z"]},{"idx":151,"pieces":["F","L","P","T","U","W","X","Y"]},{"idx":152,"pieces":["F","L","P","T","U","W","Y","Z"]},{"idx":153,"pieces":["F","L","P","T","U","X","Y","Z"]},{"idx":154,"pieces":["F","L","P","T","V","W","X","Y"]},{"idx":155,"pieces":["F","L","P","T","V","W","X","Z"]},{"idx":156,"pieces":["F","L","P","T","V","W","Y","Z"]},{"idx":157,"pieces":["F","L","P","T","W","X","Y","Z"]},{"idx":158,"pieces":["F","L","P","U","V","W","X","Y"]},{"idx":159,"pieces":["F","L","P","U","V","W","X","Z"]},{"idx":160,"pieces":["F","L","P","U","V","W","Y","Z"]},{"idx":161,"pieces":["F","L","P","U","V","X","Y","Z"]},{"idx":162,"pieces":["F","L","T","U","V","W","Y","Z"]},{"idx":163,"pieces":["F","L","T","U","V","X","Y","Z"]},{"idx":164,"pieces":["F","L","U","V","W","X","Y","Z"]},{"idx":165,"pieces":["F","N","P","T","U","V","W","Y"]},{"idx":166,"pieces":["F","N","P","T","U","V","X","Y"]},{"idx":167,"pieces":["F","N","P","T","U","V","Y","Z"]},{"idx":168,"pieces":["F","N","P","T","U","W","Y","Z"]},{"idx":169,"pieces":["F","N","P","T","U","X","Y","Z"]},{"idx":170,"pieces":["F","N","P","T","V","W","X","Y"]},{"idx":171,"pieces":["F","N","P","T","V","W","Y","Z"]},{"idx":172,"pieces":["F","N","P","U","V","W","Y","Z"]},{"idx":173,"pieces":["F","N","P","U","V","X","Y","Z"]},{"idx":174,"pieces":["I","L","N","P","T","U","V","W"]},{"idx":175,"pieces":["I","L","N","P","T","U","V","X"]},{"idx":176,"pieces":["I","L","N","P","T","U","V","Y"]},{"idx":177,"pieces":["I","L","N","P","T","U","V","Z"]},{"idx":178,"pieces":["I","L","N","P","T","U","W","X"]},{"idx":179,"pieces":["I","L","N","P","T","U","W","Y"]},{"idx":180,"pieces":["I","L","N","P","T","U","W","Z"]},{"idx":181,"pieces":["I","L","N","P","T","U","X","Y"]},{"idx":182,"pieces":["I","L","N","P","T","U","Y","Z"]},{"idx":183,"pieces":["I","L","N","P","T","V","W","X"]},{"idx":184,"pieces":["I","L","N","P","T","V","W","Y"]},{"idx":185,"pieces":["I","L","N","P","T","V","W","Z"]},{"idx":186,"pieces":["I","L","N","P","T","V","X","Y"]},{"idx":187,"pieces":["I","L","N","P","T","V","Y","Z"]},{"idx":188,"pieces":["I","L","N","P","T","W","Y","Z"]},{"idx":189,"pieces":["I","L","N","P","U","V","W","X"]},{"idx":190,"pieces":["I","L","N","P","U","V","W","Y"]},{"idx":191,"pieces":["I","L","N","P","U","V","W","Z"]},{"idx":192,"pieces":["I","L","N","P","U","V","X","Y"]},{"idx":193,"pieces":["I","L","N","P","U","V","X","Z"]},{"idx":194,"pieces":["I","L","N","P","U","V","Y","Z"]},{"idx":195,"pieces":["I","L","N","P","U","W","X","Y"]},{"idx":196,"pieces":["I","L","N","P","U","W","Y","Z"]},{"idx":197,"pieces":["I","L","N","P","U","X","Y","Z"]},{"idx":198,"pieces":["I","L","N","P","V","W","X","Y"]},{"idx":199,"pieces":["I","L","N","P","V","W","Y","Z"]},{"idx":200,"pieces":["I","L","N","T","U","V","W","X"]},{"idx":201,"pieces":["I","L","N","T","U","V","W","Y"]},{"idx":202,"pieces":["I","L","N","T","U","V","X","Y"]},{"idx":203,"pieces":["I","L","N","T","U","V","Y","Z"]},{"idx":204,"pieces":["I","L","N","T","U","W","X","Y"]},{"idx":205,"pieces":["I","L","N","U","V","W","Y","Z"]},{"idx":206,"pieces":["I","L","N","U","V","X","Y","Z"]},{"idx":207,"pieces":["I","L","P","T","U","V","W","Y"]},{"idx":208,"pieces":["I","L","P","T","U","V","W","Z"]},{"idx":209,"pieces":["I","L","P","T","U","V","X","Y"]},{"idx":210,"pieces":["I","L","P","T","U","V","X","Z"]},{"idx":211,"pieces":["I","L","P","T","U","V","Y","Z"]},{"idx":212,"pieces":["I","L","P","T","U","W","X","Y"]},{"idx":213,"pieces":["I","L","P","T","U","W","Y","Z"]},{"idx":214,"pieces":["I","L","P","T","U","X","Y","Z"]},{"idx":215,"pieces":["I","L","P","T","V","W","X","Y"]},{"idx":216,"pieces":["I","L","P","T","V","W","X","Z"]},{"idx":217,"pieces":["I","L","P","T","V","W","Y","Z"]},{"idx":218,"pieces":["I","L","P","T","V","X","Y","Z"]},{"idx":219,"pieces":["I","L","P","T","W","X","Y","Z"]},{"idx":220,"pieces":["I","L","P","U","V","W","X","Y"]},{"idx":221,"pieces":["I","L","P","U","V","W","Y","Z"]},{"idx":222,"pieces":["I","L","P","U","V","X","Y","Z"]},{"idx":223,"pieces":["I","L","P","U","W","X","Y","Z"]},{"idx":224,"pieces":["I","L","P","V","W","X","Y","Z"]},{"idx":225,"pieces":["I","L","T","U","V","W","Y","Z"]},{"idx":226,"pieces":["I","L","T","U","V","X","Y","Z"]},{"idx":227,"pieces":["I","N","P","T","U","V","W","Y"]},{"idx":228,"pieces":["I","N","P","T","U","V","X","Z"]},{"idx":229,"pieces":["I","N","P","T","U","V","Y","Z"]},{"idx":230,"pieces":["I","N","P","T","U","W","Y","Z"]},{"idx":231,"pieces":["I","N","P","T","V","W","Y","Z"]},{"idx":232,"pieces":["I","N","P","U","V","W","X","Y"]},{"idx":233,"pieces":["I","N","P","U","V","W","Y","Z"]},{"idx":234,"pieces":["I","N","P","U","V","X","Y","Z"]},{"idx":235,"pieces":["I","P","T","U","V","W","X","Y"]},{"idx":236,"pieces":["I","P","T","U","V","W","Y","Z"]},{"idx":237,"pieces":["L","N","P","T","U","V","W","X"]},{"idx":238,"pieces":["L","N","P","T","U","V","W","Y"]},{"idx":239,"pieces":["L","N","P","T","U","V","X","Y"]},{"idx":240,"pieces":["L","N","P","T","U","V","X","Z"]},{"idx":241,"pieces":["L","N","P","T","U","V","Y","Z"]},{"idx":242,"pieces":["L","N","P","T","U","W","X","Y"]},{"idx":243,"pieces":["L","N","P","T","U","W","Y","Z"]},{"idx":244,"pieces":["L","N","P","T","U","X","Y","Z"]},{"idx":245,"pieces":["L","N","P","T","V","W","X","Y"]},{"idx":246,"pieces":["L","N","P","T","V","W","Y","Z"]},{"idx":247,"pieces":["L","N","P","T","V","X","Y","Z"]},{"idx":248,"pieces":["L","N","P","U","V","W","X","Z"]},{"idx":249,"pieces":["L","N","P","U","V","W","Y","Z"]},{"idx":250,"pieces":["L","N","P","U","V","X","Y","Z"]},{"idx":251,"pieces":["L","N","P","V","W","X","Y","Z"]},{"idx":252,"pieces":["L","N","T","U","V","W","Y","Z"]},{"idx":253,"pieces":["L","N","T","U","W","X","Y","Z"]},{"idx":254,"pieces":["L","N","U","V","W","X","Y","Z"]},{"idx":255,"pieces":["L","P","T","U","V","W","X","Y"]},{"idx":256,"pieces":["L","P","T","U","V","W","X","Z"]},{"idx":257,"pieces":["L","P","T","U","V","W","Y","Z"]},{"idx":258,"pieces":["L","P","T","V","W","X","Y","Z"]},{"idx":259,"pieces":["L","T","U","V","W","X","Y","Z"]},{"idx":260,"pieces":["N","P","T","U","V","W","Y","Z"]}],"9":[{"idx":0,"pieces":["F","I","L","N","P","T","U","V","W"]},{"idx":1,"pieces":["F","I","L","N","P","T","U","V","X"]},{"idx":2,"pieces":["F","I","L","N","P","T","U","V","Y"]},{"idx":3,"pieces":["F","I","L","N","P","T","U","V","Z"]},{"idx":4,"pieces":["F","I","L","N","P","T","U","W","X"]},{"idx":5,"pieces":["F","I","L","N","P","T","U","W","Y"]},{"idx":6,"pieces":["F","I","L","N","P","T","U","W","Z"]},{"idx":7,"pieces":["F","I","L","N","P","T","U","X","Y"]},{"idx":8,"pieces":["F","I","L","N","P","T","U","X","Z"]},{"idx":9,"pieces":["F","I","L","N","P","T","U","Y","Z"]},{"idx":10,"pieces":["F","I","L","N","P","T","V","W","X"]},{"idx":11,"pieces":["F","I","L","N","P","T","V","W","Y"]},{"idx":12,"pieces":["F","I","L","N","P","T","V","W","Z"]},{"idx":13,"pieces":["F","I","L","N","P","T","V","X","Y"]},{"idx":14,"pieces":["F","I","L","N","P","T","V","X","Z"]},{"idx":15,"pieces":["F","I","L","N","P","T","V","Y","Z"]},{"idx":16,"pieces":["F","I","L","N","P","T","W","X","Y"]},{"idx":17,"pieces":["F","I","L","N","P","T","W","X","Z"]},{"idx":18,"pieces":["F","I","L","N","P","T","W","Y","Z"]},{"idx":19,"pieces":["F","I","L","N","P","T","X","Y","Z"]},{"idx":20,"pieces":["F","I","L","N","P","U","V","W","X"]},{"idx":21,"pieces":["F","I","L","N","P","U","V","W","Y"]},{"idx":22,"pieces":["F","I","L","N","P","U","V","W","Z"]},{"idx":23,"pieces":["F","I","L","N","P","U","V","X","Y"]},{"idx":24,"pieces":["F","I","L","N","P","U","V","X","Z"]},{"idx":25,"pieces":["F","I","L","N","P","U","V","Y","Z"]},{"idx":26,"pieces":["F","I","L","N","P","U","W","X","Y"]},{"idx":27,"pieces":["F","I","L","N","P","U","W","Y","Z"]},{"idx":28,"pieces":["F","I","L","N","P","U","X","Y","Z"]},{"idx":29,"pieces":["F","I","L","N","P","V","W","X","Y"]},{"idx":30,"pieces":["F","I","L","N","P","V","W","X","Z"]},{"idx":31,"pieces":["F","I","L","N","P","V","W","Y","Z"]},{"idx":32,"pieces":["F","I","L","N","P","V","X","Y","Z"]},{"idx":33,"pieces":["F","I","L","N","T","U","V","W","X"]},{"idx":34,"pieces":["F","I","L","N","T","U","V","W","Y"]},{"idx":35,"pieces":["F","I","L","N","T","U","V","W","Z"]},{"idx":36,"pieces":["F","I","L","N","T","U","V","X","Y"]},{"idx":37,"pieces":["F","I","L","N","T","U","V","X","Z"]},{"idx":38,"pieces":["F","I","L","N","T","U","V","Y","Z"]},{"idx":39,"pieces":["F","I","L","N","T","U","W","X","Y"]},{"idx":40,"pieces":["F","I","L","N","T","U","W","Y","Z"]},{"idx":41,"pieces":["F","I","L","N","T","U","X","Y","Z"]},{"idx":42,"pieces":["F","I","L","N","T","V","W","X","Y"]},{"idx":43,"pieces":["F","I","L","N","T","V","W","Y","Z"]},{"idx":44,"pieces":["F","I","L","N","U","V","W","X","Y"]},{"idx":45,"pieces":["F","I","L","N","U","V","W","Y","Z"]},{"idx":46,"pieces":["F","I","L","N","U","V","X","Y","Z"]},{"idx":47,"pieces":["F","I","L","N","U","W","X","Y","Z"]},{"idx":48,"pieces":["F","I","L","P","T","U","V","W","X"]},{"idx":49,"pieces":["F","I","L","P","T","U","V","W","Y"]},{"idx":50,"pieces":["F","I","L","P","T","U","V","W","Z"]},{"idx":51,"pieces":["F","I","L","P","T","U","V","X","Y"]},{"idx":52,"pieces":["F","I","L","P","T","U","V","X","Z"]},{"idx":53,"pieces":["F","I","L","P","T","U","V","Y","Z"]},{"idx":54,"pieces":["F","I","L","P","T","U","W","X","Y"]},{"idx":55,"pieces":["F","I","L","P","T","U","W","X","Z"]},{"idx":56,"pieces":["F","I","L","P","T","U","W","Y","Z"]},{"idx":57,"pieces":["F","I","L","P","T","U","X","Y","Z"]},{"idx":58,"pieces":["F","I","L","P","T","V","W","X","Y"]},{"idx":59,"pieces":["F","I","L","P","T","V","W","X","Z"]},{"idx":60,"pieces":["F","I","L","P","T","V","W","Y","Z"]},{"idx":61,"pieces":["F","I","L","P","T","V","X","Y","Z"]},{"idx":62,"pieces":["F","I","L","P","T","W","X","Y","Z"]},{"idx":63,"pieces":["F","I","L","P","U","V","W","X","Y"]},{"idx":64,"pieces":["F","I","L","P","U","V","W","X","Z"]},{"idx":65,"pieces":["F","I","L","P","U","V","W","Y","Z"]},{"idx":66,"pieces":["F","I","L","P","U","V","X","Y","Z"]},{"idx":67,"pieces":["F","I","L","P","U","W","X","Y","Z"]},{"idx":68,"pieces":["F","I","L","P","V","W","X","Y","Z"]},{"idx":69,"pieces":["F","I","L","T","U","V","W","X","Y"]},{"idx":70,"pieces":["F","I","L","T","U","V","W","Y","Z"]},{"idx":71,"pieces":["F","I","L","T","U","V","X","Y","Z"]},{"idx":72,"pieces":["F","I","L","T","U","W","X","Y","Z"]},{"idx":73,"pieces":["F","I","L","U","V","W","X","Y","Z"]},{"idx":74,"pieces":["F","I","N","P","T","U","V","W","X"]},{"idx":75,"pieces":["F","I","N","P","T","U","V","W","Y"]},{"idx":76,"pieces":["F","I","N","P","T","U","V","W","Z"]},{"idx":77,"pieces":["F","I","N","P","T","U","V","X","Y"]},{"idx":78,"pieces":["F","I","N","P","T","U","V","X","Z"]},{"idx":79,"pieces":["F","I","N","P","T","U","V","Y","Z"]},{"idx":80,"pieces":["F","I","N","P","T","U","W","Y","Z"]},{"idx":81,"pieces":["F","I","N","P","T","U","X","Y","Z"]},{"idx":82,"pieces":["F","I","N","P","T","V","W","X","Y"]},{"idx":83,"pieces":["F","I","N","P","T","V","W","Y","Z"]},{"idx":84,"pieces":["F","I","N","P","U","V","W","Y","Z"]},{"idx":85,"pieces":["F","I","N","P","U","V","X","Y","Z"]},{"idx":86,"pieces":["F","I","N","P","V","W","X","Y","Z"]},{"idx":87,"pieces":["F","I","N","T","U","V","W","X","Y"]},{"idx":88,"pieces":["F","I","N","T","U","V","W","Y","Z"]},{"idx":89,"pieces":["F","I","N","T","U","V","X","Y","Z"]},{"idx":90,"pieces":["F","I","P","T","U","V","W","X","Y"]},{"idx":91,"pieces":["F","I","P","T","U","V","W","Y","Z"]},{"idx":92,"pieces":["F","I","P","T","U","V","X","Y","Z"]},{"idx":93,"pieces":["F","L","N","P","T","U","V","W","X"]},{"idx":94,"pieces":["F","L","N","P","T","U","V","W","Y"]},{"idx":95,"pieces":["F","L","N","P","T","U","V","W","Z"]},{"idx":96,"pieces":["F","L","N","P","T","U","V","X","Y"]},{"idx":97,"pieces":["F","L","N","P","T","U","V","X","Z"]},{"idx":98,"pieces":["F","L","N","P","T","U","V","Y","Z"]},{"idx":99,"pieces":["F","L","N","P","T","U","W","X","Y"]},{"idx":100,"pieces":["F","L","N","P","T","U","W","Y","Z"]},{"idx":101,"pieces":["F","L","N","P","T","U","X","Y","Z"]},{"idx":102,"pieces":["F","L","N","P","T","V","W","X","Y"]},{"idx":103,"pieces":["F","L","N","P","T","V","W","X","Z"]},{"idx":104,"pieces":["F","L","N","P","T","V","W","Y","Z"]},{"idx":105,"pieces":["F","L","N","P","T","V","X","Y","Z"]},{"idx":106,"pieces":["F","L","N","P","T","W","X","Y","Z"]},{"idx":107,"pieces":["F","L","N","P","U","V","W","X","Y"]},{"idx":108,"pieces":["F","L","N","P","U","V","W","Y","Z"]},{"idx":109,"pieces":["F","L","N","P","U","V","X","Y","Z"]},{"idx":110,"pieces":["F","L","N","P","U","W","X","Y","Z"]},{"idx":111,"pieces":["F","L","N","P","V","W","X","Y","Z"]},{"idx":112,"pieces":["F","L","N","T","U","V","W","X","Y"]},{"idx":113,"pieces":["F","L","N","T","U","V","W","Y","Z"]},{"idx":114,"pieces":["F","L","N","T","U","V","X","Y","Z"]},{"idx":115,"pieces":["F","L","N","T","U","W","X","Y","Z"]},{"idx":116,"pieces":["F","L","N","U","V","W","X","Y","Z"]},{"idx":117,"pieces":["F","L","P","T","U","V","W","X","Y"]},{"idx":118,"pieces":["F","L","P","T","U","V","W","X","Z"]},{"idx":119,"pieces":["F","L","P","T","U","V","W","Y","Z"]},{"idx":120,"pieces":["F","L","P","T","U","V","X","Y","Z"]},{"idx":121,"pieces":["F","L","P","T","V","W","X","Y","Z"]},{"idx":122,"pieces":["F","L","P","U","V","W","X","Y","Z"]},{"idx":123,"pieces":["F","N","P","T","U","V","W","Y","Z"]},{"idx":124,"pieces":["F","N","P","T","U","V","X","Y","Z"]},{"idx":125,"pieces":["F","N","P","T","V","W","X","Y","Z"]},{"idx":126,"pieces":["F","P","T","U","V","W","X","Y","Z"]},{"idx":127,"pieces":["I","L","N","P","T","U","V","W","X"]},{"idx":128,"pieces":["I","L","N","P","T","U","V","W","Y"]},{"idx":129,"pieces":["I","L","N","P","T","U","V","W","Z"]},{"idx":130,"pieces":["I","L","N","P","T","U","V","X","Y"]},{"idx":131,"pieces":["I","L","N","P","T","U","V","X","Z"]},{"idx":132,"pieces":["I","L","N","P","T","U","V","Y","Z"]},{"idx":133,"pieces":["I","L","N","P","T","U","W","X","Y"]},{"idx":134,"pieces":["I","L","N","P","T","U","W","X","Z"]},{"idx":135,"pieces":["I","L","N","P","T","U","W","Y","Z"]},{"idx":136,"pieces":["I","L","N","P","T","U","X","Y","Z"]},{"idx":137,"pieces":["I","L","N","P","T","V","W","X","Y"]},{"idx":138,"pieces":["I","L","N","P","T","V","W","Y","Z"]},{"idx":139,"pieces":["I","L","N","P","T","V","X","Y","Z"]},{"idx":140,"pieces":["I","L","N","P","U","V","W","X","Y"]},{"idx":141,"pieces":["I","L","N","P","U","V","W","X","Z"]},{"idx":142,"pieces":["I","L","N","P","U","V","W","Y","Z"]},{"idx":143,"pieces":["I","L","N","P","U","V","X","Y","Z"]},{"idx":144,"pieces":["I","L","N","P","U","W","X","Y","Z"]},{"idx":145,"pieces":["I","L","N","P","V","W","X","Y","Z"]},{"idx":146,"pieces":["I","L","N","T","U","V","W","X","Y"]},{"idx":147,"pieces":["I","L","N","T","U","V","W","X","Z"]},{"idx":148,"pieces":["I","L","N","T","U","V","W","Y","Z"]},{"idx":149,"pieces":["I","L","N","T","U","V","X","Y","Z"]},{"idx":150,"pieces":["I","L","N","T","U","W","X","Y","Z"]},{"idx":151,"pieces":["I","L","N","T","V","W","X","Y","Z"]},{"idx":152,"pieces":["I","L","N","U","V","W","X","Y","Z"]},{"idx":153,"pieces":["I","L","P","T","U","V","W","X","Y"]},{"idx":154,"pieces":["I","L","P","T","U","V","W","X","Z"]},{"idx":155,"pieces":["I","L","P","T","U","V","W","Y","Z"]},{"idx":156,"pieces":["I","L","P","T","U","V","X","Y","Z"]},{"idx":157,"pieces":["I","L","P","T","U","W","X","Y","Z"]},{"idx":158,"pieces":["I","L","P","T","V","W","X","Y","Z"]},{"idx":159,"pieces":["I","L","P","U","V","W","X","Y","Z"]},{"idx":160,"pieces":["I","L","T","U","V","W","X","Y","Z"]},{"idx":161,"pieces":["I","N","P","T","U","V","W","X","Y"]},{"idx":162,"pieces":["I","N","P","T","U","V","W","Y","Z"]},{"idx":163,"pieces":["I","N","P","T","U","V","X","Y","Z"]},{"idx":164,"pieces":["I","N","P","T","V","W","X","Y","Z"]},{"idx":165,"pieces":["I","N","P","U","V","W","X","Y","Z"]},{"idx":166,"pieces":["I","P","T","U","V","W","X","Y","Z"]},{"idx":167,"pieces":["L","N","P","T","U","V","W","X","Y"]},{"idx":168,"pieces":["L","N","P","T","U","V","W","X","Z"]},{"idx":169,"pieces":["L","N","P","T","U","V","W","Y","Z"]},{"idx":170,"pieces":["L","N","P","T","U","V","X","Y","Z"]},{"idx":171,"pieces":["L","N","P","T","U","W","X","Y","Z"]},{"idx":172,"pieces":["L","N","P","T","V","W","X","Y","Z"]},{"idx":173,"pieces":["L","N","P","U","V","W","X","Y","Z"]},{"idx":174,"pieces":["L","P","T","U","V","W","X","Y","Z"]}],"10":[{"idx":0,"pieces":["F","I","L","N","P","T","U","V","W","X"]},{"idx":1,"pieces":["F","I","L","N","P","T","U","V","W","Y"]},{"idx":2,"pieces":["F","I","L","N","P","T","U","V","W","Z"]},{"idx":3,"pieces":["F","I","L","N","P","T","U","V","X","Y"]},{"idx":4,"pieces":["F","I","L","N","P","T","U","V","X","Z"]},{"idx":5,"pieces":["F","I","L","N","P","T","U","V","Y","Z"]},{"idx":6,"pieces":["F","I","L","N","P","T","U","W","X","Y"]},{"idx":7,"pieces":["F","I","L","N","P","T","U","W","X","Z"]},{"idx":8,"pieces":["F","I","L","N","P","T","U","W","Y","Z"]},{"idx":9,"pieces":["F","I","L","N","P","T","U","X","Y","Z"]},{"idx":10,"pieces":["F","I","L","N","P","T","V","W","X","Y"]},{"idx":11,"pieces":["F","I","L","N","P","T","V","W","X","Z"]},{"idx":12,"pieces":["F","I","L","N","P","T","V","W","Y","Z"]},{"idx":13,"pieces":["F","I","L","N","P","T","V","X","Y","Z"]},{"idx":14,"pieces":["F","I","L","N","P","T","W","X","Y","Z"]},{"idx":15,"pieces":["F","I","L","N","P","U","V","W","X","Y"]},{"idx":16,"pieces":["F","I","L","N","P","U","V","W","X","Z"]},{"idx":17,"pieces":["F","I","L","N","P","U","V","W","Y","Z"]},{"idx":18,"pieces":["F","I","L","N","P","U","V","X","Y","Z"]},{"idx":19,"pieces":["F","I","L","N","P","U","W","X","Y","Z"]},{"idx":20,"pieces":["F","I","L","N","P","V","W","X","Y","Z"]},{"idx":21,"pieces":["F","I","L","N","T","U","V","W","X","Y"]},{"idx":22,"pieces":["F","I","L","N","T","U","V","W","X","Z"]},{"idx":23,"pieces":["F","I","L","N","T","U","V","W","Y","Z"]},{"idx":24,"pieces":["F","I","L","N","T","U","V","X","Y","Z"]},{"idx":25,"pieces":["F","I","L","N","T","U","W","X","Y","Z"]},{"idx":26,"pieces":["F","I","L","N","T","V","W","X","Y","Z"]},{"idx":27,"pieces":["F","I","L","N","U","V","W","X","Y","Z"]},{"idx":28,"pieces":["F","I","L","P","T","U","V","W","X","Y"]},{"idx":29,"pieces":["F","I","L","P","T","U","V","W","X","Z"]},{"idx":30,"pieces":["F","I","L","P","T","U","V","W","Y","Z"]},{"idx":31,"pieces":["F","I","L","P","T","U","V","X","Y","Z"]},{"idx":32,"pieces":["F","I","L","P","T","U","W","X","Y","Z"]},{"idx":33,"pieces":["F","I","L","P","T","V","W","X","Y","Z"]},{"idx":34,"pieces":["F","I","L","P","U","V","W","X","Y","Z"]},{"idx":35,"pieces":["F","I","L","T","U","V","W","X","Y","Z"]},{"idx":36,"pieces":["F","I","N","P","T","U","V","W","X","Y"]},{"idx":37,"pieces":["F","I","N","P","T","U","V","W","X","Z"]},{"idx":38,"pieces":["F","I","N","P","T","U","V","W","Y","Z"]},{"idx":39,"pieces":["F","I","N","P","T","U","V","X","Y","Z"]},{"idx":40,"pieces":["F","I","N","P","T","U","W","X","Y","Z"]},{"idx":41,"pieces":["F","I","N","P","T","V","W","X","Y","Z"]},{"idx":42,"pieces":["F","I","N","P","U","V","W","X","Y","Z"]},{"idx":43,"pieces":["F","I","N","T","U","V","W","X","Y","Z"]},{"idx":44,"pieces":["F","I","P","T","U","V","W","X","Y","Z"]},{"idx":45,"pieces":["F","L","N","P","T","U","V","W","X","Y"]},{"idx":46,"pieces":["F","L","N","P","T","U","V","W","X","Z"]},{"idx":47,"pieces":["F","L","N","P","T","U","V","W","Y","Z"]},{"idx":48,"pieces":["F","L","N","P","T","U","V","X","Y","Z"]},{"idx":49,"pieces":["F","L","N","P","T","U","W","X","Y","Z"]},{"idx":50,"pieces":["F","L","N","P","T","V","W","X","Y","Z"]},{"idx":51,"pieces":["F","L","N","P","U","V","W","X","Y","Z"]},{"idx":52,"pieces":["F","L","N","T","U","V","W","X","Y","Z"]},{"idx":53,"pieces":["F","L","P","T","U","V","W","X","Y","Z"]},{"idx":54,"pieces":["F","N","P","T","U","V","W","X","Y","Z"]},{"idx":55,"pieces":["I","L","N","P","T","U","V","W","X","Y"]},{"idx":56,"pieces":["I","L","N","P","T","U","V","W","X","Z"]},{"idx":57,"pieces":["I","L","N","P","T","U","V","W","Y","Z"]},{"idx":58,"pieces":["I","L","N","P","T","U","V","X","Y","Z"]},{"idx":59,"pieces":["I","L","N","P","T","U","W","X","Y","Z"]},{"idx":60,"pieces":["I","L","N","P","T","V","W","X","Y","Z"]},{"idx":61,"pieces":["I","L","N","P","U","V","W","X","Y","Z"]},{"idx":62,"pieces":["I","L","P","T","U","V","W","X","Y","Z"]},{"idx":63,"pieces":["I","N","P","T","U","V","W","X","Y","Z"]},{"idx":64,"pieces":["L","N","P","T","U","V","W","X","Y","Z"]}],"11":[{"idx":0,"pieces":["F","I","L","N","P","T","U","V","W","X","Y"]},{"idx":1,"pieces":["F","I","L","N","P","T","U","V","W","X","Z"]},{"idx":2,"pieces":["F","I","L","N","P","T","U","V","W","Y","Z"]},{"idx":3,"pieces":["F","I","L","N","P","T","U","V","X","Y","Z"]},{"idx":4,"pieces":["F","I","L","N","P","T","U","W","X","Y","Z"]},{"idx":5,"pieces":["F","I","L","N","P","T","V","W","X","Y","Z"]},{"idx":6,"pieces":["F","I","L","N","P","U","V","W","X","Y","Z"]},{"idx":7,"pieces":["F","I","L","N","T","U","V","W","X","Y","Z"]},{"idx":8,"pieces":["F","I","L","P","T","U","V","W","X","Y","Z"]},{"idx":9,"pieces":["F","I","N","P","T","U","V","W","X","Y","Z"]},{"idx":10,"pieces":["F","L","N","P","T","U","V","W","X","Y","Z"]},{"idx":11,"pieces":["I","L","N","P","T","U","V","W","X","Y","Z"]}],"12":[{"idx":0,"pieces":["F","I","L","N","P","T","U","V","W","X","Y","Z"]}]}} \ No newline at end of file diff --git a/public/src/games/geniussquare/GeniusSquareGame.js b/public/src/games/geniussquare/GeniusSquareGame.js index b9cbc92..f7978de 100644 --- a/public/src/games/geniussquare/GeniusSquareGame.js +++ b/public/src/games/geniussquare/GeniusSquareGame.js @@ -46,6 +46,20 @@ const GRID_LINE = 0x252540; const D = { bg: -2, grid: 0, piece: 2, tray: 10, ui: 20, overlay: 60, overlayUI: 62 }; +// Returns the cell [dr, dc] in oriCells closest to its centroid. +// Used as the "pin" so the piece stays centered under the mouse on rotate/flip. +function _centerPin(cells) { + const n = cells.length; + const cr = cells.reduce((s, [r]) => s + r, 0) / n; + const cc = cells.reduce((s, [, c]) => s + c, 0) / n; + let best = cells[0], dist = Infinity; + for (const cell of cells) { + const d = (cell[0] - cr) ** 2 + (cell[1] - cc) ** 2; + if (d < dist) { dist = d; best = cell; } + } + return best; +} + export default class GeniusSquareGame extends Phaser.Scene { constructor() { super('GeniusSquareGame'); } @@ -374,7 +388,8 @@ export default class GeniusSquareGame extends Phaser.Scene { _updateGhostAt(r, c) { if (r < 0 || c < 0 || !this.selectedPiece) { this.ghostCells = null; return; } const oriCells = ORIENTATIONS[this.selectedPiece.pieceId][this.selectedPiece.oriIdx]; - this.ghostCells = absoluteCells(oriCells, r, c); + const [pinDr, pinDc] = _centerPin(oriCells); + this.ghostCells = absoluteCells(oriCells, r - pinDr, c - pinDc); } _onGridHover(ptr) { @@ -435,8 +450,9 @@ export default class GeniusSquareGame extends Phaser.Scene { const anchorR = cells[0][0] - oriCells[0][0]; const anchorC = cells[0][1] - oriCells[0][1]; this.ghostCells = absoluteCells(oriCells, anchorR, anchorC); - this._lastHoverR = anchorR; - this._lastHoverC = anchorC; + const [pinDr, pinDc] = _centerPin(oriCells); + this._lastHoverR = anchorR + pinDr; + this._lastHoverC = anchorC + pinDc; playSound(this, SFX.PIECE_CLICK); this._renderHumanGrid(); diff --git a/public/src/games/katamino/KataminoGame.js b/public/src/games/katamino/KataminoGame.js new file mode 100644 index 0000000..2c49d8c --- /dev/null +++ b/public/src/games/katamino/KataminoGame.js @@ -0,0 +1,703 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; +import { Button } from '../../ui/Button.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; +import { + ROWS, PIECES, PIECE_COLORS, ORIENTATIONS, + absoluteCells, newBoard, canPlace, placePiece, removePiece, isSolved, + rotateOri, flipOri, +} from './KataminoLogic.js'; + +// ── Layout ──────────────────────────────────────────────────────────────────── +const CELL = 110; // board cell size (px) +const MINI = 22; // tray mini-piece cell size (px) +const SLOT_H = 130; // tray slot height (enough for 5-tall piece + padding) + +const BOARD_Y = 90; +const TRAY_GAP = 20; + +// Set-select card dimensions +const CARD_W = 160; +const CARD_H = 110; +const CARD_GAP = 12; +const COLS_PER_ROW = 10; +const ROW_H = CARD_H + CARD_GAP; +const CARDS_MARGIN = (GAME_WIDTH - (COLS_PER_ROW * CARD_W + (COLS_PER_ROW - 1) * CARD_GAP)) / 2; + +// Scroll area in set-select screen +const SCROLL_AREA_Y = 120; +const SCROLL_AREA_H = GAME_HEIGHT - SCROLL_AREA_Y - 60; + +// ── Colors ──────────────────────────────────────────────────────────────────── +const BOARD_BG = 0x3d2208; +const BOARD_LINE = 0x7a4c1a; +const SLOT_BG = COLORS.panel; +const SLOT_SEL = 0x2a1f0a; + +const D = { bg: -2, board: 0, piece: 2, tray: 8, ui: 20, overlay: 60, overlayUI: 62 }; + +// Returns the cell [dr, dc] in oriCells closest to its centroid. +// Used as the "pin" so the piece stays centered under the mouse on rotate/flip. +function _centerPin(cells) { + const n = cells.length; + const cr = cells.reduce((s, [r]) => s + r, 0) / n; + const cc = cells.reduce((s, [, c]) => s + c, 0) / n; + let best = cells[0], dist = Infinity; + for (const cell of cells) { + const d = (cell[0] - cr) ** 2 + (cell[1] - cc) ** 2; + if (d < dist) { dist = d; best = cell; } + } + return best; +} + +export default class KataminoGame extends Phaser.Scene { + constructor() { super('KataminoGame'); } + + init(data) { + this.gameDef = data.game; + this._bank = null; + this._screen = null; // 'penta' | 'set' | 'play' + this._penta = null; + this._puzzleIdx = null; + this._pieces = null; + + // Play state + this._cols = null; + this._boardX = null; + this._board = null; + this._placedPieces = new Set(); + this._selectedPiece = null; // { pieceId, oriIdx } + this._ghostCells = null; + this._lastHoverR = -1; + this._lastHoverC = -1; + + // Rendering + this._screenContainer = null; + this._maskGfx = null; + this._boardGfx = null; + this._traySlots = []; + + // Input handler refs for cleanup + this._inputHandlers = []; + } + + create() { + this._bank = this.cache.json.get('katamino'); + + this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x0f0d0a).setDepth(D.bg); + + // Persistent keyboard handlers — check this._screen before acting + this.input.keyboard.on('keydown-R', () => this._rotateSelected()); + this.input.keyboard.on('keydown-F', () => this._flipSelected()); + this.input.keyboard.on('keydown-ESCAPE', () => { if (this._screen === 'play') this._clearSelection(); }); + + this._showPentaSelect(); + } + + // ── Screen management ────────────────────────────────────────────────────── + + _clearScreen() { + if (this._screenContainer) { this._screenContainer.destroy(true); this._screenContainer = null; } + if (this._maskGfx) { this._maskGfx.destroy(); this._maskGfx = null; } + for (const { ev, fn } of this._inputHandlers) this.input.off(ev, fn); + this._inputHandlers = []; + + // Reset play state + this._board = null; + this._cols = null; + this._placedPieces.clear(); + this._selectedPiece = null; + this._ghostCells = null; + this._lastHoverR = -1; + this._lastHoverC = -1; + this._boardGfx = null; + this._traySlots = []; + } + + _addInputHandler(ev, fn) { + this._inputHandlers.push({ ev, fn }); + this.input.on(ev, fn); + } + + // ── Progress helpers ─────────────────────────────────────────────────────── + + _isDone(penta, idx) { return localStorage.getItem(`katamino-done-${penta}-${idx}`) === '1'; } + _markDone(penta, idx) { localStorage.setItem(`katamino-done-${penta}-${idx}`, '1'); } + _countDone(penta) { + const puzzles = this._bank?.pentas?.[String(penta)] ?? []; + return puzzles.filter(p => this._isDone(penta, p.idx)).length; + } + + // ── Screen 1: Penta Select ───────────────────────────────────────────────── + + _showPentaSelect() { + this._clearScreen(); + this._screen = 'penta'; + const sc = this._screenContainer = this.add.container(0, 0).setDepth(D.ui); + + const title = this.add.text(GAME_WIDTH / 2, 55, 'KATAMINO', { + fontFamily: '"Julius Sans One"', fontSize: '72px', color: COLORS.textHex, + }).setOrigin(0.5); + const sub = this.add.text(GAME_WIDTH / 2, 135, 'The Pentomino Challenge', { + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex, + }).setOrigin(0.5); + sc.add([title, sub]); + + const TILE_W = 230, TILE_H = 200, TILE_GAP = 20; + const rowLeft = (GAME_WIDTH - (5 * TILE_W + 4 * TILE_GAP)) / 2; + + [[3,4,5,6,7], [8,9,10,11,12]].forEach((row, rowIdx) => { + row.forEach((penta, col) => { + const cx = rowLeft + col * (TILE_W + TILE_GAP) + TILE_W / 2; + const cy = 210 + rowIdx * 230 + TILE_H / 2; + this._buildPentaTile(sc, cx, cy, TILE_W, TILE_H, penta); + }); + }); + + const hint = this.add.text(GAME_WIDTH / 2, 720, 'Select a Penta to choose a puzzle', { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex, + }).setOrigin(0.5); + sc.add(hint); + + const leave = new Button(this, GAME_WIDTH - 150, GAME_HEIGHT - 50, 'Leave', + () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 160, height: 48, fontSize: 18 } + ).setDepth(D.ui + 1); + sc.add(leave); + } + + _buildPentaTile(sc, cx, cy, w, h, penta) { + const total = (this._bank?.pentas?.[String(penta)] ?? []).length; + const done = this._countDone(penta); + const allDone = total > 0 && done === total; + + const gfx = this.add.graphics(); + const hov = this.add.graphics(); hov.setVisible(false); + + const draw = (isHover) => { + const target = isHover ? hov : gfx; + target.clear(); + target.fillStyle(isHover ? 0x3d2d0a : 0x2a1f0a, 1); + target.fillRoundedRect(cx - w/2, cy - h/2, w, h, 14); + target.lineStyle(2, isHover ? COLORS.gold : (allDone ? 0xd4a017 : 0x6a4a1a), 1); + target.strokeRoundedRect(cx - w/2, cy - h/2, w, h, 14); + }; + draw(false); + hov.lineStyle(2, COLORS.gold, 1); + hov.strokeRoundedRect(cx - w/2 + 1, cy - h/2 + 1, w - 2, h - 2, 14); + + const numText = this.add.text(cx, cy - 32, String(penta), { + fontFamily: '"Julius Sans One"', fontSize: '72px', + color: allDone ? COLORS.goldHex : COLORS.textHex, + }).setOrigin(0.5, 0.5); + + const sizeText = this.add.text(cx, cy + 36, `5 × ${penta} grid`, { + fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, + }).setOrigin(0.5); + + const doneText = this.add.text(cx, cy + 62, `${done} / ${total}`, { + fontFamily: '"Julius Sans One"', fontSize: '18px', + color: allDone ? COLORS.goldHex : COLORS.mutedHex, + }).setOrigin(0.5); + + const zone = this.add.zone(cx, cy, w - 4, h - 4).setInteractive({ useHandCursor: true }); + zone.on('pointerover', () => hov.setVisible(true)); + zone.on('pointerout', () => hov.setVisible(false)); + zone.on('pointerdown', () => this._showSetSelect(penta)); + + sc.add([gfx, hov, numText, sizeText, doneText, zone]); + } + + // ── Screen 2: Set Select ─────────────────────────────────────────────────── + + _showSetSelect(penta) { + this._clearScreen(); + this._screen = 'set'; + this._penta = penta; + const sc = this._screenContainer = this.add.container(0, 0).setDepth(D.ui); + + const puzzles = this._bank?.pentas?.[String(penta)] ?? []; + const total = puzzles.length; + const done = this._countDone(penta); + + // Header (fixed, not scrolled) + const backBtn = new Button(this, 90, 50, '← Back', + () => this._showPentaSelect(), + { variant: 'ghost', width: 140, height: 44, fontSize: 16 } + ).setDepth(D.ui + 1); + const titleTxt = this.add.text(GAME_WIDTH / 2, 35, `Penta ${penta}`, { + fontFamily: '"Julius Sans One"', fontSize: '36px', color: COLORS.textHex, + }).setOrigin(0.5); + const statsTxt = this.add.text(GAME_WIDTH / 2, 78, `${done} / ${total} completed`, { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex, + }).setOrigin(0.5); + sc.add([backBtn, titleTxt, statsTxt]); + + const leave = new Button(this, GAME_WIDTH - 150, GAME_HEIGHT - 50, 'Leave', + () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 160, height: 48, fontSize: 18 } + ).setDepth(D.ui + 1); + sc.add(leave); + + // Scrollable card area + const scrollContainer = this.add.container(0, SCROLL_AREA_Y); + sc.add(scrollContainer); + + // Geometry mask to clip the scroll area + this._maskGfx = this.make.graphics({ add: false }); + this._maskGfx.fillStyle(0xffffff); + this._maskGfx.fillRect(0, SCROLL_AREA_Y, GAME_WIDTH, SCROLL_AREA_H); + scrollContainer.setMask(this._maskGfx.createGeometryMask()); + + // Build all cards at local positions (local y=0 is top of scroll area) + for (let i = 0; i < puzzles.length; i++) { + const col = i % COLS_PER_ROW; + const row = Math.floor(i / COLS_PER_ROW); + const cx = CARDS_MARGIN + col * (CARD_W + CARD_GAP) + CARD_W / 2; + const cy = row * ROW_H + CARD_H / 2; + this._buildPuzzleCard(scrollContainer, cx, cy, penta, puzzles[i]); + } + + const rows = Math.ceil(puzzles.length / COLS_PER_ROW); + const contentH = rows * ROW_H; + const maxScroll = Math.max(0, contentH - SCROLL_AREA_H); + let scrollY = 0; + + const doScroll = (dy) => { + scrollY = Phaser.Math.Clamp(scrollY + dy, 0, maxScroll); + scrollContainer.y = SCROLL_AREA_Y - scrollY; + }; + + this._addInputHandler('wheel', (ptr, _o, _dx, dy) => { + if (ptr.y >= SCROLL_AREA_Y && ptr.y <= SCROLL_AREA_Y + SCROLL_AREA_H) doScroll(dy * 0.5); + }); + + let dragOriginY = 0, dragOriginScroll = 0, dragging = false; + this._addInputHandler('pointerdown', (ptr) => { + if (ptr.y >= SCROLL_AREA_Y && ptr.y <= SCROLL_AREA_Y + SCROLL_AREA_H) { + dragging = true; dragOriginY = ptr.y; dragOriginScroll = scrollY; + } + }); + this._addInputHandler('pointermove', (ptr) => { + if (!dragging || !ptr.isDown) return; + doScroll(dragOriginScroll - (ptr.y - dragOriginY) - scrollY); + }); + this._addInputHandler('pointerup', () => { dragging = false; }); + } + + _buildPuzzleCard(sc, cx, cy, penta, puzzleData) { + const done = this._isDone(penta, puzzleData.idx); + const pieces = puzzleData.pieces; + + const gfx = this.add.graphics(); + gfx.fillStyle(done ? 0x141e10 : COLORS.panel, 1); + gfx.fillRoundedRect(cx - CARD_W/2, cy - CARD_H/2, CARD_W, CARD_H, 8); + gfx.lineStyle(2, done ? 0x3a8040 : 0x3a3020, 1); + gfx.strokeRoundedRect(cx - CARD_W/2, cy - CARD_H/2, CARD_W, CARD_H, 8); + + // Piece color chips + const CHIP_W = 18, CHIP_H = 12, CHIP_GAP = 4, CHIPS_PER_ROW = 6; + const chipLeft = cx - CARD_W/2 + 10; + const chipTop = cy - CARD_H/2 + 34; + for (let i = 0; i < pieces.length; i++) { + const col = i % CHIPS_PER_ROW; + const row = Math.floor(i / CHIPS_PER_ROW); + gfx.fillStyle(PIECE_COLORS[pieces[i]], 1); + gfx.fillRoundedRect( + chipLeft + col * (CHIP_W + CHIP_GAP), + chipTop + row * (CHIP_H + CHIP_GAP), + CHIP_W, CHIP_H, 3 + ); + } + + // Done checkmark + if (done) { + gfx.fillStyle(0x3a8040, 0.25); + gfx.fillRoundedRect(cx - CARD_W/2, cy - CARD_H/2, CARD_W, CARD_H, 8); + gfx.fillStyle(0x44aa55, 1); + gfx.fillCircle(cx + CARD_W/2 - 14, cy - CARD_H/2 + 14, 9); + gfx.lineStyle(2, 0xffffff, 1); + gfx.beginPath(); + gfx.moveTo(cx + CARD_W/2 - 20, cy - CARD_H/2 + 14); + gfx.lineTo(cx + CARD_W/2 - 14, cy - CARD_H/2 + 20); + gfx.lineTo(cx + CARD_W/2 - 7, cy - CARD_H/2 + 8); + gfx.strokePath(); + } + + const numTxt = this.add.text(cx - CARD_W/2 + 8, cy - CARD_H/2 + 8, `#${puzzleData.idx + 1}`, { + fontFamily: '"Julius Sans One"', fontSize: '11px', + color: done ? '#44aa55' : COLORS.mutedHex, + }).setOrigin(0, 0); + + // Hover highlight + const hov = this.add.graphics(); hov.setVisible(false); + hov.lineStyle(2, COLORS.accent, 1); + hov.strokeRoundedRect(cx - CARD_W/2 + 1, cy - CARD_H/2 + 1, CARD_W - 2, CARD_H - 2, 8); + + const zone = this.add.zone(cx, cy, CARD_W - 2, CARD_H - 2).setInteractive({ useHandCursor: true }); + zone.on('pointerover', () => hov.setVisible(true)); + zone.on('pointerout', () => hov.setVisible(false)); + zone.on('pointerdown', (ptr) => { + if (ptr.y < SCROLL_AREA_Y || ptr.y > SCROLL_AREA_Y + SCROLL_AREA_H) return; + this._showPlay(this._penta, puzzleData.idx, puzzleData.pieces); + }); + + sc.add([gfx, numTxt, hov, zone]); + } + + // ── Screen 3: Play ───────────────────────────────────────────────────────── + + _showPlay(penta, puzzleIdx, pieces) { + this._clearScreen(); + this._screen = 'play'; + this._penta = penta; + this._puzzleIdx = puzzleIdx; + this._pieces = pieces.slice(); + this._cols = penta; + this._board = newBoard(penta); + this._placedPieces.clear(); + + const boardW = penta * CELL; + this._boardX = (GAME_WIDTH - boardW) / 2; + + const sc = this._screenContainer = this.add.container(0, 0).setDepth(D.board); + + // Title bar + const backBtn = new Button(this, 90, 46, '← Back', + () => this._showSetSelect(penta), + { variant: 'ghost', width: 140, height: 44, fontSize: 16 } + ).setDepth(D.ui + 1); + const titleTxt = this.add.text(GAME_WIDTH / 2, 35, `Penta ${penta} — Puzzle #${puzzleIdx + 1}`, { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, + }).setOrigin(0.5); + sc.add([backBtn, titleTxt]); + + // Reset and Leave buttons + const resetBtn = new Button(this, GAME_WIDTH - 310, 46, 'Reset', + () => this._resetBoard(), + { variant: 'ghost', width: 140, height: 44, fontSize: 18 } + ).setDepth(D.ui + 1); + const leaveBtn = new Button(this, GAME_WIDTH - 150, 46, 'Leave', + () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 140, height: 44, fontSize: 18 } + ).setDepth(D.ui + 1); + sc.add([resetBtn, leaveBtn]); + + // Board graphics + this._boardGfx = this.add.graphics().setDepth(D.board); + sc.add(this._boardGfx); + + // Board input zone (use setOrigin(0) so x,y = top-left) + const zone = this.add.zone(this._boardX, BOARD_Y, boardW, ROWS * CELL) + .setOrigin(0) + .setInteractive({ useHandCursor: true }) + .setDepth(D.piece); + zone.on('pointermove', ptr => this._onBoardHover(ptr)); + zone.on('pointerdown', ptr => this._onBoardClick(ptr)); + zone.on('pointerout', () => { this._ghostCells = null; this._renderBoard(); }); + sc.add(zone); + + // Piece tray + this._buildTray(sc, pieces); + + // Keyboard hints + const trayY = BOARD_Y + ROWS * CELL + TRAY_GAP; + const hintY = trayY + SLOT_H + 16; + const hint = this.add.text(GAME_WIDTH / 2, hintY, 'R = Rotate F = Flip ESC = Deselect', { + fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, + }).setOrigin(0.5); + sc.add(hint); + + this._renderBoard(); + this._renderTray(); + } + + _buildTray(sc, pieces) { + const trayY = BOARD_Y + ROWS * CELL + TRAY_GAP; + + for (let i = 0; i < pieces.length; i++) { + const pieceId = pieces[i]; + const cx = this._boardX + i * CELL + CELL / 2; + const cy = trayY + SLOT_H / 2; + + const bg = this.add.graphics(); + const pieceGfx = this.add.graphics(); + + const container = this.add.container(cx, cy, [bg, pieceGfx]); + container.setSize(CELL - 6, SLOT_H - 6); + container.setInteractive({ useHandCursor: true }); + container.on('pointerdown', () => this._selectPiece(pieceId)); + container.setDepth(D.tray); + + this._traySlots.push({ container, bg, pieceGfx, pieceId }); + sc.add(container); + } + } + + _drawSlotBg(gfx, selected) { + gfx.clear(); + gfx.fillStyle(selected ? SLOT_SEL : SLOT_BG, 1); + gfx.fillRoundedRect(-CELL/2 + 4, -SLOT_H/2 + 4, CELL - 8, SLOT_H - 8, 6); + gfx.lineStyle(2, selected ? COLORS.accent : BOARD_LINE, selected ? 1 : 0.5); + gfx.strokeRoundedRect(-CELL/2 + 4, -SLOT_H/2 + 4, CELL - 8, SLOT_H - 8, 6); + } + + _drawMiniPiece(gfx, pieceId, dimmed) { + gfx.clear(); + const cells = ORIENTATIONS[pieceId][0]; + let maxR = 0, maxC = 0; + for (const [r, c] of cells) { maxR = Math.max(maxR, r); maxC = Math.max(maxC, c); } + const sx = -((maxC + 1) * MINI) / 2; + const sy = -((maxR + 1) * MINI) / 2; + const color = dimmed ? 0x444444 : (PIECE_COLORS[pieceId] ?? 0x888888); + const alpha = dimmed ? 0.35 : 1; + gfx.fillStyle(color, alpha); + for (const [r, c] of cells) { + gfx.fillRoundedRect(sx + c * MINI + 1, sy + r * MINI + 1, MINI - 2, MINI - 2, 3); + } + if (!dimmed) { + gfx.fillStyle(0xffffff, 0.18); + for (const [r, c] of cells) { + if (r === 0) gfx.fillRoundedRect(sx + c * MINI + 2, sy + r * MINI + 2, MINI - 4, 5, 2); + } + } + } + + _renderTray() { + for (const slot of this._traySlots) { + const placed = this._placedPieces.has(slot.pieceId); + const selected = this._selectedPiece?.pieceId === slot.pieceId; + this._drawSlotBg(slot.bg, selected && !placed); + this._drawMiniPiece(slot.pieceGfx, slot.pieceId, placed); + slot.container.setInteractive(placed ? false : { useHandCursor: true }); + } + } + + _renderBoard() { + const gfx = this._boardGfx; + const cols = this._cols; + const bx = this._boardX; + const by = BOARD_Y; + const boardW = cols * CELL; + const boardH = ROWS * CELL; + + gfx.clear(); + + // Board background + gfx.fillStyle(BOARD_BG, 1); + gfx.fillRoundedRect(bx - 4, by - 4, boardW + 8, boardH + 8, 12); + + // Grid lines + gfx.lineStyle(1, BOARD_LINE, 0.6); + for (let r = 0; r <= ROWS; r++) { + gfx.beginPath(); gfx.moveTo(bx, by + r*CELL); gfx.lineTo(bx + boardW, by + r*CELL); gfx.strokePath(); + } + for (let c = 0; c <= cols; c++) { + gfx.beginPath(); gfx.moveTo(bx + c*CELL, by); gfx.lineTo(bx + c*CELL, by + boardH); gfx.strokePath(); + } + + // Placed pieces + for (let idx = 0; idx < this._board.length; idx++) { + const val = this._board[idx]; + if (val === null) continue; + const r = (idx / cols) | 0, c = idx % cols; + const px = bx + c * CELL, py = by + r * CELL; + const color = PIECE_COLORS[val] ?? 0x888888; + gfx.fillStyle(color, 1); + gfx.fillRoundedRect(px + 3, py + 3, CELL - 6, CELL - 6, 9); + gfx.fillStyle(0xffffff, 0.15); + gfx.fillRoundedRect(px + 5, py + 5, CELL - 10, 10, 3); + gfx.fillStyle(0x000000, 0.1); + gfx.fillRoundedRect(px + 3, py + CELL - 14, CELL - 6, 11, 3); + } + + // Ghost preview + if (this._ghostCells && this._selectedPiece) { + const pid = this._selectedPiece.pieceId; + const clr = PIECE_COLORS[pid] ?? 0xffffff; + const ok = this._ghostCells.every(([r, c]) => + r >= 0 && r < ROWS && c >= 0 && c < cols && this._board[r * cols + c] === null + ); + gfx.fillStyle(clr, ok ? 0.55 : 0.2); + for (const [r, c] of this._ghostCells) { + if (r < 0 || r >= ROWS || c < 0 || c >= cols) continue; + gfx.fillRoundedRect(bx + c*CELL + 3, by + r*CELL + 3, CELL - 6, CELL - 6, 9); + } + } + } + + // ── Piece selection & placement ──────────────────────────────────────────── + + _selectPiece(pieceId) { + if (this._screen !== 'play' || this._placedPieces.has(pieceId)) return; + if (this._selectedPiece?.pieceId === pieceId) { this._clearSelection(); return; } + this._selectedPiece = { pieceId, oriIdx: 0 }; + this._ghostCells = null; + this._renderTray(); + this._renderBoard(); + } + + _clearSelection() { + this._selectedPiece = null; + this._ghostCells = null; + this._lastHoverR = -1; + this._lastHoverC = -1; + this._renderTray(); + this._renderBoard(); + } + + _rotateSelected() { + if (this._screen !== 'play' || !this._selectedPiece) return; + this._selectedPiece.oriIdx = rotateOri(this._selectedPiece.pieceId, this._selectedPiece.oriIdx); + this._updateGhostAt(this._lastHoverR, this._lastHoverC); + this._renderBoard(); + } + + _flipSelected() { + if (this._screen !== 'play' || !this._selectedPiece) return; + this._selectedPiece.oriIdx = flipOri(this._selectedPiece.pieceId, this._selectedPiece.oriIdx); + this._updateGhostAt(this._lastHoverR, this._lastHoverC); + this._renderBoard(); + } + + _updateGhostAt(r, c) { + if (r < 0 || c < 0 || !this._selectedPiece) { this._ghostCells = null; return; } + const oriCells = ORIENTATIONS[this._selectedPiece.pieceId][this._selectedPiece.oriIdx]; + const [pinDr, pinDc] = _centerPin(oriCells); + this._ghostCells = absoluteCells(oriCells, r - pinDr, c - pinDc); + } + + _onBoardHover(ptr) { + if (!this._selectedPiece || this._screen !== 'play') return; + const r = Math.floor((ptr.worldY - BOARD_Y) / CELL); + const c = Math.floor((ptr.worldX - this._boardX) / CELL); + this._lastHoverR = r; + this._lastHoverC = c; + this._updateGhostAt(r, c); + this._renderBoard(); + } + + _onBoardClick(ptr) { + if (this._screen !== 'play') return; + const r = Math.floor((ptr.worldY - BOARD_Y) / CELL); + const c = Math.floor((ptr.worldX - this._boardX) / CELL); + if (r < 0 || r >= ROWS || c < 0 || c >= this._cols) return; + + const cellVal = this._board[r * this._cols + c]; + if (cellVal !== null) { this._liftPiece(cellVal); return; } + + if (!this._selectedPiece || !this._ghostCells) return; + if (!canPlace(this._board, this._cols, this._ghostCells)) { + playSound(this, SFX.PIECE_CLICK); + return; + } + + const { pieceId } = this._selectedPiece; + this._board = placePiece(this._board, this._cols, this._ghostCells, pieceId); + this._placedPieces.add(pieceId); + playSound(this, SFX.CARD_PLACE); + this._clearSelection(); + this._renderBoard(); + this._renderTray(); + + if (isSolved(this._board)) this._showWinOverlay(); + } + + _liftPiece(pieceId) { + const cells = []; + for (let i = 0; i < this._board.length; i++) { + if (this._board[i] === pieceId) cells.push([(i / this._cols) | 0, i % this._cols]); + } + this._board = removePiece(this._board, this._cols, cells); + this._placedPieces.delete(pieceId); + + const oriIdx = this._matchOrientation(pieceId, cells); + this._selectedPiece = { pieceId, oriIdx }; + const oriCells = ORIENTATIONS[pieceId][oriIdx]; + const anchorR = cells[0][0] - oriCells[0][0]; + const anchorC = cells[0][1] - oriCells[0][1]; + this._ghostCells = absoluteCells(oriCells, anchorR, anchorC); + const [pinDr, pinDc] = _centerPin(oriCells); + this._lastHoverR = anchorR + pinDr; + this._lastHoverC = anchorC + pinDc; + + playSound(this, SFX.PIECE_CLICK); + this._renderBoard(); + this._renderTray(); + } + + _matchOrientation(pieceId, cells) { + let minR = Infinity, minC = Infinity; + for (const [r, c] of cells) { if (r < minR) minR = r; if (c < minC) minC = c; } + const normKey = cells.map(([r, c]) => `${r - minR},${c - minC}`).sort().join(';'); + const oris = ORIENTATIONS[pieceId]; + for (let i = 0; i < oris.length; i++) { + if (oris[i].map(([r, c]) => `${r},${c}`).join(';') === normKey) return i; + } + return 0; + } + + _resetBoard() { + if (this._screen !== 'play') return; + this._board = newBoard(this._cols); + this._placedPieces.clear(); + this._clearSelection(); + } + + // ── Win overlay ──────────────────────────────────────────────────────────── + + _showWinOverlay() { + this._markDone(this._penta, this._puzzleIdx); + + const penta = this._penta; + const puzzleIdx = this._puzzleIdx; + const allPuzzles = this._bank?.pentas?.[String(penta)] ?? []; + + const dim = this.add.rectangle(GAME_WIDTH/2, GAME_HEIGHT/2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.65) + .setDepth(D.overlay); + this._screenContainer.add(dim); + + const panelW = 480, panelH = 360; + const panelX = GAME_WIDTH/2 - panelW/2; + const panelY = GAME_HEIGHT/2 - panelH/2; + + const panel = this.add.graphics().setDepth(D.overlay + 1); + panel.fillStyle(0x1e1a12, 1); + panel.fillRoundedRect(panelX, panelY, panelW, panelH, 20); + panel.lineStyle(2, COLORS.gold, 1); + panel.strokeRoundedRect(panelX, panelY, panelW, panelH, 20); + this._screenContainer.add(panel); + + const solvedTxt = this.add.text(GAME_WIDTH/2, panelY + 52, 'Solved!', { + fontFamily: '"Julius Sans One"', fontSize: '56px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + const subTxt = this.add.text(GAME_WIDTH/2, panelY + 108, `Penta ${penta} — Puzzle #${puzzleIdx + 1}`, { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + this._screenContainer.add([solvedTxt, subTxt]); + + const btnCX = GAME_WIDTH / 2; + const hasNext = puzzleIdx + 1 < allPuzzles.length; + + if (hasNext) { + const next = allPuzzles[puzzleIdx + 1]; + const nextBtn = new Button(this, btnCX, panelY + 170, 'Next Puzzle', + () => this._showPlay(penta, next.idx, next.pieces), + { width: 280, height: 54, fontSize: 22 } + ).setDepth(D.overlayUI + 1); + this._screenContainer.add(nextBtn); + } + + const replayBtn = new Button(this, btnCX, panelY + (hasNext ? 240 : 190), 'Play Again', + () => this._showPlay(penta, puzzleIdx, this._pieces), + { variant: 'ghost', width: 280, height: 50, fontSize: 20 } + ).setDepth(D.overlayUI + 1); + + const backBtn = new Button(this, btnCX, panelY + (hasNext ? 300 : 250), 'Back to Set', + () => this._showSetSelect(penta), + { variant: 'ghost', width: 280, height: 50, fontSize: 20 } + ).setDepth(D.overlayUI + 1); + + this._screenContainer.add([replayBtn, backBtn]); + } +} diff --git a/public/src/games/katamino/KataminoLogic.js b/public/src/games/katamino/KataminoLogic.js new file mode 100644 index 0000000..c4108d8 --- /dev/null +++ b/public/src/games/katamino/KataminoLogic.js @@ -0,0 +1,122 @@ +// Katamino — pure game logic, no Phaser. +// +// Board is a flat array (ROWS × cols, row-major): +// null = empty +// pieceId = piece letter placed there ('F', 'I', 'L', …) + +export const ROWS = 5; + +export const PIECES = [ + { id: 'F', cells: [[0,1],[0,2],[1,0],[1,1],[2,1]] }, + { id: 'I', cells: [[0,0],[1,0],[2,0],[3,0],[4,0]] }, + { id: 'L', cells: [[0,0],[1,0],[2,0],[3,0],[3,1]] }, + { id: 'N', cells: [[0,0],[1,0],[2,0],[2,1],[3,1]] }, + { id: 'P', cells: [[0,0],[0,1],[1,0],[1,1],[2,0]] }, + { id: 'T', cells: [[0,0],[0,1],[0,2],[1,1],[2,1]] }, + { id: 'U', cells: [[0,0],[0,1],[1,0],[2,0],[2,1]] }, + { id: 'V', cells: [[0,0],[1,0],[2,0],[2,1],[2,2]] }, + { id: 'W', cells: [[0,0],[1,0],[1,1],[2,1],[2,2]] }, + { id: 'X', cells: [[0,1],[1,0],[1,1],[1,2],[2,1]] }, + { id: 'Y', cells: [[0,0],[1,0],[1,1],[2,0],[3,0]] }, + { id: 'Z', cells: [[0,0],[0,1],[1,1],[2,1],[2,2]] }, +]; + +export const PIECE_IDS = PIECES.map(p => p.id); + +export const PIECE_COLORS = { + F: 0xff6b9d, + I: 0x45c4f0, + L: 0xff9940, + N: 0xa0e03a, + P: 0x9b6cf5, + T: 0xf7e03c, + U: 0x3ae0c4, + V: 0xff4a4a, + W: 0x4af74a, + X: 0xf74af7, + Y: 0x4a9fff, + Z: 0xf0a020, +}; + +// ── Orientation generation ───────────────────────────────────────────────────── + +function normalize(cells) { + let minR = Infinity, minC = Infinity; + for (const [r, c] of cells) { if (r < minR) minR = r; if (c < minC) minC = c; } + return cells.map(([r, c]) => [r - minR, c - minC]).sort((a, b) => a[0] - b[0] || a[1] - b[1]); +} + +function keyOf(cells) { + return cells.map(([r, c]) => `${r},${c}`).join(';'); +} + +function computeOrientations(cells) { + const seen = new Map(); + const base = cells.map(([r, c]) => [r, c]); + for (let flip = 0; flip < 2; flip++) { + let work = flip ? base.map(([r, c]) => [r, -c]) : base; + for (let rot = 0; rot < 4; rot++) { + const norm = normalize(work); + const k = keyOf(norm); + if (!seen.has(k)) seen.set(k, norm); + work = work.map(([r, c]) => [c, -r]); + } + } + return [...seen.values()]; +} + +export const ORIENTATIONS = Object.fromEntries( + PIECES.map(p => [p.id, computeOrientations(p.cells)]) +); + +const ORI_INDEX = Object.fromEntries( + PIECES.map(p => [p.id, new Map(ORIENTATIONS[p.id].map((o, i) => [keyOf(o), i]))]) +); + +function transformedIndex(pieceId, oriIdx, fn) { + const cur = ORIENTATIONS[pieceId][oriIdx]; + const next = normalize(cur.map(fn)); + return ORI_INDEX[pieceId].get(keyOf(next)) ?? oriIdx; +} + +export function rotateOri(pieceId, oriIdx) { + return transformedIndex(pieceId, oriIdx, ([r, c]) => [c, -r]); +} + +export function flipOri(pieceId, oriIdx) { + return transformedIndex(pieceId, oriIdx, ([r, c]) => [r, -c]); +} + +// ── Board helpers ───────────────────────────────────────────────────────────── + +export function absoluteCells(oriCells, anchorR, anchorC) { + return oriCells.map(([dr, dc]) => [anchorR + dr, anchorC + dc]); +} + +export function newBoard(cols) { + return Array(ROWS * cols).fill(null); +} + +export function canPlace(board, cols, cells) { + for (const [r, c] of cells) { + if (r < 0 || r >= ROWS || c < 0 || c >= cols) return false; + if (board[r * cols + c] !== null) return false; + } + return true; +} + +export function placePiece(board, cols, cells, pieceId) { + const next = board.slice(); + for (const [r, c] of cells) next[r * cols + c] = pieceId; + return next; +} + +export function removePiece(board, cols, cells) { + const next = board.slice(); + for (const [r, c] of cells) next[r * cols + c] = null; + return next; +} + +export function isSolved(board) { + return board.every(cell => cell !== null); +} diff --git a/public/src/main.js b/public/src/main.js index c297bfa..eb340f9 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -81,6 +81,7 @@ import RummikubGame from './games/rummikub/RummikubGame.js'; import GinRummyGame from './games/ginrummy/GinRummyGame.js'; import RiskGame from './games/risk/RiskGame.js'; import GeniusSquareGame from './games/geniussquare/GeniusSquareGame.js'; +import KataminoGame from './games/katamino/KataminoGame.js'; const config = { type: Phaser.AUTO, @@ -175,6 +176,7 @@ const config = { GinRummyGame, RiskGame, GeniusSquareGame, + KataminoGame, ], }; diff --git a/public/src/scenes/GameRoomScene.js b/public/src/scenes/GameRoomScene.js index 548f40f..4b047d1 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', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame' }; + 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', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame', katamino: 'KataminoGame' }; 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 d619f2b..f0914d2 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -70,6 +70,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.json('jewelquest', '/data/jewelquest.json'); this.load.json('zuma', '/data/zuma.json'); this.load.json('dotlink', '/data/dotlink.json'); + this.load.json('katamino', '/data/katamino.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/games/registry.js b/server/games/registry.js index 0823561..505eb11 100644 --- a/server/games/registry.js +++ b/server/games/registry.js @@ -97,3 +97,4 @@ registerGame({ slug: 'rummikub', name: 'Rummikub', category: 'cards', cardGame: registerGame({ slug: 'ginrummy', name: 'Gin Rummy', category: 'cards', cardGame: true, minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, hasTutorial: false, iconFrame: 69 }); registerGame({ slug: 'risk', name: 'Risk', category: 'tabletop', minPlayers: 2, maxPlayers: 6, minOpponents: 1, maxOpponents: 5, defaultOpponents: 3, hasTutorial: true, iconFrame: 54 }); registerGame({ slug: 'geniussquare', name: 'Genius Square', category: 'logic', minPlayers: 1, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, iconFrame: 70 }); +registerGame({ slug: 'katamino', name: 'Katamino', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 71 }); diff --git a/server/scripts/genKatamino.js b/server/scripts/genKatamino.js new file mode 100644 index 0000000..0b4a0b6 --- /dev/null +++ b/server/scripts/genKatamino.js @@ -0,0 +1,176 @@ +// Offline generator for Katamino puzzle bank. +// +// For each Penta level N (3–12) it enumerates every C(12, N) combination of +// the 12 standard pentominoes and runs a backtracking solver to check whether +// that set can tile a 5×N rectangle. Valid combinations become the puzzle bank. +// +// Usage: +// node server/scripts/genKatamino.js [outFile] + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT_FILE = process.argv[2] + ? path.resolve(process.argv[2]) + : path.join(__dirname, '../../public/data/katamino.json'); + +const ROWS = 5; + +const PIECES = [ + { id: 'F', cells: [[0,1],[0,2],[1,0],[1,1],[2,1]] }, + { id: 'I', cells: [[0,0],[1,0],[2,0],[3,0],[4,0]] }, + { id: 'L', cells: [[0,0],[1,0],[2,0],[3,0],[3,1]] }, + { id: 'N', cells: [[0,0],[1,0],[2,0],[2,1],[3,1]] }, + { id: 'P', cells: [[0,0],[0,1],[1,0],[1,1],[2,0]] }, + { id: 'T', cells: [[0,0],[0,1],[0,2],[1,1],[2,1]] }, + { id: 'U', cells: [[0,0],[0,1],[1,0],[2,0],[2,1]] }, + { id: 'V', cells: [[0,0],[1,0],[2,0],[2,1],[2,2]] }, + { id: 'W', cells: [[0,0],[1,0],[1,1],[2,1],[2,2]] }, + { id: 'X', cells: [[0,1],[1,0],[1,1],[1,2],[2,1]] }, + { id: 'Y', cells: [[0,0],[1,0],[1,1],[2,0],[3,0]] }, + { id: 'Z', cells: [[0,0],[0,1],[1,1],[2,1],[2,2]] }, +]; + +// ── Orientation precompute ──────────────────────────────────────────────────── + +function normalize(cells) { + let minR = Infinity, minC = Infinity; + for (const [r, c] of cells) { if (r < minR) minR = r; if (c < minC) minC = c; } + return cells.map(([r, c]) => [r - minR, c - minC]).sort((a, b) => a[0] - b[0] || a[1] - b[1]); +} + +function keyOf(cells) { return cells.map(([r, c]) => `${r},${c}`).join(';'); } + +function computeOrientations(cells) { + const seen = new Map(); + const base = cells.map(([r, c]) => [r, c]); + for (let flip = 0; flip < 2; flip++) { + let work = flip ? base.map(([r, c]) => [r, -c]) : base; + for (let rot = 0; rot < 4; rot++) { + const norm = normalize(work); + const k = keyOf(norm); + if (!seen.has(k)) seen.set(k, norm); + work = work.map(([r, c]) => [c, -r]); + } + } + return [...seen.values()]; +} + +// Pre-flatten each orientation to [r0,c0, r1,c1, ...] for fast access +const ORIS_BY_ID = Object.fromEntries( + PIECES.map(p => [p.id, computeOrientations(p.cells).map(cells => { + const flat = new Int8Array(10); + for (let i = 0; i < 5; i++) { flat[i*2] = cells[i][0]; flat[i*2+1] = cells[i][1]; } + return flat; + })]) +); + +// ── Backtracking solver ─────────────────────────────────────────────────────── +// Iterates a fixed pieceIds[] array + used[] boolean array so we never mutate +// the structure being iterated (which would cause infinite loops). +// Cell indices are stored in plain local variables (i0..i4) rather than a +// shared buffer so recursive calls cannot clobber each other's placed data. + +function hasSolution(cols, pieceIds) { + const board = new Int8Array(ROWS * cols); // 0=empty, 1=filled + const used = new Uint8Array(pieceIds.length); + return _solve(board, cols, pieceIds, used); +} + +function _solve(board, cols, pieceIds, used) { + // First empty cell (row-major forcing) + let emptyIdx = -1; + for (let i = 0; i < board.length; i++) { if (board[i] === 0) { emptyIdx = i; break; } } + if (emptyIdx === -1) return true; // all cells filled (N pieces × 5 = 5×N board) + + const targetR = (emptyIdx / cols) | 0; + const targetC = emptyIdx % cols; + + for (let pi = 0; pi < pieceIds.length; pi++) { + if (used[pi]) continue; + const oris = ORIS_BY_ID[pieceIds[pi]]; + + for (let oi = 0; oi < oris.length; oi++) { + const ori = oris[oi]; + + // Try each cell of this orientation as the "pin" onto targetR/targetC + for (let k = 0; k < 5; k++) { + const anchorR = targetR - ori[k*2]; + const anchorC = targetC - ori[k*2+1]; + + // Validate all 5 cells and collect board indices into local variables + const a0r = anchorR + ori[0], a0c = anchorC + ori[1]; + const a1r = anchorR + ori[2], a1c = anchorC + ori[3]; + const a2r = anchorR + ori[4], a2c = anchorC + ori[5]; + const a3r = anchorR + ori[6], a3c = anchorC + ori[7]; + const a4r = anchorR + ori[8], a4c = anchorC + ori[9]; + + if (a0r < 0 || a0r >= ROWS || a0c < 0 || a0c >= cols || board[a0r*cols+a0c]) continue; + if (a1r < 0 || a1r >= ROWS || a1c < 0 || a1c >= cols || board[a1r*cols+a1c]) continue; + if (a2r < 0 || a2r >= ROWS || a2c < 0 || a2c >= cols || board[a2r*cols+a2c]) continue; + if (a3r < 0 || a3r >= ROWS || a3c < 0 || a3c >= cols || board[a3r*cols+a3c]) continue; + if (a4r < 0 || a4r >= ROWS || a4c < 0 || a4c >= cols || board[a4r*cols+a4c]) continue; + + const i0=a0r*cols+a0c, i1=a1r*cols+a1c, i2=a2r*cols+a2c, i3=a3r*cols+a3c, i4=a4r*cols+a4c; + board[i0]=1; board[i1]=1; board[i2]=1; board[i3]=1; board[i4]=1; + used[pi] = 1; + + if (_solve(board, cols, pieceIds, used)) return true; + + board[i0]=0; board[i1]=0; board[i2]=0; board[i3]=0; board[i4]=0; + used[pi] = 0; + } + } + } + return false; +} + +// ── Combination generator ───────────────────────────────────────────────────── + +function combinations(arr, k) { + const result = []; + function pick(start, cur) { + if (cur.length === k) { result.push([...cur]); return; } + for (let i = start; i <= arr.length - (k - cur.length); i++) { + cur.push(arr[i]); + pick(i + 1, cur); + cur.pop(); + } + } + pick(0, []); + return result; +} + +// ── Main generation ─────────────────────────────────────────────────────────── + +const pieceIds = PIECES.map(p => p.id); +const pentas = {}; +const t0 = Date.now(); + +for (let n = 3; n <= 12; n++) { + const combos = combinations(pieceIds, n); + const valid = []; + process.stdout.write(`Penta ${n} (5×${n}): ${combos.length} combos … `); + const t1 = Date.now(); + + for (const combo of combos) { + if (hasSolution(n, combo)) { + valid.push({ idx: valid.length, pieces: combo }); + } + } + + const dt = ((Date.now() - t1) / 1000).toFixed(1); + console.log(`${valid.length} valid (${dt}s)`); + pentas[String(n)] = valid; +} + +const output = { + generatedAt: new Date().toISOString(), + totalTime: `${((Date.now() - t0) / 1000).toFixed(1)}s`, + pentas, +}; + +fs.writeFileSync(OUT_FILE, JSON.stringify(output)); +console.log(`\nWrote ${OUT_FILE} (${(fs.statSync(OUT_FILE).size / 1024).toFixed(0)} KB)`);