{"_id":"@bjoernboss/mws-crossword","_rev":"4-d52bfbb35ac764ca10b6ff9cb957e22b","name":"@bjoernboss/mws-crossword","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@bjoernboss/mws-crossword","version":"1.0.0","author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"license":"BSD-3-Clause","_id":"@bjoernboss/mws-crossword@1.0.0","maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"homepage":"https://github.com/BjoernBoss/mws-crossword#readme","bugs":{"url":"https://github.com/BjoernBoss/mws-crossword/issues"},"dist":{"shasum":"c1cbf765495cb3d1104e33d2ce774319e000b988","tarball":"https://registry.npmjs.org/@bjoernboss/mws-crossword/-/mws-crossword-1.0.0.tgz","fileCount":15,"integrity":"sha512-zQOSdCvgYqiuTZTaVfBE3QBNyoP+U8fgqawEoKokusMo9OPE0kT+E0KHEuIZTglwBeiHqqdgMuNlc3YPPojCRA==","signatures":[{"sig":"MEQCIHSh3ZcKgBvG7ucWUL5nZjYgEGtMUANVJM/htsGbSFxSAiBMkiphB20mbIuLel+A88nA7g7y1GZddtK1bmukKwP7fw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":104049},"main":"dist/crossword.js","type":"module","types":"dist/crossword.d.ts","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/crossword.d.ts","default":"./dist/crossword.js"},"./package.json":"./package.json"},"gitHead":"c5b6d6566f488f46327b000f1ee0b8b33536429e","scripts":{"prepare":"tsc"},"_npmUser":{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"},"repository":{"url":"git+https://github.com/BjoernBoss/mws-crossword.git","type":"git"},"_npmVersion":"11.1.0","description":"Module in TypeScript to create and play crosswords together - For Modular Web Server","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@bjoernboss/mws":"^1.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/mws-crossword_1.0.0_1781484060397_0.6040559823038805","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@bjoernboss/mws-crossword","version":"1.1.0","author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"license":"BSD-3-Clause","_id":"@bjoernboss/mws-crossword@1.1.0","maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"homepage":"https://github.com/BjoernBoss/mws-crossword#readme","bugs":{"url":"https://github.com/BjoernBoss/mws-crossword/issues"},"dist":{"shasum":"b8965b746d0458f21871327002a212d922527626","tarball":"https://registry.npmjs.org/@bjoernboss/mws-crossword/-/mws-crossword-1.1.0.tgz","fileCount":15,"integrity":"sha512-EDO7MYsbnCjqv5pSzhjICVYuVHVKAC3vqPnu4+mDOnLAzGkuS4THCANGgwE34o2LjDPDn4PlYHSVtZGgBwxIRQ==","signatures":[{"sig":"MEUCIEEHM5GoMaVwK3xgCPc3llae1uZwB8rfqz1zne0k8eDxAiEAuGp43dpx5oV8y/6d2ymdx5lBZfDyNzc2nvz/ZHmrCPk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":104907},"main":"dist/crossword.js","type":"module","types":"dist/crossword.d.ts","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/crossword.d.ts","default":"./dist/crossword.js"},"./package.json":"./package.json"},"gitHead":"e00fa93d19e9144db389dd09ef245c2073e39ecf","scripts":{"prepare":"tsc"},"_npmUser":{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"},"repository":{"url":"git+https://github.com/BjoernBoss/mws-crossword.git","type":"git"},"_npmVersion":"11.13.0","description":"Module in TypeScript to create and play crosswords together - For Modular Web Server","directories":{},"_nodeVersion":"26.2.0","dependencies":{"@bjoernboss/mws":"^1.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/mws-crossword_1.1.0_1781749691134_0.9271565896108669","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@bjoernboss/mws-crossword","version":"1.1.1","author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"license":"BSD-3-Clause","_id":"@bjoernboss/mws-crossword@1.1.1","maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"homepage":"https://github.com/BjoernBoss/mws-crossword#readme","bugs":{"url":"https://github.com/BjoernBoss/mws-crossword/issues"},"dist":{"shasum":"64cbd8fda578a8d1e20f5433375f526519a2b9b2","tarball":"https://registry.npmjs.org/@bjoernboss/mws-crossword/-/mws-crossword-1.1.1.tgz","fileCount":15,"integrity":"sha512-xw4Az8aMMSTBNaaaYE0MMG0jo1FULIdBVfEkSkNjpOtJItFSqMTLSq9kJ82uKBFdOy7pbltaIJcENMgWyFeMWw==","signatures":[{"sig":"MEUCIQDsM8FgK6lzXjCpjvmAgYtulFqxPdGf3NBm28ZWfWvQ7gIgTENGLMXH9GsiNdUNNSn/l1BDcFHYgngQgg2Ci5vXw1g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":105413},"main":"dist/crossword.js","type":"module","types":"dist/crossword.d.ts","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/crossword.d.ts","default":"./dist/crossword.js"},"./package.json":"./package.json"},"gitHead":"8bc6343635f9ba8830c4f68f44c0ad45efb6cc2b","scripts":{"prepare":"tsc"},"_npmUser":{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"},"repository":{"url":"git+https://github.com/BjoernBoss/mws-crossword.git","type":"git"},"_npmVersion":"11.13.0","description":"Module in TypeScript to create and play crosswords together - For Modular Web Server","directories":{},"_nodeVersion":"26.2.0","dependencies":{"@bjoernboss/mws":"^1.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/mws-crossword_1.1.1_1781824323288_0.27895735713524505","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@bjoernboss/mws-crossword","version":"1.2.0","type":"module","description":"Module in TypeScript to create and play crosswords together - For Modular Web Server","license":"BSD-3-Clause","author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/BjoernBoss/mws-crossword.git"},"publishConfig":{"access":"public"},"main":"dist/crossword.js","types":"dist/crossword.d.ts","exports":{".":{"default":"./dist/crossword.js","types":"./dist/crossword.d.ts"},"./package.json":"./package.json"},"scripts":{"prepare":"tsc"},"dependencies":{"@bjoernboss/mws":"^1.4.0"},"devDependencies":{"@types/node":"^25.6.0","typescript":"^6.0.3"},"engines":{"node":">=22.0.0"},"gitHead":"e8cd3f2c5cb99733533a9a1ac434d7efb4ccc463","_id":"@bjoernboss/mws-crossword@1.2.0","bugs":{"url":"https://github.com/BjoernBoss/mws-crossword/issues"},"homepage":"https://github.com/BjoernBoss/mws-crossword#readme","_nodeVersion":"26.5.1","_npmVersion":"11.13.0","dist":{"integrity":"sha512-MuK2Y7frdX9wi344fCnCF93klp0Oy2XH6gjZBgpAlr35au3JfICr0zM6kAie8aOEoFVVxrQZ0yTrQUAAlaxG+A==","shasum":"cd6185c280cea0372e902ae484b26120ee124d5b","tarball":"https://registry.npmjs.org/@bjoernboss/mws-crossword/-/mws-crossword-1.2.0.tgz","fileCount":15,"unpackedSize":108695,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCuzwOQtCtlgTKt1bBRFyQk6GAtRPXoVmrNtvcQuFloWAIgH8YOMVKj/us61ZX7AEKtFC0ud6tCrcbHauUub7bmKh0="}]},"_npmUser":{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"},"directories":{},"maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mws-crossword_1.2.0_1785930558421_0.3434006940847658"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-15T00:41:00.170Z","modified":"2026-08-05T11:49:18.710Z","1.0.0":"2026-06-15T00:41:00.540Z","1.1.0":"2026-06-18T02:28:11.369Z","1.1.1":"2026-06-18T23:12:03.451Z","1.2.0":"2026-08-05T11:49:18.570Z"},"bugs":{"url":"https://github.com/BjoernBoss/mws-crossword/issues"},"author":{"name":"Bjoern Boss Henrichsen","email":"bjoernbossdev@gmail.com"},"license":"BSD-3-Clause","homepage":"https://github.com/BjoernBoss/mws-crossword#readme","repository":{"type":"git","url":"git+https://github.com/BjoernBoss/mws-crossword.git"},"description":"Module in TypeScript to create and play crosswords together - For Modular Web Server","maintainers":[{"name":"bjoernboss","email":"bjoernbossdev@gmail.com"}],"readme":"# \\[MWS\\] Module to Create and Play Crosswords Together\r\n![TypeScript](https://img.shields.io/badge/language-TypeScript-blue?style=flat-square)\r\n[![License](https://img.shields.io/badge/license-BSD--3--Clause-brightgreen?style=flat-square)](LICENSE.txt)\r\n\r\nA collaborative crossword module for [`@bjoernboss/mws`](https://github.com/BjoernBoss/mws).\r\n\r\nPlayers can create, edit, and solve crossword puzzles together in real time using WebSockets.\r\n\r\nGame state is stored as JSON files in a configurable data directory and persists across server restarts. All active sessions are managed by the `Crossword` module.\r\n\r\n## Installation\r\n\r\n\t$ npm install @bjoernboss/mws-crossword\r\n\r\nRequires Node.js 22 or later.\r\n\r\n## Setup\r\n\r\nThe `Crossword` module takes a data directory path and an optional `Params` object controlling what operations clients may perform. Mount it under a path using `dispatch`:\r\n\r\n```typescript\r\nimport { Server, dispatch, addLogger, createConsoleLogger } from \"@bjoernboss/mws\";\r\nimport { Crossword } from \"@bjoernboss/mws-crossword\";\r\n\r\naddLogger(createConsoleLogger());\r\n\r\nconst server = new Server();\r\nconst crossword = new Crossword('./data/crossword', {\r\n    query: true,\r\n    create: true,\r\n    delete: true,\r\n    edit: true\r\n});\r\n\r\nserver.listen(dispatch({ '/crossword': crossword }), { port: 8080 });\r\n```\r\n\r\nThe module serves its own pages, static assets, and WebSocket endpoints from its mount point. Navigate to `http://localhost:8080/crossword/` to open the lobby.\r\n\r\nImportant: The module caches the loaded games in memory. The same data directory should therefore not be used by multiple `Crossword` modules simultaneously.\r\n\r\n## Frontend\r\n\r\nThe module ships with a complete browser frontend across its three pages. Operations not permitted by the configured parameters are hidden from the UI.\r\n\r\n### Lobby\r\n\r\n- Lists all crosswords alphabetically, each linking to its play page; fetch failures show an error notification with a reload button.\r\n- Creating (link to the editor) and deleting are only offered when permitted; deletion requires a confirmation dialog and cannot be undone.\r\n\r\n### Editor\r\n\r\n- Grid dimensions are chosen on startup (1x1 to 64x64); solid cells are then painted by clicking and dragging with the mouse, or by touch on mobile.\r\n- Crossword numbers are assigned automatically and re-rendered live while painting.\r\n- Finishing asks for a game name (validated client-side against the naming rules) and uploads the layout; navigating away with unsaved changes is guarded by a confirmation prompt.\r\n\r\n### Play\r\n\r\n- Players join by name - pre-filled from the cookie - or as passive watchers; watch mode is entered automatically when editing is not permitted.\r\n- Focusing a cell highlights the whole word and smoothly zooms and pans the view to frame it; Escape resets the view to the full grid.\r\n- Keyboard support: typed letters advance along the current direction, arrow keys navigate while skipping solid cells, Tab or Space toggles between horizontal and vertical, and Backspace clears or steps back.\r\n- Letters can be marked as uncertain guesses - via the Guess button or by holding Shift - and are rendered distinctly until confirmed.\r\n- Every author is assigned a distinct color (hues spread for maximum separation): grid cells are tinted with their author's color, and a player list shows all authors with their online status.\r\n- Concurrent editing is conflict-safe on the client as well: local changes are sent as acknowledged deltas, and unacknowledged local edits are never overwritten by remote updates and are re-pushed after reconnects.\r\n- Lost connections are retried automatically and otherwise reported through a notification with a reload button; server-side save failures are surfaced to all players.\r\n- On-screen keyboards on mobile are handled by resizing the layout to the visual viewport, in both the editor and the play page.\r\n\r\n## Parameters\r\n\r\nThe `Params` object controls module behavior and access. All fields are optional:\r\n\r\n| Field | Default | Description |\r\n|---|---|---|\r\n| `query` | `false` | List existing games and view the lobby page |\r\n| `create` | `false` | Create new crossword puzzles via the editor |\r\n| `delete` | `false` | Delete existing crossword puzzles |\r\n| `edit` | `false` | Modify game cells and set player names via WebSocket |\r\n| `lifetime` | `86400000` (24h) | Cookie lifetime in milliseconds |\r\n\r\nAt minimum `query` and `edit` should be enabled for a functional game. Parameters can also be set per-request through `params` when dispatching to the module. Request parameter override the corresponding default, allowing parent modules to implement authentication or per-route access policies.\r\n\r\n## Endpoints\r\n\r\nThe `Endpoints` export provides the path constants used by the module. All paths are relative to the module's mount point.\r\n\r\n| Path | Method | Description |\r\n|---|---|---|\r\n| `/` | GET | Game lobby: list, create, and delete crosswords |\r\n| `/play` | GET | Play/solve a crossword collaboratively (query param: `game`) |\r\n| `/editor` | GET | Create a new crossword layout |\r\n| `/games` | GET | JSON array of available game names |\r\n| `/game/{name}` | POST | Create a new game (JSON body with `width`, `height`, `grid`) |\r\n| `/game/{name}` | DELETE | Delete an existing game |\r\n| `/static/*` | GET | Static assets (CSS, JS) served with immutable cache headers |\r\n| `/ws/{name}` | WebSocket | Join a game session |\r\n\r\n## WebSocket Protocol\r\n\r\nClients connect to `/ws/{name}` to join a game session. The server sends the full game state on connection and delta updates after every change. Clients send JSON commands to interact with the game.\r\n\r\n### Client Commands\r\n\r\n| Command | Fields | Description |\r\n|---|---|---|\r\n| `name` | `{ cmd: 'name', name: string }` | Set the player name (required before grid updates are accepted) |\r\n| `update` | `{ cmd: 'update', data: GridCell[], id: number }` | Push a delta grid update; each cell includes an `index` field identifying its position in the linearized grid (`x + y * width`); `id` is a monotonically increasing ack-stamp |\r\n\r\n### Server Messages\r\n\r\nThe server sends one of three message types: a `GameState` object, an `Ack` object, or a string error identifier.\r\n\r\n- **`GameState`** object: `{ failed, delta, width, height, grid, online }` where `failed` indicates a write-back error, `delta` indicates whether `grid` contains a delta (only changed cells with `index` fields) or the full grid, and `online` lists currently connected player names.\r\n- **`Ack`** object: `{ ack: number }` confirming the server received a client `update` message with the given `id`.\r\n- **`\"unknown-game\"`**: the requested game does not exist.\r\n- **`\"corrupted-game\"`**: the game file could not be parsed.\r\n- **`\"dropped-game\"`**: the game was deleted while connected.\r\n- **`\"shutdown\"`**: the server is shutting down.\r\n\r\nAfter an error identifier is sent, the server closes the WebSocket.\r\n\r\n### Delta Encoding\r\n\r\nTo minimize bandwidth, grid updates use delta encoding. Instead of transmitting the full grid on every change, only the modified cells are sent - both from client to server and from server to clients. Each cell in a delta carries an `index` field indicating its position in the linearized grid array (`x + y * width`).\r\n\r\nThe server sends the full grid state (`delta: false`) on initial connection and uses delta messages (`delta: true`) for subsequent broadcasts. Non-grid changes (player joins, name changes, disconnects) are broadcast as empty deltas that only update the `online` list.\r\n\r\n### Conflict Resolution\r\n\r\nEach cell carries a timestamp. When a client pushes an update, only cells with a strictly newer timestamp than the server's current state are applied. Cells with equal or older timestamps are silently discarded. This ensures that concurrent edits from multiple players converge without explicit locking.\r\n\r\n## Game Rules\r\n\r\n- Grid dimensions: 1x1 to 64x64\r\n- Game names: alphanumeric with hyphens, dots, spaces, and underscores (max 64 characters)\r\n- Characters: uppercase A-Z only (lowercase input is uppercased; non-letter input is rejected)\r\n- Solid cells cannot be modified\r\n- Unnamed players cannot update the grid\r\n- Crossword numbering is assigned automatically based on standard crossword conventions (a cell gets a number if it starts a horizontal or vertical word)\r\n- Max upload size: 100 KB\r\n\r\n## Persistence\r\n\r\nGames are stored as JSON files (`{name}.json`) in the data directory. Writebacks are debounced by 60 seconds after the last change. When all clients disconnect, any pending changes are flushed immediately before the game is unloaded from memory. A retention timer keeps the game loaded briefly after the last disconnect to handle quick reconnections.\r\n\r\nIf a writeback fails, the game state notifies all connected clients via the `failed` flag. The server retries the writeback on the next debounce cycle. If all clients disconnect while the writeback is still failing, the in-memory state is lost and a warning is logged.\r\n\r\n## Cookies\r\n\r\nThe `Cookies` export provides the cookie name constants used by the module. The play page stores the last used player name in a cookie (`crossword-last-name`, configurable lifetime, default 24 hours) so it can be pre-filled on the next visit.\r\n","readmeFilename":"README.md"}