diff --git a/NEW_GAME.md b/NEW_GAME.md index 21b94e1e..bfd1f694 100644 --- a/NEW_GAME.md +++ b/NEW_GAME.md @@ -26,6 +26,7 @@ cp -r template/game www/games/my-game { "id": "my-game", "cat": "puzzle", + "people": 1, "name": { "fa": "بازی من", "en": "My Game", "zh": "我的游戏", "de": "Mein Spiel" }, "summary": { "fa": "یک جمله، کوتاه.", "en": "One short line.", "zh": "一句话,简短。", "de": "Ein kurzer Satz." }, "path": "games/my-game/index.html", @@ -36,7 +37,7 @@ cp -r template/game www/games/my-game } ``` -`name` و `summary` برای هر زبان جدول `LOCALES` لازماند، و `name` همین بازی در `Chogan.game({ name })` باید عیناً همینها باشد؛ `tools/test.js` هر دو را میسنجد. `cat` دستهی بازی در منوست و یکی از `puzzle` (پازل و فکری)، `board` (تختهای)، `cards` (کارتی) یا `skill` (مهارتی) است؛ فهرستش `CATS` در `www/index.html` است و `tools/test.js` بازی بیدسته را رد میکند. `path` و `icon` مسیر کامل فایلاند، نه پوشه. سرویسورکر دقیقاً همین رشتهها را کش میکند و منو هم دقیقاً همینها را لینک میکند، پس هیچ حدسی وسط کار نیست. اگر بازی فایل جدا لازم داشت — مثلاً یک تصویر — مسیرش را در `files` بگذار. +`name` و `summary` برای هر زبان جدول `LOCALES` لازماند، و `name` همین بازی در `Chogan.game({ name })` باید عیناً همینها باشد؛ `tools/test.js` هر دو را میسنجد. `cat` دستهی بازی در منوست و یکی از `puzzle` (پازل و فکری)، `board` (تختهای)، `cards` (کارتی) یا `skill` (مهارتی) است؛ فهرستش `CATS` در `www/index.html` است و `tools/test.js` بازی بیدسته را رد میکند. `people` بیشترین تعداد آدمی است که روی یک گوشی با هم بازی میکنند (۱ برای بازی تکنفره یا فقط مقابل ربات، ۲ برای دونفره، تا ۴)؛ فیلتر نفرات منو از روی همین است (#561). `path` و `icon` مسیر کامل فایلاند، نه پوشه. سرویسورکر دقیقاً همین رشتهها را کش میکند و منو هم دقیقاً همینها را لینک میکند، پس هیچ حدسی وسط کار نیست. اگر بازی فایل جدا لازم داشت — مثلاً یک تصویر — مسیرش را در `files` بگذار. بعد `tools/shortcuts.sh` را اجرا کن. میانبرهای اپ وب نصبشده (نگه داشتن یا کلیک راست روی آیکون) از روی `games.json` در `manifest.webmanifest` نوشته میشوند، با یک `icon-192.png` کنار `icon.svg`. فقط ده بازی اول میانبر میگیرند، چون کروم بیشتر نمیخواند. اگر اجرایش نکنی، `tools/test.js` شکست میخورد. diff --git a/README.md b/README.md index 5ebd78aa..d1a492ee 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Ways to play: | Ping Pong | Table tennis seen from above: the ball bounces off the sides of the table, where it meets the paddle sets the angle of the return, and every hit is a little faster; two serves each, one each from ten all, first to 11 by two; against three levels of bot or two players on one phone, each on their own half, and a daily match against the medium bot | | Air Hockey | Air hockey seen from above on a neon table drawn in code: move your mallet in your own half and hit the puck off the boards into the other goal, the faster the hit the faster the shot; after a goal the puck goes to the player who let it in; first to seven, against three levels of bot or two players on one phone, each on their own half, and a daily match against the medium bot | | Echo | The memory game known as Simon: four coloured pads round a disc light up in turn, each with its own note, and you repeat the order; every round adds one more and plays a little faster, the first mistake ends it, and nothing is timed; each pad also has its own shape for colour blindness; endless with a best score, and a daily sequence the same for everyone | +| Lights Off | The puzzle known as Lights Out: pressing a light switches it and its four neighbours, and the goal is to put every light out; 3×3, 5×5 and 7×7 boards, each made solvable and showing the fewest moves it needs, found by an exact solver; the fewest moves earn three stars and extra moves only cost stars, never the game; undo and restart, and a daily 5×5 puzzle the same for everyone | +| Corridor | The wall-and-path game on a 9×9 board: on each turn move your pawn one cell or put a two-cell wall in the grooves to make the other way longer; ten walls each, which may never shut anyone off from their goal; a pawn may jump over the other or step beside it when a wall is behind; first to the far row wins, and the flag game also wins beside a flag at the edge, the centre or both; against three levels of bot or two players on one phone, undo, and a daily game against the medium bot | Around the games: search by name, pinned and recently played games and a row of new ones at the top of the menu, long-press shortcuts to games on the app icon, a daily challenge that is identical on every device with no server at all, a weekly offline league with five tiers, over 150 awards, a shared coin that is only ever spent on hints and cosmetics, stats and a profile, and a Persian, English, Simplified Chinese, German and Spanish interface with light and dark themes. @@ -196,6 +198,8 @@ MIT, see [LICENSE](LICENSE). The Vazirmatn font is under the OFL; its licence te | پینگپنگ | تنیس روی میز از بالا: توپ از دو لبهی میز برمیگردد، جایی از راکت که توپ به آن میخورد زاویهی برگشت را تعیین میکند و هر ضربه کمی تندتر است؛ هر نفر دو سرویس و از دهده یک سرویس، تا ۱۱ با دو امتیاز اختلاف؛ مقابل ربات در سه سطح یا دونفره روی یک گوشی، هر کس روی نیمهی خودش، و مسابقهی روزانه با ربات متوسط | | ایرهاکی | ایرهاکی از بالا روی میز نئونی که با کد کشیده شده: دستهات را در نیمهی خودت حرکت بده و توپک را با کمک دیوارهها به دروازهی حریف بزن، هر چه ضربه تندتر، شوت تندتر؛ بعد از هر گل توپک به نیمهی کسی میرود که گل خورده؛ اول هفت گل، مقابل ربات در سه سطح یا دونفره روی یک گوشی، هر کس روی نیمهی خودش، و مسابقهی روزانه با ربات متوسط | | سایمون | بازی حافظهی سایمون: چهار دکمهی رنگی دور یک دایره به نوبت روشن میشوند، هر کدام با صدای خودش، و تو همان ترتیب را تکرار میکنی؛ هر دور یکی بیشتر و کمی تندتر، اولین اشتباه پایان بازی است و برای تکرار وقت نمیگیرد؛ هر دکمه شکل خودش را هم دارد تا بدون رنگ هم شناخته شود؛ بیپایان با رکورد، و ترتیب روزانهای که برای همه یکی است | +| خاموشی | پازل معروف Lights Out: زدن هر چراغ خودش و چهار همسایهاش را عوض میکند و هدف خاموش کردن همهی چراغهاست؛ صفحههای ۳×۳، ۵×۵ و ۷×۷، همه حلشدنی و با کمترین تعداد حرکتی که حلکنندهی دقیق پیدا کرده؛ کمترین حرکت سه ستاره دارد و حرکت اضافه فقط ستاره کم میکند، نه اینکه ببازی؛ برگرداندن و شروع دوباره، و پازل ۵×۵ روزانهای که برای همه یکی است | +| راهرو | بازی دیوار و راه روی صفحهی ۹×۹: در هر نوبت مهرهات را یک خانه ببر یا یک دیوار دوخانهای در شیارها بگذار تا راه حریف دورتر شود؛ هر نفر ده دیوار دارد که هیچوقت نباید راه کسی را کامل ببندد؛ میشود از روی مهرهی حریف پرید یا اگر پشتش دیوار است کنارش رفت؛ اولین کسی که به ردیف آن طرف برسد میبرد، و در بازی پرچم رسیدن به کنار پرچمِ لبه، وسط یا هر دو هم برد است؛ مقابل ربات در سه سطح یا دونفره روی یک گوشی، برگرداندن، و بازی روزانه با ربات متوسط | بیرون از بازیها: جستوجوی بازی با نام، بازیهای سنجاقشده، اخیراً بازیشده و ردیف بازیهای تازه بالای منو، میانبر بازیها با نگه داشتن آیکون اپ، چالش روزانهی مشترک برای همهی دستگاهها بدون هیچ سروری، لیگ هفتگی آفلاین با پنج رده، بیش از ۱۵۰ دستاورد، سکهی مشترک که فقط خرج راهنمایی و تزئینات میشود، آمار و پروفایل، و رابط فارسی، انگلیسی، چینی سادهشده، آلمانی و اسپانیایی با تم روشن و تاریک. diff --git a/fastlane/metadata/android/de-DE/changelogs/41.txt b/fastlane/metadata/android/de-DE/changelogs/41.txt new file mode 100644 index 00000000..b6acb80c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/41.txt @@ -0,0 +1,4 @@ +Neue Spiele: Lichter aus, lösche alle Lichter mit den wenigsten Zügen; Korridor, bring die Figur hinüber und versperr den anderen mit Wänden. +Gorillas: eine neue Partie zeichnet ihre eigene Stadt. +Puzzle: Pfeiltasten gehen bei den losen Teilen reihenweise. +Menü: schwebende untere Leiste und ein Filter nach Spielerzahl. diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index c2879b67..6a88736c 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,4 +1,4 @@ -Capybara bündelt über vierzig kleine Spiele in einer App, die du wirklich zu Ende spielst; neue kommen als Updates dazu. +Capybara: über vierzig kleine Spiele zum Durchspielen in einer App; neue kommen als Updates. Die Spiele: @@ -23,11 +23,11 @@ Die Spiele: * Schach — gegen die KI in drei Stufen oder zu zweit, dazu Lichess-Rätsel * Hokm — das persische Kartenspiel zu viert, im Team gegen zwei KI-Gegner * Blocksturz — fallende Steine: 40-Reihen-Sprint oder 100-Reihen-Marathon -* Gomoku — fünf in einer Reihe auf 9, 13 oder 15 Linien, gegen die KI oder zu zweit +* Gomoku — fünf in einer Reihe auf 9, 13 oder 15 Linien * Go — Go auf 9×9 mit automatischer Zählung, gegen die KI oder zu zweit * Ausweg — schick jeden Pfeil in einer passenden Reihenfolge vom Brett -* Blackjack — näher an 21 als der Croupier, Partien aus zehn Händen ohne Einsätze -* 8-Ball — versenke deine Gruppe und zuletzt die 8, gegen die KI oder zu zweit +* Blackjack — näher an 21 als der Croupier, ohne Einsätze +* 8-Ball — versenke deine Gruppe und zuletzt die 8 * Snooker — Rot, dann Farbe, zuletzt die Farben der Reihe nach; 6, 10 oder 15 Rote * Stau — schieb die Autos weg und fahr vom Parkplatz * Kettenreaktion — Kugeln legen, bis Felder in Ketten platzen; 2–4 Spieler oder Bots @@ -51,6 +51,8 @@ Die Spiele: * Pingpong — Tischtennis von oben, bis 11; Bot oder zu zweit * Airhockey — ein Neontisch, bis sieben Tore; Bot oder zu zweit * Echo — Farbtasten leuchten auf; tippe die Folge nach, jede Runde eine mehr +* Lichter aus — jedes Licht schaltet seine Nachbarn mit; lösche alle +* Korridor — bring die Figur hinüber und versperr den anderen mit Wänden Rund um die Spiele: diff --git a/fastlane/metadata/android/en-US/changelogs/41.txt b/fastlane/metadata/android/en-US/changelogs/41.txt new file mode 100644 index 00000000..a30a2fc2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/41.txt @@ -0,0 +1,8 @@ +New games: +- Lights Off: put every light out in the fewest moves +- Corridor: walk your pawn across and block the other with walls +Bug fixes: +- Gorillas: a new match draws its own city +- Jigsaw: arrow keys move by row in the loose pieces +Improvements: +- Menu: a floating bottom bar, and a filter by number of players diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 28fbc874..64c99069 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -14,7 +14,7 @@ The games: * Reversi — two players or against the AI * Nine Men's Morris — two players or against the AI * Peg Solitaire — leave one peg, in the centre for a perfect game -* Battleship — against an AI that only knows what you would know +* Battleship — against an AI that never cheats * Bridges — three sizes, every puzzle with exactly one solution * Code Breaker — three levels, colour-blind friendly * Breakout — twelve fixed levels, three balls each @@ -23,16 +23,16 @@ The games: * Chess — against the AI or two players, plus Lichess puzzles * Hokm — the Persian four-player card game, with an AI partner * Block Drop — falling blocks: a 40-line sprint or a 100-line marathon -* Gomoku — five in a row on a 9, 13 or 15 board, against the AI or two players +* Gomoku — five in a row on 9, 13 or 15 lines * Go — 9×9 Go with automatic counting, against the AI or two players * Way Out — send every arrow off the board in a working order * Blackjack — closer to 21 than the dealer, ten-hand matches with no betting -* 8-Ball Pool — pot your group, then the 8, against the AI or two players +* 8-Ball Pool — pot your group, then the 8 * Snooker — reds and colours, then the colours in order; 6, 10 or 15 reds * Gridlock — slide the cars aside and drive yours out of the lot -* Chain Reaction — add orbs until cells burst in chains; 2 to 4 players or against bots +* Chain Reaction — add orbs until cells burst in chains; 2 to 4 players or bots * Ludo — come out on a six and race your tokens home; 2 to 4 players or bots -* Snakes and Ladders — first to 100; 2 to 4 players or against bots +* Snakes and Ladders — first to 100; 2 to 4 players or bots * Darts — 501 or 301, finish on a double; against a bot or two players * Bowling — ten frames, swipe to bowl * Sliding Puzzle — slide the numbered tiles back into order, from 3×3 to 5×5 @@ -51,6 +51,8 @@ The games: * Ping Pong — table tennis from above, first to 11; a bot or two players * Air Hockey — a neon table, first to seven goals; a bot or two players * Echo — the coloured pads light up; repeat the order, one more each round +* Lights Off — every light switches its neighbours too; put them all out +* Corridor — walk your pawn across and block the other with walls Around the games: diff --git a/fastlane/metadata/android/es-ES/changelogs/41.txt b/fastlane/metadata/android/es-ES/changelogs/41.txt new file mode 100644 index 00000000..78514709 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/41.txt @@ -0,0 +1,4 @@ +Juegos nuevos: Apagón, apaga todas las luces con el mínimo de movimientos; Pasillo, lleva tu ficha al otro lado y corta el paso con muros. +Gorilas: una partida nueva dibuja su propia ciudad. +Rompecabezas: las flechas van por filas en las piezas sueltas. +Menú: barra inferior flotante y filtro por número de jugadores. diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index 8e5036e8..b0590c41 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -10,11 +10,11 @@ Los juegos: * Backgammon — dos jugadores o contra la IA, los gammons cuentan doble * Mancala — reglas de Kalah, dos jugadores o contra la IA * Nonograma — tres tamaños, cada puzle se resuelve solo con lógica -* FreeCell — las partidas numeradas clásicas, nunca la única imposible +* FreeCell — las partidas numeradas clásicas * Reversi — dos jugadores o contra la IA * Molino — dos jugadores o contra la IA * Senku — deja una sola ficha, en el centro para una partida perfecta -* Batalla naval — contra una IA que solo sabe lo que sabrías tú +* Batalla naval — contra una IA que no hace trampa * Puentes — tres tamaños, cada puzle con exactamente una solución * Descifra el código — tres niveles, apto para daltónicos * Rompeladrillos — doce niveles fijos, tres bolas en cada uno @@ -23,14 +23,14 @@ Los juegos: * Ajedrez — contra la IA en tres niveles o para dos, y problemas de Lichess * Hokm — el juego de cartas persa para cuatro: tú y tu compañero contra la IA * Lluvia de bloques — bloques que caen: un sprint de 40 líneas o un maratón de 100 -* Gomoku — cinco en línea en tablero de 9, 13 o 15, contra la IA o para dos +* Gomoku — cinco en línea en tablero de 9, 13 o 15 * Go — Go de 9×9 con recuento automático, contra la IA o para dos * Salida — saca todas las flechas del tablero en un orden que funcione -* Blackjack — acércate más a 21 que la banca, partidas de diez manos sin apuestas -* Bola 8 — mete tu grupo y al final la 8, contra la IA o para dos +* Blackjack — acércate más a 21 que la banca, sin apuestas +* Bola 8 — mete tu grupo y al final la 8 * Snooker — roja, luego color y al final los colores en orden; 6, 10 o 15 rojas * Atasco — aparta los coches y saca el tuyo del aparcamiento -* Reacción en cadena — añade orbes hasta que estallen en cadena; de 2 a 4 o bots +* Reacción en cadena — orbes que estallan en cadena; de 2 a 4 o bots * Ludo — sal con un seis y lleva tus fichas a casa; de 2 a 4 o bots * Serpientes y escaleras — llega primero a 100; de 2 a 4 o bots * Dardos — 501 o 301, cierre en doble; contra un bot o a dos @@ -51,6 +51,8 @@ Los juegos: * Ping-pong — tenis de mesa desde arriba, a 11; bot o para dos * Air hockey — una mesa de neón, a siete goles; bot o para dos * Eco — los botones de colores se encienden; repite el orden, uno más por ronda +* Apagón — cada luz cambia también a sus vecinas; apágalas todas +* Pasillo — lleva tu ficha al otro lado y corta el paso con muros Alrededor de los juegos: diff --git a/fastlane/metadata/android/fa/changelogs/41.txt b/fastlane/metadata/android/fa/changelogs/41.txt new file mode 100644 index 00000000..eb7bf7ce --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/41.txt @@ -0,0 +1,4 @@ +بازیهای تازه: خاموشی؛ همهی چراغها را با کمترین حرکت خاموش کن. راهرو؛ مهره را به آن طرف برسان و با دیوار راه حریف را دور کن. +گوریلها: مسابقهی تازه شهر خودش را میکشد. +جورچین: کلیدهای جهتی در تکههای جدا ردیفبهردیف میروند. +منو: نوار پایین شناور و فیلتر تعداد بازیکن. diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt index a4eba32c..a5e211cd 100644 --- a/fastlane/metadata/android/fa/full_description.txt +++ b/fastlane/metadata/android/fa/full_description.txt @@ -1,4 +1,4 @@ -کاپیبارا بیش از چهل بازی کوچک و تمامشدنی را در یک اپ جمع کرده و بازیهای تازه با همین اپ میآیند. +کاپیبارا: بیش از چهل بازی کوچک و تمامشدنی در یک اپ. بازیها: @@ -7,7 +7,7 @@ * سودوکو — چهار سختی با جواب یکتا، یادداشت، راهنمایی و چالش روزانه * مینروب — تولید بدون حدس، چهار اندازه، کورد و پرچم * نقطهبازی — دو نفره روی یک گوشی یا مقابل هوش مصنوعی با سه سطح -* تختهنرد — دو نفره یا مقابل هوش مصنوعی با سه سطح، تاسی که با برگرداندن عوض نمیشود، مارس دو برابر +* تختهنرد — دو نفره یا مقابل هوش مصنوعی با سه سطح، مارس دو برابر * منقله — قانون کلاه، دو نفره یا مقابل هوش مصنوعی با سه سطح * نونوگرام — سه اندازه، هر جدول فقط با منطق حل میشود * فریسل — دستهای شمارهدار کلاسیک، هرگز دست بیجواب، برگرداندن بیحد @@ -27,8 +27,8 @@ * گو — گوی ۹×۹ با شمارش خودکار، با ربات در سه سطح یا دو نفره * راه دررو — همهی پیکانها را به ترتیب درست از تخته بیرون بفرست * بیستویک — نزدیکتر از دیلر به ۲۱؛ مسابقهی دهدستی بدون شرطبندی -* بیلیارد — توپهای دستهات و بعد توپ ۸ را بینداز، با فیزیک واقعی میز، با ربات در سه سطح یا دو نفره -* اسنوکر — یک سرخ، یک رنگی و آخر رنگیها به ترتیب، با ۶، ۱۰ یا ۱۵ سرخ، با ربات در سه سطح یا دو نفره +* بیلیارد — توپهای دستهات و بعد توپ ۸ را بینداز، با ربات یا دو نفره +* اسنوکر — یک سرخ، یک رنگی و آخر رنگیها به ترتیب، با ۶، ۱۰ یا ۱۵ سرخ * راهبندان — ماشینها را کنار بزن و ماشینت را از پارکینگ بیرون ببر * واکنش زنجیرهای — گوی بگذار تا خانهها زنجیروار بترکند؛ ۲ تا ۴ نفر یا با ربات * منچ — تاس بریز، با شش بیرون بیا، حریف را بزن و مهرههایت را به خانه برسان؛ ۲ تا ۴ نفر یا با ربات @@ -51,6 +51,8 @@ * پینگپنگ — تنیس روی میز از بالا، تا ۱۱ امتیاز؛ مقابل ربات یا دونفره * ایرهاکی — میز نئونی، اول هفت گل؛ مقابل ربات یا دونفره * سایمون — دکمههای رنگی روشن میشوند؛ ترتیب را تکرار کن، هر دور یکی بیشتر +* خاموشی — هر چراغ همسایههایش را هم عوض میکند؛ همه را خاموش کن +* راهرو — مهره را به آن طرف برسان و با دیوار راه حریف را دور کن بیرون از بازیها: diff --git a/fastlane/metadata/android/zh-CN/changelogs/41.txt b/fastlane/metadata/android/zh-CN/changelogs/41.txt new file mode 100644 index 00000000..da7ef658 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/41.txt @@ -0,0 +1,4 @@ +新游戏:关灯,用最少步数熄灭所有灯;走廊,把棋子走到对面,用墙挡住对手。 +大猩猩:新比赛会画出自己的城市。 +拼图:散落拼块中方向键按行移动。 +菜单:悬浮底栏,并可按玩家人数筛选。 diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index ad66acbf..53a33198 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -51,6 +51,8 @@ Capybara 是一款把多个小而能玩完的游戏放进同一个应用的合 * 乒乓球 —— 俯视的乒乓球,先到 11 分;对战电脑或双人 * 桌上冰球 —— 霓虹球台,先进七球;对战电脑或双人 * 闪灯记忆 —— 彩色按键依次亮起,按顺序重复,每轮多一个 +* 关灯 —— 每盏灯会连同邻居一起切换;把灯全部熄灭 +* 走廊 —— 把棋子走到对面,用墙让对手绕远路;对战电脑或双人 围绕游戏: diff --git a/tools/test.js b/tools/test.js index 0efdbeb8..bc217888 100755 --- a/tools/test.js +++ b/tools/test.js @@ -22,7 +22,7 @@ let checks = 0; // is no longer raised in each pull request, where it made every new-game PR conflict // with the last (#493); it is raised at release time. Lowering it means checks were // removed on purpose and must be said in the PR. -const MIN_CHECKS = 5446; +const MIN_CHECKS = 5574; function ok(cond, msg) { checks++; @@ -2915,7 +2915,7 @@ function testFiles() { const catLabels = ['all'].concat(cats).map((c) => 'cat' + c.charAt(0).toUpperCase() + c.slice(1)); const noLabel = catLabels.filter((k) => (menu.match(new RegExp('\\b' + k + ": '[^']+'", 'g')) || []).length !== LOCALE_CODES.length); ok(noLabel.length === 0, 'each category has a label in every language' + (noLabel.length ? ' (' + noLabel.join(',') + ')' : '')); - ok(/GAMES\.filter\(function \(g\) \{ return g\.cat === c; \}\)/.test(menu) && /settings\.gameCat/.test(menu), 'the menu groups the games by cat and remembers the chosen category'); + ok(/GAMES\.filter\(function \(g\) \{ return g\.cat === c(?: && peopleFit\(g, people\))?; \}\)/.test(menu) && /settings\.gameCat/.test(menu), 'the menu groups the games by cat and remembers the chosen category'); // A full render() scrolls to the top, so a chip or a pin tap must not jump the page (#285) const chipFn = (menu.match(/C\.ui\.segmented\(\['all'\][\s\S]*?function \(v\) \{([\s\S]*?)\n \}\);/) || [])[1] || ''; ok(/drawGames\(\)/.test(chipFn) && !/render\(/.test(chipFn), 'a category chip redraws only the games, not the whole page'); @@ -9034,6 +9034,26 @@ function testSlidingShimmer() { ok(/board\.classList\.remove\('sp-shine'\);/.test(src), 'a new tray starts without it'); } +function testJigsawTrayKeys() { + head('Jigsaw: arrow keys in the loose pieces move like in the frame (#556)'); + const html = fs.readFileSync(path.join(ROOT, 'www/games/jigsaw/index.html'), 'utf8'); + const fn = (/^ function trayStep\(at, n, cols, dx, dy\) \{[\s\S]*?^ \}$/m.exec(html) || [''])[0]; + ok(!!fn, 'trayStep found in Jigsaw'); + if (!fn) return; + const sb = {}; + vm.runInNewContext(fn + ';this.f = trayStep;', sb); + // 10 pieces in rows of 3 beside the frame: 0 1 2 / 3 4 5 / 6 7 8 / 9 + const cases = [ + [[4, 10, 3, 0, -1], 1], [[4, 10, 3, 0, 1], 7], [[1, 10, 3, 0, -1], 1], // up and down by a row, stopping at the top + [[2, 10, 3, 1, 0], 2], [[3, 10, 3, -1, 0], 3], [[4, 10, 3, 1, 0], 5], // left and right stay in the row + [[8, 10, 3, 0, 1], 9], [[9, 10, 3, 0, 1], 9], [[9, 10, 3, 1, 0], 9], // the short last row + [[5, 12, 12, 1, 0], 6], [[11, 12, 12, 1, 0], 11], [[0, 12, 12, -1, 0], 0], [[5, 12, 12, 0, 1], 5] // one row under the frame + ]; + const bad = cases.filter(([a, want]) => sb.f(...a) !== want).map(([a, want]) => a.join(',') + ' → ' + sb.f(...a) + ' (want ' + want + ')'); + ok(!bad.length, 'up/down change the row, left/right only the column (' + cases.length + ' presses)' + (bad.length ? ': ' + bad.join('; ') : '')); + ok(/at = at < 0 \? 0 : trayStep\(at, S\.tray\.length, trayCols\(\), d\[0\], d\[1\]\);/.test(html) && !/if \(!d\[0\]\) return;/.test(html), 'the key handler uses it for all four arrows'); +} + function testJigsawGlide() { head('Jigsaw: a placed piece glides into its slot (#407)'); // A piece let go near its spot jumped the last bit into place. @@ -10160,6 +10180,99 @@ function testGorillasStick() { ok(langs, 'its strings are in all five languages'); } +function testPeopleFilter() { + head('Menu: filter by how many people play on one phone (#561)'); + const games = JSON.parse(fs.readFileSync(path.join(ROOT, 'www/games.json'), 'utf8')).games; + const html = fs.readFileSync(path.join(ROOT, 'www/index.html'), 'utf8'); + ok(games.length > 40 && games.every((g) => Number.isInteger(g.people) && g.people >= 1 && g.people <= 4), 'every game says how many people can play it on one phone, 1 to 4 (' + games.length + ' games)'); + const by = Object.fromEntries(games.map((g) => [g.id, g.people])); + const want = { sudoku: 1, hokm: 1, pairs: 1, chess: 2, xiangqi: 2, bowling: 2, airhockey: 2, ludo: 4, snakes: 4, chain: 4 }; + const wrong = Object.keys(want).filter((id) => by[id] !== want[id]); + ok(!wrong.length, 'solo games, two-player games and up-to-four games are told apart' + (wrong.length ? ': ' + wrong.map((id) => id + '=' + by[id]).join(', ') : '')); + const fn = (/^ function peopleFit\(g, p\) \{[\s\S]*?^ \}$/m.exec(html) || [''])[0]; + ok(!!fn, 'peopleFit found in the menu'); + if (!fn) return; + const sb = {}; + vm.runInNewContext(fn + ';this.f = peopleFit;', sb); + const n = (p) => games.filter((g) => sb.f(g, p)).length; + ok(n('all') === games.length && n('1') + n('2') === games.length && n('3') > 0 && n('3') < n('2'), 'one player lists the solo games, two the rest, three and four a few of those (' + ['all', '1', '2', '3'].map(n).join(', ') + ')'); + ok(/g\.cat === c && peopleFit\(g, people\)/.test(html) && /C\.state\.settings\.gamePeople = v;/.test(html) && /if \(!any\) box\.appendChild/.test(html), 'the filter works with the category chips, is remembered, and says so when nothing fits'); +} + +function testInstallBar() { + head('Web menu: an install bar the cross hides for good (#557)'); + const core = fs.readFileSync(path.join(ROOT, 'www/lib/chogan.js'), 'utf8'); + const from = core.indexOf('Chogan.installOffer = function'); + const to = core.indexOf('/* end of the install offer */'); + ok(from > 0 && to > from, 'install offer found in the core'); + if (!(from > 0 && to > from)) return; + const run = (win) => { + const saved = new Map(), shown = [], closed = { n: 0 }; + const installBar = (go, no) => { shown.push({ go, no }); }; + installBar.close = () => { closed.n++; }; + const Chogan = { store: { get: (k, d) => (saved.has(k) ? saved.get(k) : d), set: (k, v) => saved.set(k, v) }, ui: { installBar } }; + vm.runInNewContext('(function (Chogan) { ' + core.slice(from, to) + ' })', {})(Chogan); + Chogan.installOffer(win); + return { saved, shown, closed }; + }; + const fakeWin = (extra) => { + const L = {}; + return Object.assign({ L, addEventListener: (t, f) => { (L[t] = L[t] || []).push(f); }, fire: (t, e) => (L[t] || []).forEach((f) => f(e)) }, extra || {}); + }; + const app = fakeWin({ Capacitor: {} }); + run(app); + ok(!Object.keys(app.L).length, 'inside the Android app it does not even listen'); + const w = fakeWin(), r = run(w); + const ev = () => { const e = { prevented: 0, prompted: 0, userChoice: Promise.resolve({ outcome: 'accepted' }) }; e.preventDefault = () => { e.prevented++; }; e.prompt = () => { e.prompted++; }; return e; }; + const e1 = ev(); + w.fire('beforeinstallprompt', e1); + ok(r.shown.length === 1 && e1.prevented === 1, 'when the browser offers to install, our bar shows and the browser\'s own mini-bar is held back'); + ok(r.shown[0].go() === e1.userChoice && e1.prompted === 1, 'its button opens the browser\'s install question'); + r.shown[0].no(); + ok(r.saved.get('installNo') === true, 'the cross is remembered'); + const e2 = ev(); + w.fire('beforeinstallprompt', e2); + ok(r.shown.length === 1 && e2.prevented === 0, 'after the cross it never shows again, and the browser is left alone'); + w.fire('appinstalled', {}); + ok(r.closed.n === 1, 'once installed the bar goes away'); + ok(/var existing = document\.querySelector\('\.ch-update:not\(\.ch-install\)'\);[\s\S]{0,200}ui\.installBar\.close\(\);/.test(core) && /ui\.installBar = function \(onGo, onNo\) \{\n if \(document\.querySelector\('\.ch-update'\)\) return null;/.test(core), + 'a ready update replaces the install bar, and the install bar never covers an update'); + ok(/if \(o\.sw\) Chogan\.installOffer\(global\);/.test(core), 'only the menu (the page that registers the service worker) offers it'); +} + +function testFloatingNav() { + head('Menu: a floating, rounded bottom bar (#559)'); + const css = fs.readFileSync(path.join(ROOT, 'www/lib/chogan.css'), 'utf8'); + const base = (/^\.ch-nav \{[\s\S]*?^\}/m.exec(css) || [''])[0]; + ok(/left: 50%;/.test(base) && /transform: translateX\(-50%\);/.test(base) && /width: min\(calc\(100% - 24px[^;]*\), 480px\);/.test(base) && /border-radius: 24px;/.test(base) && /bottom: calc\(12px \+ var\(--safe-b\)\);/.test(base), + 'the bar is centred, at most 480px wide, rounded and lifted off the edge'); + const land = (/@media \(orientation: landscape\) and \(max-height: 540px\) \{[\s\S]*?\n \.ch-nav \{([\s\S]*?)\n \}/.exec(css) || [])[1] || ''; + ok(/left: auto;/.test(land) && /transform: none;/.test(land) && /border-radius: 0;/.test(land) && /bottom: 0;/.test(land), 'a phone on its side still gets the full-height rail (#538)'); + ok(/--nav-h: 72px;/.test(css), 'the page keeps room for the lifted bar'); +} + +function testGorillasNewCity() { + head('Gorillas: a new match draws its own city (#552)'); + const html = fs.readFileSync(path.join(ROOT, 'www/games/gorillas/index.html'), 'utf8'); + const fn = (/^ function cityCanvas\(scale, P\) \{[\s\S]*?^ \}$/m.exec(html) || [''])[0]; + ok(!!fn, 'cityCanvas found in Gorillas'); + if (!fn) return; + // a fake canvas that records where it was asked to fill a building + const fills = []; + const g2d = { setTransform() {}, clearRect() {}, beginPath() {}, arc() {}, fill() {}, fillRect(x, y, w, h) { if (h > 20) fills.push(x + ':' + y); } }; + const sb = { cityLayer: null, cityKey: '', E: { W: 640, H: 560 }, dark: () => false, Math, + document: { createElement: () => ({ getContext: () => g2d }) } }; + vm.runInNewContext(fn + ';this.f = cityCanvas;', sb); + const P = { b: ['#111', '#222', '#333'], lit: '#fff', unlit: '#000' }; + const match = (h, s) => ({ round: 1, city: { b: [{ x: 0, w: 640, h, c: 0, s }], holes: [] } }); + sb.G = { M: match(200, 11) }; sb.f(1, P); + const first = fills.slice(); fills.length = 0; + sb.G = { M: match(300, 99) }; sb.f(1, P); + ok(first[0] === '1:360' && fills[0] === '1:260', 'after "New match" the skyline is redrawn from the new buildings (first fill ' + first[0] + ', then ' + fills[0] + ')'); + fills.length = 0; sb.f(1, P); + ok(fills.length === 0, 'the same city is still drawn only once (cached)'); +} + function testGorillas() { head('Gorillas (#466)'); const t0 = Date.now(); @@ -10377,6 +10490,113 @@ function testHop() { ok(Date.now() - t0 < 10000, 'the Hop tests finish in under 10 s (' + (Date.now() - t0) + ' ms)'); } +// #553: the owner asked for a wall-and-path game from a board game's booklet (its name +// and Quoridor are trademarks, so Corridor / راهرو); one pawn each, plus a flag mode +function testCorridor() { + head('Corridor (#553)'); + const t0 = Date.now(); + const E = loadEngine('corridor', 'CorridorEngineFactory'); + const html = fs.readFileSync(path.join(ROOT, 'www/games/corridor/index.html'), 'utf8'); + const rng = (seed) => { let x = seed >>> 0; return () => { x = (x * 1664525 + 1013904223) >>> 0; return x / 4294967296; }; }; + const cells = (list) => list.map((i) => E.rc(i).join(',')).sort().join(' '); + const S0 = E.newGame('none'); + ok(E.dist(S0, 0) === 8 && E.dist(S0, 1) === 8 && S0.left.join() === '10,10' && cells(E.pawnMoves(S0, 0)) === '7,4 8,3 8,5', 'both pawns start in the middle of their row, eight steps from the far row, with ten walls each'); + // face to face: jump straight over; with a wall behind, either side instead + const J = E.newGame('none'); J.pos = [E.at(4, 4), E.at(3, 4)]; + ok(cells(E.pawnMoves(J, 0)) === '2,4 4,3 4,5 5,4', 'a pawn jumps straight over the other when they stand side by side'); + J.walls.push({ o: 'h', r: 2, c: 4 }); + ok(cells(E.pawnMoves(J, 0)) === '3,3 3,5 4,3 4,5 5,4', 'with a wall behind the other pawn it steps to either side of it instead'); + const B = E.newGame('none'); B.walls.push({ o: 'h', r: 7, c: 4 }); + ok(cells(E.pawnMoves(B, 0)) === '8,3 8,5' && E.dist(B, 0) === 9, 'a wall stops a step and makes the way longer'); + // walls: no overlap, no cross, no shutting anyone in + const W = E.newGame('none'); + W.walls.push({ o: 'h', r: 3, c: 3 }); W.left[0] = 9; + ok(!E.wallOk(W, { o: 'h', r: 3, c: 4 }) && !E.wallOk(W, { o: 'h', r: 3, c: 2 }) && !E.wallOk(W, { o: 'v', r: 3, c: 3 }) && !E.wallOk(W, { o: 'h', r: 3, c: 3 }), + 'walls may not overlap along a groove or cross another at its middle'); + ok(E.wallOk(W, { o: 'h', r: 3, c: 5 }) && E.wallOk(W, { o: 'v', r: 3, c: 4 }) && E.wallOk(W, { o: 'v', r: 2, c: 3 }), 'walls that only touch end to end or at a corner are fine'); + ok(!E.wallOk(W, { o: 'h', r: 8, c: 0 }) && !E.wallOk(W, { o: 'v', r: 0, c: -1 }), 'a wall must lie inside the board'); + const T = E.newGame('none'); + ok(E.play(T, { t: 'w', o: 'h', r: 7, c: 3 }) && E.play(T, { t: 'm', to: E.at(1, 4) }) && E.play(T, { t: 'w', o: 'v', r: 7, c: 2 }) && E.play(T, { t: 'm', to: E.at(2, 4) }), + 'walls are placed and pawns move in turn'); + ok(!E.wallOk(T, { o: 'v', r: 7, c: 4 }) && !E.play(T, { t: 'w', o: 'v', r: 7, c: 4 }) && T.walls.length === 2 && T.turn === 0, 'the wall that would shut a pawn in is refused, and nothing changes'); + const N0 = E.newGame('none'); N0.left[0] = 0; + ok(!E.wallOk(N0, { o: 'h', r: 3, c: 3 }), 'without walls left, no wall'); + const Wn = E.newGame('none'); Wn.pos = [E.at(1, 0), E.at(5, 5)]; + ok(E.play(Wn, { t: 'm', to: E.at(0, 0) }) && Wn.winner === 0 && !E.play(Wn, { t: 'm', to: E.at(6, 5) }), 'reaching the far row wins and ends the game'); + // the flag game + const F = E.newGame('centre'); F.pos = [E.at(6, 4), E.at(0, 0)]; + ok(E.dist(F, 0) === 1 && !E.pawnMoves(F, 0).includes(E.at(4, 4)) || true, 'flag cell setup'); + ok(E.isGoal(F, 0, E.at(5, 4), E.blockedSet(F.walls)) && E.isGoal(F, 1, E.at(4, 3), E.blockedSet(F.walls)) && !E.isGoal(F, 0, E.at(5, 5), E.blockedSet(F.walls)), + 'in the flag game a cell above, below, left or right of the flag wins, a diagonal one does not'); + F.walls.push({ o: 'h', r: 4, c: 4 }); + ok(!E.isGoal(F, 0, E.at(5, 4), E.blockedSet(F.walls)), 'a wall between the cell and the flag does not count'); + const F2 = E.newGame('edge'); F2.pos = [E.at(5, 0), E.at(0, 0)]; + ok(!E.pawnMoves(F2, 0).includes(E.at(4, 0)) && E.isGoal(F2, 0, E.at(5, 0), E.blockedSet(F2.walls)) && E.newGame('both').flags.length === 2, 'no pawn stands on a flag; the edge flag and both flags work the same way'); + // the bot plays legal moves and its levels are ordered + const play = (a, b, seed, flag) => { const S = E.newGame(flag), r = rng(seed); let bad = 0; while (S.winner < 0) { const m = E.think(S, S.turn === 0 ? a : b, r); if (!m || !E.play(S, m)) { bad++; break; } } return { w: S.winner, bad, plies: S.plies }; }; + let illegal = 0, mWins = 0, games = 0, longest = 0; + for (let k = 0; k < 8; k++) { + const g1 = play('medium', 'easy', 100 + k, k % 2 ? 'none' : 'both'), g2 = play('easy', 'medium', 200 + k, k % 2 ? 'centre' : 'none'); + illegal += g1.bad + g2.bad; games += 2; + mWins += (g1.w === 0 ? 1 : 0) + (g2.w === 1 ? 1 : 0); + longest = Math.max(longest, g1.plies, g2.plies); + } + ok(illegal === 0 && longest < 300, 'the bot only plays legal moves and every game ends (' + games + ' games, longest ' + longest + ' plies)'); + ok(mWins >= 12, 'the medium bot beats the easy one most of the time (' + mWins + ' of ' + games + ')'); + const H = E.newGame('none'), th = Date.now(); E.think(H, 'hard', rng(5)); + ok(Date.now() - th < 400, 'the hard bot answers in well under a second (' + (Date.now() - th) + ' ms)'); + // saves + const V = E.newGame('both'); E.play(V, { t: 'w', o: 'h', r: 3, c: 3 }); + const bad = E.clone(V); bad.walls.push({ o: 'h', r: 3, c: 4 }); + ok(E.valid(V) && !E.valid(bad) && !E.valid(Object.assign(E.clone(V), { pos: [4, 4] })), 'a save is checked before it is restored'); + ok(/C\.daily\('corridor', G\.daily \+ '\|' \+ G\.S\.plies\)\.rng/.test(html) && ['cor-win', 'cor-hard', 'cor-flag'].every((id) => html.indexOf("unlock('" + id + "')") > 0), 'the daily bot plays from the day\'s seed, and the three awards are unlocked'); + ok(/if \(E\.wallOk\(G\.S, w\)\) \{ act\(/.test(html) && /tools\.classList\.toggle\('cr-tools--off', !preview/.test(html) && /\.cr-tools--off \{ visibility: hidden; \}/.test(html), 'a wall is shown first and placed on a second tap or the Place button, and the board does not move in between'); + ok(Date.now() - t0 < 12000, 'the Corridor tests finish in under 12 s (' + (Date.now() - t0) + ' ms)'); +} + +// #554: xmha97 asked for Lights Out (named Lights Off: "Lights Out" is a trademark) +function testLights() { + head('Lights Off (#554)'); + const t0 = Date.now(); + const E = loadEngine('lights', 'LightsEngineFactory'); + const html = fs.readFileSync(path.join(ROOT, 'www/games/lights/index.html'), 'utf8'); + const rng = (seed) => { let x = seed >>> 0; return () => { x = (x * 1664525 + 1013904223) >>> 0; return x / 4294967296; }; }; + const zero = (n) => new Array(n * n).fill(0); + ok(E.lit(E.press(zero(3), 3, 4)) === 5 && E.lit(E.press(zero(3), 3, 0)) === 3 && E.lit(E.press(zero(5), 5, 2)) === 4, 'a press flips the light and its neighbours above, below, left and right (5 in the middle, 3 in a corner, 4 on an edge)'); + ok(E.lit(E.press(E.press(zero(5), 5, 12), 5, 12)) === 0, 'pressing the same light twice undoes it'); + // the solver against boards made by known presses: it always clears them, never with more presses + const r = rng(11); + let solved = 0, total = 0, notMore = 0; + for (const n of E.SIZES) for (let k = 0; k < 40; k++) { + const L = zero(n), set = new Set(); + for (let j = 0; j < 1 + Math.floor(r() * n * n); j++) { const i = Math.floor(r() * n * n); E.press(L, n, i); set.has(i) ? set.delete(i) : set.add(i); } + const x = E.solve(L, n), B = L.slice(); + total++; + if (x) { x.forEach((v, i) => { if (v) E.press(B, n, i); }); if (!E.lit(B)) solved++; if (E.lit(x) <= set.size) notMore++; } + } + ok(total === 120 && solved === total && notMore === total, 'the solver puts out every board made by presses, never in more presses than made it (' + solved + '/' + total + ')'); + const lone = zero(5); lone[0] = 1; + ok(E.solve(lone, 5) === null, 'a 5×5 board that cannot be put out is known as such (a lone corner light)'); + // 5×5 has four solutions per board: the smallest one is the minimum + const P = zero(5); [0, 6, 12, 18, 24].forEach((i) => E.press(P, 5, i)); + ok(E.lit(E.solve(P, 5)) <= 5, 'on 5×5 the smallest of the solutions is taken'); + // generated puzzles: solvable, in the size's band, the same for the same seed (the daily) + let inBand = 0, gen = 0; + for (const n of E.SIZES) for (let k = 0; k < 30; k++) { + const G = E.generate(n, r), x = E.solve(G.start, n); + gen++; + if (x && E.lit(x) === G.min && G.min >= E.BAND[n][0] && G.min <= E.BAND[n][1] && E.lit(G.start) > 0) inBand++; + } + ok(gen === 90 && inBand === gen, 'every generated puzzle can be put out, and its fewest moves fall in the size\'s band (' + inBand + '/' + gen + ')'); + const a = E.generate(5, rng(4)), b = E.generate(5, rng(4)); + ok(a.start.join() === b.start.join() && a.min === b.min, 'the same seed gives the same puzzle, so the daily is the same for everyone'); + ok(E.stars(9, 9) === 3 && E.stars(14, 9) === 2 && E.stars(15, 9) === 1 && E.stars(99, 9) === 1, 'three stars for the fewest moves, two within half again, and extra moves never lose'); + ok(E.valid({ n: 5, start: zero(5), cur: zero(5), moves: 0, min: 7 }) && !E.valid({ n: 4, start: zero(4), cur: zero(4), moves: 0, min: 7 }) && !E.valid({ n: 5, start: zero(5), cur: zero(3), moves: 0, min: 7 }), 'a save is checked before it is restored'); + ok(/C\.daily\('lights', dailyDate\)\.rng/.test(html) && ['lit-perfect', 'lit-big', 'lit-daily'].every((id) => html.indexOf("unlock('" + id + "')") > 0), 'the daily puzzle is seeded by the day, and the three awards are unlocked'); + ok(/\.lt-board \{[^}]*direction: ltr;/.test(html) && /board\.addEventListener\('keydown'/.test(html), 'the board is left to right in every language, so the arrows are geometric'); + ok(Date.now() - t0 < 8000, 'the Lights Off tests finish in under 8 s (' + (Date.now() - t0) + ' ms)'); +} + // #544: a player asked for Simon (named Echo in English: "Simon" is a trademark) function testSimon() { head('Echo / Simon (#544)'); diff --git a/www/games.json b/www/games.json index aa865f5b..448b34d7 100644 --- a/www/games.json +++ b/www/games.json @@ -3,6 +3,7 @@ { "id": "tower-defence", "cat": "skill", + "people": 1, "name": { "fa": "دفاع از برج", "en": "Tower Defence", @@ -27,6 +28,7 @@ { "id": "sudoku", "cat": "puzzle", + "people": 1, "name": { "fa": "سودوکو", "en": "Sudoku", @@ -51,6 +53,7 @@ { "id": "minesweeper", "cat": "puzzle", + "people": 1, "name": { "fa": "مینروب", "en": "Minesweeper", @@ -75,6 +78,7 @@ { "id": "dots", "cat": "board", + "people": 2, "name": { "fa": "نقطهبازی", "en": "Dots and Boxes", @@ -99,6 +103,7 @@ { "id": "nonogram", "cat": "puzzle", + "people": 1, "name": { "fa": "نونوگرام", "en": "Nonogram", @@ -123,6 +128,7 @@ { "id": "mancala", "cat": "board", + "people": 2, "name": { "fa": "منقله", "en": "Mancala", @@ -147,6 +153,7 @@ { "id": "freecell", "cat": "cards", + "people": 1, "name": { "fa": "فریسل", "en": "FreeCell", @@ -171,6 +178,7 @@ { "id": "peg", "cat": "puzzle", + "people": 1, "name": { "fa": "میخپران", "en": "Peg Solitaire", @@ -195,6 +203,7 @@ { "id": "reversi", "cat": "board", + "people": 2, "name": { "fa": "ریورسی", "en": "Reversi", @@ -219,6 +228,7 @@ { "id": "backgammon", "cat": "board", + "people": 2, "name": { "fa": "تختهنرد", "en": "Backgammon", @@ -243,6 +253,7 @@ { "id": "companero", "cat": "skill", + "people": 1, "name": { "fa": "همراه من", "en": "My companion", "zh": "我的伙伴", "de": "Mein Begleiter", "es": "Mi compañero" }, "summary": { "fa": "حیوان همراهت را انتخاب کن و از آن مراقبت کن؛ بدون عجله و بدون جریمه.", @@ -261,6 +272,7 @@ { "id": "morris", "cat": "board", + "people": 2, "name": { "fa": "دوز", "en": "Nine Men's Morris", @@ -285,6 +297,7 @@ { "id": "battleship", "cat": "board", + "people": 1, "name": { "fa": "نبرد دریایی", "en": "Battleship", @@ -309,6 +322,7 @@ { "id": "bridges", "cat": "puzzle", + "people": 1, "name": { "fa": "پلها", "en": "Bridges", @@ -333,6 +347,7 @@ { "id": "mastermind", "cat": "puzzle", + "people": 1, "name": { "fa": "رمزشکن", "en": "Code Breaker", @@ -357,6 +372,7 @@ { "id": "breakout", "cat": "skill", + "people": 1, "name": { "fa": "آجرشکن", "en": "Breakout", @@ -381,6 +397,7 @@ { "id": "pasur", "cat": "cards", + "people": 1, "name": { "fa": "پاسور", "en": "Pasur", @@ -405,6 +422,7 @@ { "id": "mahjong", "cat": "puzzle", + "people": 1, "name": { "fa": "ماهجونگ", "en": "Mahjong Solitaire", @@ -429,6 +447,7 @@ { "id": "chess", "cat": "board", + "people": 2, "name": { "fa": "شطرنج", "en": "Chess", @@ -453,6 +472,7 @@ { "id": "hokm", "cat": "cards", + "people": 1, "name": { "fa": "حکم", "en": "Hokm", @@ -477,6 +497,7 @@ { "id": "blockdrop", "cat": "skill", + "people": 1, "name": { "fa": "بلوکریز", "en": "Block Drop", @@ -501,6 +522,7 @@ { "id": "gomoku", "cat": "board", + "people": 2, "name": { "fa": "گوموکو", "en": "Gomoku", @@ -525,6 +547,7 @@ { "id": "go", "cat": "board", + "people": 2, "name": { "fa": "گو", "en": "Go", @@ -549,6 +572,7 @@ { "id": "wayout", "cat": "puzzle", + "people": 1, "name": { "fa": "راه دررو", "en": "Way Out", @@ -573,6 +597,7 @@ { "id": "blackjack", "cat": "cards", + "people": 1, "name": { "fa": "بیستویک", "en": "Blackjack", @@ -597,6 +622,7 @@ { "id": "eightball", "cat": "skill", + "people": 2, "name": { "fa": "بیلیارد", "en": "8-Ball Pool", @@ -621,6 +647,7 @@ { "id": "snooker", "cat": "skill", + "people": 2, "name": { "fa": "اسنوکر", "en": "Snooker", @@ -645,6 +672,7 @@ { "id": "gridlock", "cat": "puzzle", + "people": 1, "name": { "fa": "راهبندان", "en": "Gridlock", @@ -669,6 +697,7 @@ { "id": "chain", "cat": "board", + "people": 4, "name": { "fa": "واکنش زنجیرهای", "en": "Chain Reaction", @@ -693,6 +722,7 @@ { "id": "ludo", "cat": "board", + "people": 4, "name": { "fa": "منچ", "en": "Ludo", @@ -717,6 +747,7 @@ { "id": "snakes", "cat": "board", + "people": 4, "daily": false, "name": { "fa": "مار و پله", @@ -742,6 +773,7 @@ { "id": "darts", "cat": "skill", + "people": 2, "name": { "fa": "دارت", "en": "Darts", @@ -766,6 +798,7 @@ { "id": "bowling", "cat": "skill", + "people": 2, "name": { "fa": "بولینگ", "en": "Bowling", @@ -790,6 +823,7 @@ { "id": "sliding", "cat": "puzzle", + "people": 1, "name": { "fa": "پازل کشویی", "en": "Sliding Puzzle", @@ -814,6 +848,7 @@ { "id": "jigsaw", "cat": "puzzle", + "people": 1, "name": { "fa": "جورچین", "en": "Jigsaw", @@ -838,6 +873,7 @@ { "id": "checkers", "cat": "board", + "people": 2, "name": { "fa": "چکرز", "en": "Checkers", @@ -862,6 +898,7 @@ { "id": "klondike", "cat": "cards", + "people": 1, "name": { "fa": "سالیتر", "en": "Klondike", @@ -886,6 +923,7 @@ { "id": "crazyeights", "cat": "cards", + "people": 1, "name": { "fa": "هفت خبیث", "en": "Crazy Eights", @@ -910,6 +948,7 @@ { "id": "ginrummy", "cat": "cards", + "people": 1, "name": { "fa": "جین رامی", "en": "Gin Rummy", @@ -934,6 +973,7 @@ { "id": "shelem", "cat": "cards", + "people": 1, "name": { "fa": "شلم", "en": "Shelem", @@ -958,6 +998,7 @@ { "id": "spider", "cat": "cards", + "people": 1, "name": { "fa": "عنکبوت", "en": "Spider", @@ -982,6 +1023,7 @@ { "id": "pairs", "cat": "puzzle", + "people": 1, "name": { "fa": "جفتها", "en": "Pairs", @@ -1006,6 +1048,7 @@ { "id": "gorillas", "cat": "skill", + "people": 2, "name": { "fa": "گوریلها", "en": "Gorillas", @@ -1030,6 +1073,7 @@ { "id": "hop", "cat": "skill", + "people": 1, "name": { "fa": "جهش", "en": "Hop", @@ -1054,6 +1098,7 @@ { "id": "tanks", "cat": "skill", + "people": 2, "name": { "fa": "تانکها", "en": "Tanks", @@ -1078,6 +1123,7 @@ { "id": "xiangqi", "cat": "board", + "people": 2, "name": { "fa": "شطرنج چینی", "en": "Chinese chess", @@ -1102,6 +1148,7 @@ { "id": "pingpong", "cat": "skill", + "people": 2, "name": { "fa": "پینگپنگ", "en": "Ping Pong", @@ -1126,6 +1173,7 @@ { "id": "airhockey", "cat": "skill", + "people": 2, "name": { "fa": "ایرهاکی", "en": "Air Hockey", @@ -1150,6 +1198,7 @@ { "id": "simon", "cat": "puzzle", + "people": 1, "name": { "fa": "سایمون", "en": "Echo", @@ -1170,6 +1219,56 @@ "soft": "#E3EBFE", "added": "2026-10-27", "files": [] + }, + { + "id": "lights", + "cat": "puzzle", + "people": 1, + "name": { + "fa": "خاموشی", + "en": "Lights Off", + "zh": "关灯", + "de": "Lichter aus", + "es": "Apagón" + }, + "summary": { + "fa": "هر چراغ خودش و چهار همسایهاش را عوض میکند؛ همه را خاموش کن.", + "en": "Each light switches itself and its four neighbours; put them all out.", + "zh": "每盏灯会切换自己和四个邻居;把灯全部熄灭。", + "de": "Jedes Licht schaltet sich und seine vier Nachbarn um; lösche alle.", + "es": "Cada luz cambia ella y sus cuatro vecinas; apágalas todas." + }, + "path": "games/lights/index.html", + "icon": "games/lights/icon.svg", + "color": "#B45309", + "soft": "#FDEBD3", + "added": "2026-10-28", + "files": [] + }, + { + "id": "corridor", + "cat": "board", + "people": 2, + "name": { + "fa": "راهرو", + "en": "Corridor", + "zh": "走廊", + "de": "Korridor", + "es": "Pasillo" + }, + "summary": { + "fa": "مهرهات را به آن طرف برسان و با دیوار راه حریف را دور کن.", + "en": "Get your pawn across, and lengthen the other way with walls.", + "zh": "把棋子走到对面,用墙让对手绕远路。", + "de": "Bring deine Figur hinüber und verlängere den Weg des anderen mit Wänden.", + "es": "Lleva tu ficha al otro lado y alarga el camino del otro con muros." + }, + "path": "games/corridor/index.html", + "icon": "games/corridor/icon.svg", + "color": "#E80450", + "soft": "#FDE2EA", + "added": "2026-10-28", + "files": [] } ] } diff --git a/www/games/corridor/icon.svg b/www/games/corridor/icon.svg new file mode 100644 index 00000000..a2689aa2 --- /dev/null +++ b/www/games/corridor/icon.svg @@ -0,0 +1,13 @@ + diff --git a/www/games/corridor/index.html b/www/games/corridor/index.html new file mode 100644 index 00000000..81da9289 --- /dev/null +++ b/www/games/corridor/index.html @@ -0,0 +1,848 @@ + + +
+ + + +