silver-tongue 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > You arrive speaking pidgin; you leave with a silver tongue.
4
4
 
5
- A language-learning life game. You arrive in a new city knowing a few words and earn your living by understanding people. Every job, purchase and conversation happens in the language you're learning.
5
+ A language-learning life game. Your phone is dead, your wallet is gone, and you're standing on a street in a Chinese city with ¥20 in your pocket. An old man on a bench teaches you your first words; after that you earn your living by understanding people. Every job, purchase and conversation happens in the language you're learning.
6
6
 
7
7
  The first course is Mandarin (HSK 1), in a Chinese city, explained in English.
8
8
 
@@ -15,7 +15,7 @@ npx silver-tongue
15
15
  | Key | Does |
16
16
  |---|---|
17
17
  | `1`–`9` | choose from the menu, or pick a reply |
18
- | `w` | word help: look up a word from the last line |
18
+ | `w` | word help: look up a word from the last line or the replies; `s` explains the whole sentence |
19
19
  | `enter` / `⌫` | say / undo, when building a reply from tiles |
20
20
  | `esc` | back |
21
21
  | `q` | save and quit (from the menu) |
@@ -1 +1 @@
1
- {"id":"zh-china-en","typing":false,"words":{"w0003":{"id":"w0003","w":"三","lv":"1","gloss":"three; 3","pron":"sān"},"w0009":{"id":"w0009","w":"不","lv":"1","gloss":"no; not","pron":"bù"},"w0020":{"id":"w0020","w":"五","lv":"1","gloss":"five; 5","pron":"wǔ"},"w0028":{"id":"w0028","w":"你","lv":"1","gloss":"you (informal)","pron":"nǐ"},"w0037":{"id":"w0037","w":"几","lv":"1","gloss":"how much; how many","pron":"jǐ"},"w0051":{"id":"w0051","w":"吗","lv":"1","gloss":"(question particle for \"yes-no\" questions)","pron":"ma"},"w0060":{"id":"w0060","w":"四","lv":"1","gloss":"four; 4","pron":"sì"},"w0072":{"id":"w0072","w":"好","lv":"1","gloss":"good; appropriate; proper","pron":"hǎo"},"w0084":{"id":"w0084","w":"工作","lv":"1","gloss":"to work; (of a machine) to operate","pron":"gōngzuò"},"w0099":{"id":"w0099","w":"是","lv":"1","gloss":"to be (followed by substantives only); correct; right; true","pron":"shì"},"w0108":{"id":"w0108","w":"水","lv":"1","gloss":"water; river","pron":"shuǐ"},"w0133":{"id":"w0133","w":"茶","lv":"1","gloss":"tea; tea plant","pron":"chá"},"x0001":{"id":"x0001","w":"杯","lv":"1","gloss":"cup; glass (measure word for drinks)","pron":"bēi","bonus":true}},"concepts":{"tea":["w0133"],"water":["w0108"],"three":["w0003"],"four":["w0060"],"five":["w0020"]},"groups":{"drinks":["tea","water"],"numbers_3_5":["three","four","five"]},"world":{"start":"street","currency":"¥","slotsPerDay":4,"startWallet":20,"foodPerDay":5,"rentPerWeek":50,"places":{"street":{"links":["noodle_shop"]},"noodle_shop":{"links":["street"]}},"npcs":{"cook":{"place":"noodle_shop"}}},"scenes":[{"id":"noodle-intro","place":"noodle_shop","npc":"cook","stage":1,"after":[],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"greet","slots":{},"expect":{"action":"greet"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}]},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}]}}}},{"id":"job","slots":{},"expect":{"action":"accept"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"工作,好吗?","tokens":[{"start":0,"end":2,"word":"w0084"},{"start":3,"end":4,"word":"w0072"},{"start":4,"end":5,"word":"w0051"}]},"reply":{"text":"好。","tokens":[{"start":0,"end":1,"word":"w0072"}]}}}}]},{"id":"noodle-shift","place":"noodle_shop","npc":"cook","stage":1,"after":["noodle-intro"],"requires":{"trust":{"cook":1}},"repeatable":true,"trustGain":1,"exchanges":[{"id":"drink","slots":{"item":"drinks"},"expect":{"action":"fetch","item":"$item"},"hinges":["$item"],"pay":1,"missCost":1,"variants":{"item=tea":{"npc":{"text":"茶。","tokens":[{"start":0,"end":1,"word":"w0133"}]},"reply":{"text":"好,茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0133"}]}},"item=water":{"npc":{"text":"水。","tokens":[{"start":0,"end":1,"word":"w0108"}]},"reply":{"text":"好,水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0108"}]}}}},{"id":"order","slots":{"item":"drinks","count":"numbers_3_5"},"expect":{"action":"serve","item":"$item","count":"$count"},"hinges":["$item","$count"],"pay":3,"missCost":2,"variants":{"count=three|item=tea":{"npc":{"text":"三杯茶。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}]},"reply":{"text":"好,三杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}]},"rephrase":{"text":"茶。三杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}]}},"count=three|item=water":{"npc":{"text":"三杯水。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}]},"reply":{"text":"好,三杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}]},"rephrase":{"text":"水。三杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}]}},"count=four|item=tea":{"npc":{"text":"四杯茶。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}]},"reply":{"text":"好,四杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}]},"rephrase":{"text":"茶。四杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}]}},"count=four|item=water":{"npc":{"text":"四杯水。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}]},"reply":{"text":"好,四杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}]},"rephrase":{"text":"水。四杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}]}},"count=five|item=tea":{"npc":{"text":"五杯茶。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}]},"reply":{"text":"好,五杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}]},"rephrase":{"text":"茶。五杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}]}},"count=five|item=water":{"npc":{"text":"五杯水。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}]},"reply":{"text":"好,五杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}]},"rephrase":{"text":"水。五杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}]}}}}]}],"reactions":{"wrong-generic":{"text":"不是。","tokens":[{"start":0,"end":1,"word":"w0009"},{"start":1,"end":2,"word":"w0099"}]},"wrong-count":{"text":"几杯?","tokens":[{"start":0,"end":1,"word":"w0037"},{"start":1,"end":2,"word":"x0001"}]}},"learnerFtl":"# Text-game UI. Every id here is required by packages/tui (UI_KEYS).\n\nhud = Day { $day } · slot { $slot }/{ $slots } · { $currency }{ $wallet } · { $rank }\n\nrank-0 = Pidgin\nrank-1 = Getting By\nrank-2 = Conversational\nrank-3 = Fluent\nrank-4 = Silver Tongue\n\nmenu-title = What now?\nmenu-talk = Talk to { $npc }: { $scene }\nmenu-go = Go to { $place }\nmenu-sleep = Sleep (end the day)\nmenu-quit = Save and quit\n\nkeys-explore = [{ $keys }] choose · [q] quit\nkeys-pick = [{ $keys }] reply · [w] word help\nkeys-tiles = [{ $keys }] add tile · [⌫] undo · [enter] say it · [w] word help\nkeys-help = [{ $keys }] look up · [esc] back\n\nhelp-title = Which word?\nreply-title = Your reply:\ntiles-answer = You say:\nyou = You\n\nmismatch = That's not what they asked for.\nrephrased = (slower)\nwallet-change = { $sign }{ $currency }{ $amount } ({ $reason })\nreason-wages = wages\nreason-mixup = mix-up\nreason-food = food\nreason-rent = rent\ntrust-up = { $npc } trusts you a little more ({ $trust }).\nscene-done = Done. You earned { $currency }{ $earned }.\nunlocked = New: { $scene }\nrank-up = You're now: { $rank }\nday-ended = Day { $day } is over. You sleep.\nreject-unknown-scene = There's nobody here for that.\nreject-in-scene = Finish the conversation first.\nreject-wrong-place = They're not here.\nreject-locked = They're not ready to talk about that yet.\nreject-no-slots = You're out of time today. Sleep first.\nreject-stale-run = That conversation can't continue. Start it again.\nreject-no-pick = Choose a reply with the number keys.\nreject-bad-choice = There's no reply with that number.\nreject-no-tiles = Build your reply from the tiles.\nreject-bad-tile = There's no tile with that number.\nreject-not-linked = You can't get there from here.\nreject-unknown-word = That word isn't in the dictionary.\nnotice-read-only = Your progress can't be saved on this computer, so this session won't be kept.\nnotice-bad-save = Your save couldn't be read. It was kept as a backup and a new game started.\n\nplace-street = The street\nplace-street-desc = Bikes, steam and shouting. A noodle shop glows across the road.\nplace-noodle_shop = Noodle shop\nplace-noodle_shop-desc = Steam everywhere. The cook waves you over.\n\nnpc-cook = Cook\n\nscene-noodle-intro = Get a job\nscene-noodle-shift = Work a shift\n"}
1
+ {"id":"zh-china-en","typing":false,"words":{"w0003":{"id":"w0003","w":"三","lv":"1","gloss":"three; 3","pron":"sān"},"w0009":{"id":"w0009","w":"不","lv":"1","gloss":"no; not","pron":"bù"},"w0010":{"id":"w0010","w":"不客气","lv":"1","gloss":"you're welcome; don't mention it","pron":"bùkèqi"},"w0020":{"id":"w0020","w":"五","lv":"1","gloss":"five; 5","pron":"wǔ"},"w0028":{"id":"w0028","w":"你","lv":"1","gloss":"you (informal)","pron":"nǐ"},"w0034":{"id":"w0034","w":"再见","lv":"1","gloss":"goodbye","pron":"zàijiàn"},"w0037":{"id":"w0037","w":"几","lv":"1","gloss":"how much; how many","pron":"jǐ"},"w0051":{"id":"w0051","w":"吗","lv":"1","gloss":"(question particle for \"yes-no\" questions)","pron":"ma"},"w0060":{"id":"w0060","w":"四","lv":"1","gloss":"four; 4","pron":"sì"},"w0072":{"id":"w0072","w":"好","lv":"1","gloss":"good; appropriate; proper","pron":"hǎo"},"w0079":{"id":"w0079","w":"对不起","lv":"1","gloss":"I'm sorry; excuse me; I beg your pardon","pron":"duìbuqǐ"},"w0084":{"id":"w0084","w":"工作","lv":"1","gloss":"to work; (of a machine) to operate","pron":"gōngzuò"},"w0099":{"id":"w0099","w":"是","lv":"1","gloss":"to be (followed by substantives only); correct; right; true","pron":"shì"},"w0108":{"id":"w0108","w":"水","lv":"1","gloss":"water; river","pron":"shuǐ"},"w0112":{"id":"w0112","w":"没关系","lv":"1","gloss":"it doesn't matter","pron":"méiguānxi"},"w0133":{"id":"w0133","w":"茶","lv":"1","gloss":"tea; tea plant","pron":"chá"},"w0141":{"id":"w0141","w":"谢谢","lv":"1","gloss":"to thank; thanks","pron":"xièxie"},"x0001":{"id":"x0001","w":"杯","lv":"1","gloss":"cup; glass (measure word for drinks)","pron":"bēi","bonus":true}},"concepts":{"tea":["w0133"],"water":["w0108"],"three":["w0003"],"four":["w0060"],"five":["w0020"],"hello":["w0028","w0072"],"thanks":["w0141"],"sorry":["w0079"],"bye":["w0034"]},"groups":{"courtesies":["hello","thanks","sorry","bye"],"drinks":["tea","water"],"numbers_3_5":["three","four","five"]},"world":{"start":"street","currency":"¥","slotsPerDay":4,"startWallet":20,"foodPerDay":5,"rentPerWeek":50,"places":{"street":{"links":["noodle_shop"]},"noodle_shop":{"links":["street"]}},"npcs":{"wang":{"place":"street"},"cook":{"place":"noodle_shop"}}},"scenes":[{"id":"noodle-intro","place":"noodle_shop","npc":"cook","stage":1,"after":["street-hello"],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"greet","slots":{},"expect":{"action":"greet"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"job","slots":{},"expect":{"action":"accept"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"工作,好吗?","tokens":[{"start":0,"end":2,"word":"w0084"},{"start":3,"end":4,"word":"w0072"},{"start":4,"end":5,"word":"w0051"}],"meaning":"Work here — OK?"},"reply":{"text":"好。","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"OK."}}}}]},{"id":"noodle-shift","place":"noodle_shop","npc":"cook","stage":1,"after":["noodle-intro"],"requires":{"trust":{"cook":1}},"repeatable":true,"trustGain":1,"exchanges":[{"id":"drink","slots":{"item":"drinks"},"expect":{"action":"fetch","item":"$item"},"hinges":["$item"],"pay":1,"missCost":1,"variants":{"item=tea":{"npc":{"text":"茶。","tokens":[{"start":0,"end":1,"word":"w0133"}],"meaning":"Tea."},"reply":{"text":"好,茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0133"}],"meaning":"OK, tea."}},"item=water":{"npc":{"text":"水。","tokens":[{"start":0,"end":1,"word":"w0108"}],"meaning":"Water."},"reply":{"text":"好,水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0108"}],"meaning":"OK, water."}}}},{"id":"order","slots":{"item":"drinks","count":"numbers_3_5"},"expect":{"action":"serve","item":"$item","count":"$count"},"hinges":["$item","$count"],"pay":3,"missCost":2,"variants":{"count=three|item=tea":{"npc":{"text":"三杯茶。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Three cups of tea."},"reply":{"text":"好,三杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, three cups of tea."},"rephrase":{"text":"茶。三杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Three cups."}},"count=three|item=water":{"npc":{"text":"三杯水。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Three cups of water."},"reply":{"text":"好,三杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, three cups of water."},"rephrase":{"text":"水。三杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Three cups."}},"count=four|item=tea":{"npc":{"text":"四杯茶。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Four cups of tea."},"reply":{"text":"好,四杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, four cups of tea."},"rephrase":{"text":"茶。四杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Four cups."}},"count=four|item=water":{"npc":{"text":"四杯水。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Four cups of water."},"reply":{"text":"好,四杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, four cups of water."},"rephrase":{"text":"水。四杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Four cups."}},"count=five|item=tea":{"npc":{"text":"五杯茶。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Five cups of tea."},"reply":{"text":"好,五杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, five cups of tea."},"rephrase":{"text":"茶。五杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Five cups."}},"count=five|item=water":{"npc":{"text":"五杯水。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Five cups of water."},"reply":{"text":"好,五杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, five cups of water."},"rephrase":{"text":"水。五杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Five cups."}}}}]},{"id":"street-hello","place":"street","npc":"wang","stage":1,"after":[],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"hello","slots":{},"expect":{"action":"greet"},"hinges":["hello"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"polite","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}},"said=bye":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}},{"id":"polite-again","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}},"said=bye":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}},{"id":"bye","slots":{},"expect":{"action":"farewell"},"hinges":["bye"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}}]},{"id":"street-practice","place":"street","npc":"wang","stage":1,"after":["street-hello"],"requires":{},"repeatable":true,"trustGain":1,"exchanges":[{"id":"polite-1","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}},"said=bye":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}},{"id":"polite-2","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}},"said=bye":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}},{"id":"polite-3","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}},"said=bye":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}}]}],"reactions":{"wrong-generic":{"text":"不是。","tokens":[{"start":0,"end":1,"word":"w0009"},{"start":1,"end":2,"word":"w0099"}],"meaning":"No."},"wrong-count":{"text":"几杯?","tokens":[{"start":0,"end":1,"word":"w0037"},{"start":1,"end":2,"word":"x0001"}],"meaning":"How many cups?"}},"learnerFtl":"# Text-game UI. Every id here is required by packages/tui (UI_KEYS).\n\nhud = Day { $day } · slot { $slot }/{ $slots } · { $currency }{ $wallet } · { $rank }\n\nrank-0 = Pidgin\nrank-1 = Getting By\nrank-2 = Conversational\nrank-3 = Fluent\nrank-4 = Silver Tongue\n\nmenu-title = What now?\nmenu-talk = Talk to { $npc }: { $scene }\nmenu-go = Go to { $place }\nmenu-sleep = Sleep (end the day)\nmenu-quit = Save and quit\n\nkeys-explore = [{ $keys }] choose · [q] quit\nkeys-pick = [{ $keys }] reply · [w] word help\nkeys-tiles = [{ $keys }] add tile · [⌫] undo · [enter] say it · [w] word help\nkeys-help = [{ $keys }] look up · [esc] back\nkeys-help-sentence = [{ $keys }] look up · [s] whole sentence · [esc] back\n\nhelp-title = Which word?\nhelp-sentence = The whole sentence\nhelp-in-replies = In the replies:\nreply-title = Your reply:\ntiles-answer = You say:\nyou = You\n\nmismatch = That's not what they asked for.\nrephrased = (slower)\nwallet-change = { $sign }{ $currency }{ $amount } ({ $reason })\nreason-wages = wages\nreason-mixup = mix-up\nreason-food = food\nreason-rent = rent\ntrust-up = { $npc } trusts you a little more ({ $trust }).\nscene-done = Done. You earned { $currency }{ $earned }.\nunlocked = New: { $scene }\nrank-up = You're now: { $rank }\nday-ended = Day { $day } is over. You sleep.\nreject-unknown-scene = There's nobody here for that.\nreject-in-scene = Finish the conversation first.\nreject-wrong-place = They're not here.\nreject-locked = They're not ready to talk about that yet.\nreject-no-slots = You're out of time today. Sleep first.\nreject-stale-run = That conversation can't continue. Start it again.\nreject-no-pick = Choose a reply with the number keys.\nreject-bad-choice = There's no reply with that number.\nreject-no-tiles = Build your reply from the tiles.\nreject-bad-tile = There's no tile with that number.\nreject-not-linked = You can't get there from here.\nreject-unknown-word = That word isn't in the dictionary.\nnotice-read-only = Your progress can't be saved on this computer, so this session won't be kept.\nnotice-bad-save = Your save couldn't be read. It was kept as a backup and a new game started.\n\n# The story. intro-1, intro-2, … open a new game ($currency, $wallet, $rent are available).\nintro-1 = Your phone is dead, your wallet is gone, and the bus that brought you here left an hour ago.\nintro-2 = You're standing on a street in a Chinese city with { $currency }{ $wallet } in your pocket. You know nobody, and nobody here speaks your language.\nintro-3 = Food costs money. Rent is { $currency }{ $rent }, due at the end of the week. To earn anything you'll need work, and for work you'll need words.\nintro-4 = On a bench by the road, an old man is watching you with open curiosity. Maybe start there.\n\nplace-street = The street\nplace-street-desc = Bikes, steam and shouting. An old man sits on a bench by the road. Across it, a noodle shop glows.\nplace-noodle_shop = Noodle shop\nplace-noodle_shop-desc = Steam everywhere. The cook is shouting orders at nobody in particular.\n\nnpc-wang = Old Wang\nnpc-cook = Cook\n\n# Scene names for the menu; scene-<id>-start and scene-<id>-end are optional narration.\nscene-street-hello = Say hello\nscene-street-hello-start = The old man pats the bench beside him. \"Wang,\" he says, pointing at himself. He seems to have decided you need lessons: he says something, you answer. Use [w] when you're stuck.\nscene-street-hello-end = Old Wang beams. He points across the road at the noodle shop, mimes slurping noodles, then rubs his fingers together. Work. Money.\n\nscene-street-practice = Practise greetings\nscene-street-practice-start = Old Wang shuffles over to make room. Another lesson, then.\nscene-street-practice-end = Old Wang nods, satisfied, and goes back to watching the street.\n\nscene-noodle-intro = Ask about work\nscene-noodle-intro-start = The cook looks you up and down, wiping her hands on her apron.\nscene-noodle-intro-end = She throws you an apron. You have a job, sort of.\n\nscene-noodle-shift = Work a shift\nscene-noodle-shift-start = Orders fly across the counter. Keep up.\nscene-noodle-shift-end = The rush dies down. The cook counts coins into your hand.\n"}
@@ -1167,14 +1167,14 @@ function pickOptions(ctx, ex, combo) {
1167
1167
  }
1168
1168
  function buildTiles(ctx, ex, combo) {
1169
1169
  const rightKey = comboKey(combo);
1170
- const pieces = tilePieces(ex.variants[rightKey].reply);
1170
+ const pieces2 = tilePieces(ex.variants[rightKey].reply);
1171
1171
  const extra = /* @__PURE__ */ new Set();
1172
1172
  for (const key of shuffle(Object.keys(ex.variants), ctx.rng)) {
1173
1173
  if (key === rightKey) continue;
1174
- for (const p of tilePieces(ex.variants[key].reply)) if (!pieces.includes(p)) extra.add(p);
1174
+ for (const p of tilePieces(ex.variants[key].reply)) if (!pieces2.includes(p)) extra.add(p);
1175
1175
  if (extra.size >= 2) break;
1176
1176
  }
1177
- return shuffle([...pieces, ...[...extra].slice(0, 2)], ctx.rng);
1177
+ return shuffle([...pieces2, ...[...extra].slice(0, 2)], ctx.rng);
1178
1178
  }
1179
1179
  function optionsEvent(ex, run) {
1180
1180
  return run.mode === "pick" ? { type: "replyOptions", mode: "pick", options: run.options.map((k) => ex.variants[k].reply) } : { type: "replyOptions", mode: "tiles", tiles: run.tiles };
@@ -1318,9 +1318,9 @@ function isRunShape(x) {
1318
1318
  if (!["pick", "tiles", "type"].includes(x.mode) || !isStrings(x.options) || !isStrings(x.tiles)) return false;
1319
1319
  return ["exchange", "misses", "earned", "mixups"].every((k) => isCount(x[k]));
1320
1320
  }
1321
- function tilesCover(tiles, pieces) {
1321
+ function tilesCover(tiles, pieces2) {
1322
1322
  const left = [...tiles];
1323
- return pieces.every((p) => {
1323
+ return pieces2.every((p) => {
1324
1324
  const i = left.indexOf(p);
1325
1325
  return i >= 0 && left.splice(i, 1).length === 1;
1326
1326
  });
@@ -1439,16 +1439,67 @@ function fitLine(line, cols) {
1439
1439
  if (used < cols) out.push({ text: " ".repeat(cols - used) });
1440
1440
  return out;
1441
1441
  }
1442
+ function pieces(line) {
1443
+ const out = [];
1444
+ for (const span of line) {
1445
+ let cur = "";
1446
+ const flush = () => {
1447
+ if (cur) out.push({ text: cur, span });
1448
+ cur = "";
1449
+ };
1450
+ for (const ch of span.text) {
1451
+ const wide = charWidth(ch.codePointAt(0)) === 2;
1452
+ if (ch === " ") cur += ch;
1453
+ else if (wide || cur.endsWith(" ")) {
1454
+ flush();
1455
+ cur = ch;
1456
+ if (wide) flush();
1457
+ } else cur += ch;
1458
+ }
1459
+ flush();
1460
+ }
1461
+ return out;
1462
+ }
1463
+ function wrapLine(line, cols) {
1464
+ const lines = [];
1465
+ let cur = [];
1466
+ let curFrom;
1467
+ let used = 0;
1468
+ const finish = () => {
1469
+ const last = cur.at(-1);
1470
+ if (last) last.text = last.text.replace(/ +$/, "");
1471
+ lines.push(cur.filter((s) => s.text));
1472
+ cur = [];
1473
+ curFrom = void 0;
1474
+ used = 0;
1475
+ };
1476
+ const add = ({ text, span }) => {
1477
+ if (used > 0 && used + strWidth(text.replace(/ +$/, "")) > cols) finish();
1478
+ if (used === 0 && lines.length > 0) text = text.replace(/^ +/, "");
1479
+ if (!text) return;
1480
+ if (curFrom === span) cur[cur.length - 1].text += text;
1481
+ else cur.push({ ...span, text });
1482
+ curFrom = span;
1483
+ used += strWidth(text);
1484
+ };
1485
+ for (const p of pieces(line)) {
1486
+ if (strWidth(p.text.replace(/ +$/, "")) > cols) for (const ch of p.text) add({ text: ch, span: p.span });
1487
+ else add(p);
1488
+ }
1489
+ finish();
1490
+ return lines;
1491
+ }
1442
1492
 
1443
1493
  // ../tui/src/text.ts
1444
1494
  var import_bundle = __toESM(require_bundle(), 1);
1445
1495
  function makeText(ftl, locale = "en") {
1446
1496
  const bundle = new import_bundle.FluentBundle(locale, { useIsolating: false });
1447
1497
  bundle.addResource(new import_bundle.FluentResource(ftl));
1448
- return (id, args) => {
1498
+ const t = (id, args) => {
1449
1499
  const msg = bundle.getMessage(id);
1450
1500
  return msg?.value ? bundle.formatPattern(msg.value, args ?? {}, []) : id;
1451
1501
  };
1502
+ return Object.assign(t, { has: (id) => !!bundle.getMessage(id)?.value });
1452
1503
  }
1453
1504
  var UI_KEYS = {
1454
1505
  hud: ["day", "slot", "slots", "currency", "wallet", "rank"],
@@ -1466,6 +1517,9 @@ var UI_KEYS = {
1466
1517
  "keys-pick": ["keys"],
1467
1518
  "keys-tiles": ["keys"],
1468
1519
  "keys-help": ["keys"],
1520
+ "keys-help-sentence": ["keys"],
1521
+ "help-sentence": [],
1522
+ "help-in-replies": [],
1469
1523
  "help-title": [],
1470
1524
  "reply-title": [],
1471
1525
  "tiles-answer": [],
@@ -1524,9 +1578,10 @@ function border(left, label, right, fill, cols, rightLabel = "") {
1524
1578
  function renderScreen(m, cols, rows) {
1525
1579
  const inner = Math.max(1, cols - 4);
1526
1580
  const bodyRows = Math.max(1, rows - 2);
1527
- const prompt = m.prompt.slice(-bodyRows);
1581
+ const wrap = (lines) => lines.flatMap((l) => wrapLine(l, inner));
1582
+ const prompt = wrap(m.prompt).slice(-bodyRows);
1528
1583
  const logRows = Math.max(0, bodyRows - prompt.length - (prompt.length ? 1 : 0));
1529
- const log = logRows > 0 ? m.log.slice(-logRows) : [];
1584
+ const log = logRows > 0 ? wrap(m.log).slice(-logRows) : [];
1530
1585
  const body = [...Array(logRows - log.length).fill([]), ...log];
1531
1586
  if (prompt.length) body.push([]);
1532
1587
  body.push(...prompt);
@@ -1550,6 +1605,7 @@ function startApp(opts) {
1550
1605
  let replyMode = "pick";
1551
1606
  let tileInput = [];
1552
1607
  let lastLine = null;
1608
+ let resuming = false;
1553
1609
  const push = (...lines) => {
1554
1610
  log = [...log, ...lines].slice(-LOG_LIMIT);
1555
1611
  };
@@ -1560,6 +1616,16 @@ function startApp(opts) {
1560
1616
  ];
1561
1617
  const keyRange = (n) => n <= 1 ? "1" : `1-${Math.min(n, 9)}`;
1562
1618
  const echo = (text) => push([{ text: `${t("you")}: `, color: "green", bold: true }, { text }]);
1619
+ const narrate = (id, args) => {
1620
+ if (t.has(id)) push([{ text: t(id, args), dim: true }]);
1621
+ };
1622
+ function tellIntro() {
1623
+ const s = core2.state;
1624
+ const fresh = s.day === 1 && s.slot === 0 && !s.run && s.place === course2.world.start && !Object.keys(s.scenesDone).length && !Object.keys(s.words).length;
1625
+ if (!fresh) return;
1626
+ const args = { currency: course2.world.currency, wallet: s.wallet, rent: course2.world.rentPerWeek };
1627
+ for (let i = 1; t.has(`intro-${i}`); i++) push([{ text: t(`intro-${i}`, args) }], []);
1628
+ }
1563
1629
  function enterPlace(place) {
1564
1630
  push([], [{ text: t(`place-${place}`), bold: true }], [{ text: t(`place-${place}-desc`), dim: true }]);
1565
1631
  }
@@ -1575,6 +1641,7 @@ function startApp(opts) {
1575
1641
  case "sceneStarted":
1576
1642
  mode = "scene";
1577
1643
  push([]);
1644
+ if (!resuming) narrate(`scene-${e.scene}-start`);
1578
1645
  break;
1579
1646
  case "lineSpoken":
1580
1647
  lastLine = e.line;
@@ -1614,6 +1681,7 @@ function startApp(opts) {
1614
1681
  break;
1615
1682
  case "sceneEnded":
1616
1683
  mode = "explore";
1684
+ narrate(`scene-${e.scene}-end`);
1617
1685
  push([{ text: t("scene-done", { currency: course2.world.currency, earned: e.earned }), bold: true }]);
1618
1686
  break;
1619
1687
  case "unlocked":
@@ -1660,16 +1728,28 @@ function startApp(opts) {
1660
1728
  }
1661
1729
  function helpWords() {
1662
1730
  if (!lastLine) return [];
1663
- const line = lastLine;
1664
- return line.tokens.map((tk) => ({ text: line.text.slice(tk.start, tk.end), word: tk.word }));
1731
+ const pieces2 = (l) => l.tokens.map((tk) => ({ text: l.text.slice(tk.start, tk.end), word: tk.word }));
1732
+ const said = pieces2(lastLine).map((p) => ({ ...p, inReplies: false }));
1733
+ const seen = new Set(said.map((p) => p.word));
1734
+ const replies = replyMode === "pick" ? pickOptions2.flatMap(pieces2) : [];
1735
+ const extra = replies.filter((p) => !seen.has(p.word) && seen.add(p.word)).map((p) => ({ ...p, inReplies: true }));
1736
+ return [...said, ...extra].slice(0, 9);
1665
1737
  }
1666
1738
  function prompt(width) {
1667
1739
  if (mode === "explore") {
1668
1740
  return [[{ text: t("menu-title"), dim: true }], ...menu().map((m, i) => [{ text: `${i + 1}) ${m.label}` }])];
1669
1741
  }
1670
1742
  if (mode === "help") {
1671
- const words = helpWords().map((w, i) => `${i + 1}) ${w.text}`);
1672
- return [[{ text: t("help-title"), dim: true }], ...wrapItems(words, width)];
1743
+ const items = helpWords().map((w, i) => ({ ...w, label: `${i + 1}) ${w.text}` }));
1744
+ const said = items.filter((w) => !w.inReplies).map((w) => w.label);
1745
+ const inReplies = items.filter((w) => w.inReplies).map((w) => w.label);
1746
+ const sentence = lastLine?.meaning ? [[{ text: `s) ${t("help-sentence")}` }]] : [];
1747
+ return [
1748
+ [{ text: t("help-title"), dim: true }],
1749
+ ...wrapItems(said, width),
1750
+ ...sentence,
1751
+ ...inReplies.length ? [[{ text: t("help-in-replies"), dim: true }], ...wrapItems(inReplies, width)] : []
1752
+ ];
1673
1753
  }
1674
1754
  const title = [{ text: t("reply-title"), dim: true }];
1675
1755
  if (replyMode === "pick") return [title, ...pickOptions2.map((o, i) => [{ text: `${i + 1}) ` }, ...lineSpans(o, /* @__PURE__ */ new Set())])];
@@ -1694,7 +1774,7 @@ function startApp(opts) {
1694
1774
  wallet: s.wallet,
1695
1775
  rank: t(`rank-${rankFor(s.words, wordIds, opts.now())}`)
1696
1776
  });
1697
- const [footerId, count] = mode === "explore" ? ["keys-explore", menu().length] : mode === "help" ? ["keys-help", helpWords().length] : replyMode === "pick" ? ["keys-pick", pickOptions2.length] : ["keys-tiles", tiles.length];
1777
+ const [footerId, count] = mode === "explore" ? ["keys-explore", menu().length] : mode === "help" ? [lastLine?.meaning ? "keys-help-sentence" : "keys-help", helpWords().length] : replyMode === "pick" ? ["keys-pick", pickOptions2.length] : ["keys-tiles", tiles.length];
1698
1778
  const footer = t(footerId, { keys: keyRange(count) });
1699
1779
  term2.write(renderScreen({ title: t(`place-${s.place}`), hud, log, prompt: prompt(cols - 4), footer }, cols, rows));
1700
1780
  }
@@ -1716,6 +1796,14 @@ function startApp(opts) {
1716
1796
  { text: ` \u2014 ${w.gloss}` }
1717
1797
  ]);
1718
1798
  }
1799
+ if (key.name === "s" && lastLine?.meaning) {
1800
+ const pron = lastLine.tokens.flatMap((tk) => course2.words[tk.word]?.pron ?? []).join(" ");
1801
+ push([
1802
+ { text: lastLine.text, bold: true },
1803
+ ...pron ? [{ text: ` ${pron}`, color: "yellow" }] : [],
1804
+ { text: ` \u2014 ${lastLine.meaning}` }
1805
+ ]);
1806
+ }
1719
1807
  if (key.name === "escape" || key.name === "w") mode = "scene";
1720
1808
  } else if (key.name === "w") {
1721
1809
  mode = "help";
@@ -1735,8 +1823,11 @@ function startApp(opts) {
1735
1823
  render();
1736
1824
  }
1737
1825
  if (opts.notice) push([{ text: t(opts.notice), color: "yellow" }]);
1826
+ tellIntro();
1738
1827
  enterPlace(core2.state.place);
1828
+ resuming = true;
1739
1829
  apply(describeRun(course2, core2.state));
1830
+ resuming = false;
1740
1831
  term2.onKey(press);
1741
1832
  term2.onResize(render);
1742
1833
  render();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silver-tongue",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Learn a language by living in it: a text game for the terminal.",
5
5
  "type": "module",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/jamil314/silver-tongue.git", "directory": "packages/tui-node" },