clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / mods / worlds / crate-works.cjs
crate-works.cjs 6.2K
1// Crate Works: a warehouse world about Sokoban pushing under editable rules.2//3// Levels are drawn as ASCII maps (see `board` below) instead of hand-counted4// coordinates. One character is one cell. World bible:5// docs/worlds/crate-works.md6const { word, object, level } = require('@miau/engine');78// Things are symbols, rule words are uppercase letters.9// . empty floor # WALL @ MIAU $ CRATE10// & TRUCK ~ GAP11// M MIAU C CRATE W WALL T TRUCK G GAP12// I IS A AND Y YOU P PUSH S STOP N WIN K SINK13const LEGEND = {14 '.': [],15 '#': ['obj:WALL'], '@': ['obj:MIAU'], '$': ['obj:CRATE'], '&': ['obj:TRUCK'], '~': ['obj:GAP'],16 M: ['MIAU'], C: ['CRATE'], W: ['WALL'], T: ['TRUCK'], G: ['GAP'],17 I: ['IS'], A: ['AND'], Y: ['YOU'], P: ['PUSH'], S: ['STOP'], N: ['WIN'], K: ['SINK'],18};1920// board(rows, meta) turns equal-length strings into a level. Object ids are21// derived from the level id and the starting cell, so they stay stable as22// long as the map does.23function board(rows, meta) {24 const width = rows[0].length;25 const objects = [];26 rows.forEach((row, y) => {27 if (row.length !== width) throw new Error(`${meta.id}: row ${y} is ${row.length} wide, expected ${width}`);28 [...row].forEach((ch, x) => {29 const specs = LEGEND[ch];30 if (!specs) throw new Error(`${meta.id}: unknown map character '${ch}' at column ${x}, row ${y}`);31 specs.forEach((spec, n) => {32 const id = `${meta.id}-${x}-${y}${n ? `-${n}` : ''}`;33 objects.push(spec.startsWith('obj:') ? object(spec.slice(4), x, y, id) : word(spec, x, y, id));34 });35 });36 });37 return level(width, rows.length, objects, meta);38}3940module.exports = {41 manifest: {42 id: 'crate-works',43 name: 'Crate Works',44 version: '0.1.0',45 engine: 1,46 rules: 2, // authored against post-W6 rule semantics (D23)47 author: 'Crate Works world team',48 description: 'A loading dock where crates, gaps, trucks, and the rules that hold them can all be shoved.',49 world: { glyph: '📦', tagline: 'Every crate goes somewhere. So does every word.', accent: 'push' },50 },51 vocabulary: ['MIAU', 'CRATE', 'TRUCK', 'WALL', 'GAP'],52 palette: {53 MIAU: { glyph: '🐈', faces: 'left' },54 CRATE: '📦',55 TRUCK: '🚚',56 WALL: '🧱',57 GAP: '🚧',58 },59 levels: [60 // 1. A crate blocks the only doorway; WIN waits on the far side.61 board([62 'MIY..WIS..CIP',63 '#############',64 '.....#.......',65 '.@..$....N...',66 '.....#.......',67 '.....#..TI..&',68 '.....#.......',69 ], {70 id: 'crates-move',71 title: 'Crates move',72 hint: 'The crate is in the doorway. TRUCK IS is waiting for a word.',73 category: 'required',74 introduces: ['CRATE', 'TRUCK', 'PUSH'],75 }),7677 // 2. Every rule sits across the gap line. Sink the crate to make a bridge.78 board([79 '......~MIY..',80 '......~.....',81 '.@....~CIP..',82 '....$.~.....',83 '......~GIK..',84 '......~....&',85 '......~TI.N.',86 ], {87 id: 'fill-the-gap',88 title: 'Fill the gap',89 hint: 'A gap swallows whatever goes in, and closes behind it.',90 category: 'required',91 introduces: ['GAP', 'SINK'],92 }),9394 // 3. Two gaps deep, one crate, and one loose word: PUSH. The crate has to95 // go in while it is still pushable; PUSH goes in last.96 board([97 'MIY#WIS#GIK#C#',98 '...~~......#I#',99 '&..~~.......P.',100 '...~~...$.....',101 'TIN~~.......@.',102 ], {103 id: 'spend-it-last',104 title: 'Spend it last',105 hint: 'Words sink too, and take their rule with them. Pick the order.',106 category: 'required',107 introduces: ['sinking words'],108 reuses: ['GAP', 'SINK', 'PUSH'],109 }),110111 // 4. CRATE AND MIAU IS YOU. The crate climbs its lane while the cat must112 // keep clear of the sentence; walls hold one still while the other moves.113 board([114 '#....#####&',115 '..MIY#####.',116 '.....###...',117 '.A...###.##',118 'C....###...',119 '.....#####.',120 '..@..#$....',121 '###########',122 'WIS..TIN...',123 ], {124 id: 'in-step',125 mustForm: ['CRATE IS YOU'],126 title: 'In step',127 hint: 'Two things can answer to YOU. A wall holds one still while the other moves.',128 category: 'required',129 introduces: ['AND', 'shared YOU'],130 }),131132 // 5. A tiny storeroom: two crates, two gaps, and the WIN word in front of133 // them. Reorder the load so the word crosses last.134 board([135 'MIY.GIK.CIP.',136 '###.....#...',137 'WIS#####T###',138 '#....###I###',139 '#..$N~~...##',140 '#@.$.##&####',141 '#....#######',142 ], {143 id: 'load-order',144 title: 'Load order',145 hint: 'WIN has to cross too. Nothing crosses a gap until something fills it.',146 category: 'optional',147 introduces: ['push order'],148 reuses: ['GAP', 'SINK', 'PUSH'],149 }),150151 // 6. TRUCK IS PUSH AND WIN: you can never stand on it. Push it onto a152 // crate, then make the crate YOU (or the other way round).153 board([154 'WIS.MIY.TIPAN',155 '#############',156 '#....#......#',157 '#.&..#.@C...#',158 '#..#.....#..#',159 '##.#######.##',160 '#$.........IY',161 '#############',162 ], {163 id: 'back-it-up',164 mustForm: ['CRATE IS YOU'],165 title: 'Back it up',166 hint: 'The truck rolls away from you. It would not roll away from something standing still.',167 category: 'optional',168 introduces: ['pushable WIN'],169 reuses: ['PUSH', 'shared YOU'],170 }),171172 // 7. The truck sits behind a moat two gaps thick and there is one loose173 // word on the board. Take CRATE out of CRATE IS PUSH and finish GAP IS174 // CRATE: every gap becomes a crate nobody can push, so you walk through.175 board([176 'WIS.MIY.GIK.',177 '########I###',178 '#........###',179 '#.###.######',180 '#..C.....~~~',181 '#.#I#.#..~~~',182 '#.#P#.#.~~.T',183 '#.###.#.~~&I',184 '#@......~~.N',185 ], {186 id: 'night-shift',187 mustForm: ['GAP IS CRATE'],188 title: 'Night shift',189 hint: 'GAP IS has room for one more word. Crates only block you while they are PUSH.',190 category: 'challenge',191 introduces: ['noun transform'],192 reuses: ['GAP', 'SINK', 'PUSH'],193 }),194 ],195};