diff --git a/data/entities.yaml b/data/entities.yaml index ba614270e..eab83b1f7 100644 --- a/data/entities.yaml +++ b/data/entities.yaml @@ -42,8 +42,11 @@ description: - A plain wooden workbench, providing space to make other things using the `make` command. - - 'Example:' - - 'make "log"' + - | + Example: + ``` + make "log" + ``` properties: [portable] capabilities: [make] @@ -343,10 +346,11 @@ description: - Employs hot lead typesetting to arrange glyphs into a mold for printing. - | - An equipped `linotype` device enables the `format` command: - - | - `format : a -> text` can turn any value into a suitable text - representation. + An equipped `linotype`{=entity} device enables the `format` command: + ``` + format : a -> text + ``` + which can turn any value into a suitable text representation. properties: [portable] capabilities: [format] @@ -359,7 +363,10 @@ - | Facilitates the concatenation of text values. - | - The infix operator `++ : text -> text -> text` + The infix operator + ``` + ++ : text -> text -> text + ``` can be used to concatenate two text values. For example, - | "Number of widgets: " ++ format numWidgets @@ -373,8 +380,10 @@ description: - Simple, yet accurate measuring device. Can determine the length of a text value. - | - `chars : text -> int` computes the number of characters in a - `text` value. + ``` + chars : text -> int + ``` + computes the number of characters in a `text`{=type} value. properties: [portable] capabilities: [charcount] @@ -385,10 +394,11 @@ description: - A simple machine for the textually-inclined; plain but effective. - | - An equipped `wedge` enables the `split` command: - - | - `split : int -> text -> text * text` splits a `text` value into - two pieces, one before the given index and one after. + An equipped `wedge`{=entity} enables the `split` command: + ``` + split : int -> text -> text * text + ``` + splits a `text`{=type} value into two pieces, one before the given index and one after. properties: [portable] capabilities: [split] @@ -401,7 +411,7 @@ information, made of twisted cotton fibers. Multiple strings can also be woven into larger configurations such as cloth or nets. - | - An equipped `string` device enables several commands for working with + An equipped `string`{=entity} device enables several commands for working with `text` values: - | `format : a -> text` can turn any value into a suitable text @@ -410,7 +420,9 @@ The infix operator `++ : text -> text -> text` can be used to concatenate two text values. For example, - | + ``` "Number of widgets: " ++ format numWidgets + ``` - | `chars : text -> int` computes the number of characters in a `text` value. @@ -446,7 +458,10 @@ - A wild lambda. They are somewhat rare, but regrow when picked. Lambdas are delicious when cooked into curry. - Lambdas can also be used to create functions. For example, - - ' def thrice : cmd unit -> cmd unit = \c. c;c;c end' + - | + ``` + def thrice : cmd unit -> cmd unit = \c. c;c;c end + ``` - defines the function `thrice` which repeats a command three times. properties: [portable, growable] growth: [100, 200] @@ -751,13 +766,19 @@ description: - Equipping treads on a robot allows it to move and turn. - The `move` command moves the robot forward one unit. - - 'Example:' - - ' move; move; // move two units' + - | + Example:' + ``` + move; move; // move two units + ``` - The `turn` command takes a direction as an argument, which can be either absolute (north, west, east, south) or relative (left, right, forward, back, down). - - 'Example:' - - ' move; turn left; move; turn right' + - | + Example: + ``` + move; turn left; move; turn right + ``` capabilities: [move, turn] properties: [portable] @@ -771,6 +792,15 @@ capabilities: [move, turn, moveheavy] properties: [portable] +- name: tape drive + display: + attr: device + char: '%' + description: + - A "tape drive" allows you to `backup`; that is, to `drive` in reverse. + capabilities: [backup] + properties: [portable] + - name: dozer blade display: attr: silver @@ -809,13 +839,13 @@ char: '≪' description: - A fast grabber is an improved version of the basic grabber - not only - can it 'grab', 'place', and 'give', it can also 'swap'. - - The 'swap' command allows the robot to execute grab and place at the + can it `grab`, `place`, and `give`, it can also `swap`. + - The `swap` command allows the robot to execute grab and place at the same time so that the location where the robot is standing does not become empty. - You can use this to prevent failures where multiple robots are trying to grab, place or scan a given location. - - In addition you retain the capability to use the 'atomic' command, + - In addition you retain the capability to use the `atomic` command, with which you can implement other commands that are safe when run in parallel. capabilities: [grab, swap, give, place, atomic] @@ -893,7 +923,9 @@ attr: device char: '!' description: - - A drill allows robots to drill through rocks and mountains, and extract resources from mines. + - A drill allows robots to `drill` through rocks and mountains (with + e.g. `drill forward`), and extract resources from mines (with + `drill down`). capabilities: [drill] properties: [portable] @@ -926,12 +958,18 @@ - A 3D printer gives you the capability of printing more robots! You can access the 3D printer via the `build` command. - 'Example:' - - ' build {move; grab; turn back; move; give base "tree"}' + - | + ``` + build {move; grab; turn back; move; give base "tree"} + ``` - | builds a robot to get the tree on the cell to the north (if there is one) and bring it back to the base. The `build` command always returns a reference to the newly constructed robot. For example, - - ' r <- build {move}; view r' + - | + ``` + r <- build {move}; view r + ``` - | builds a robot and then views it. @@ -946,13 +984,19 @@ description: - | A dictionary allows a robot to remember definitions and reuse them - later. You can access this ability with either a `def` command, + later. You can access this ability with either a `def`{=snippet} command, which creates a name for an expression or command that is - available from then on, or with a `let` expression, which names an + available from then on, or with a `let`{=snippet} expression, which names an expression or command locally within another expression. - - ' def m2 : cmd unit = move; move end' - - ' let x : int = 3 in x^2 + 2*x + 1' - - The type annotations in `def` and `let` are optional. + - | + ``` + def m2 : cmd unit = move; move end + ``` + - | + ``` + let x : int = 3 in x^2 + 2*x + 1 + ``` + - The type annotations in `def`{=snippet} are optional. properties: [portable] capabilities: [env] @@ -968,7 +1012,10 @@ is `if` followed by three arguments: a boolean test and then two delayed expressions (i.e. expressions in curly braces) of the same type. - 'Example:' - - 'if (x > 3) {move} {turn right; move}' + - | + ``` + if (x > 3) {move} {turn right; move}' + ``` properties: [portable] capabilities: [cond] @@ -1080,7 +1127,10 @@ - "That way you can view any heard message later either in the logger or the message window." - "To wait for a message and get the string value, use:" - - "`l <- listen; log $ \"I have waited for someone to say \" ++ l`" + - | + ``` + l <- listen; log $ \"I have waited for someone to say \" ++ l + ``` properties: [portable] capabilities: [listen] @@ -1158,7 +1208,9 @@ robot's current heading. For example, the following code moves east and then restores the same heading as before: - | + ``` d <- heading; turn east; move; turn d + ``` properties: [portable] capabilities: [orient] @@ -1191,7 +1243,7 @@ char: R attr: silver description: - - Enables robots to use the 'watch' command. + - Enables robots to use the `watch` command. - | `watch : dir -> cmd unit` will mark an adjacent (in the specified direction) location of interest to monitor for placement or removal of items. A subsequent call to `wait` will be interrupted upon a change to the location. @@ -1226,8 +1278,9 @@ commands in between them. It can be used via the `atomic` command. For example, suppose robot A executes the following code:" - | + ``` b <- ishere "rock"; if b {grab} {} - + ``` - "This seems like a safe way to execute `grab` only when there is a rock to grab. However, it is actually possible for the `grab` to fail, if some other robot B snatches the rock right after robot A sensed @@ -1235,7 +1288,9 @@ - "This will make robot A very sad and it will crash." - "To prevent this situation, robot A can wrap the commands in `atomic`, like so:" - | + ``` atomic (b <- ishere "rock"; if b {grab} {}) + ``` properties: [portable] capabilities: [atomic] @@ -1246,9 +1301,11 @@ char: '#' description: - A net is a device woven out of many strings. With a net - equipped, you can use the `try` command to catch errors. For example, + equipped, you can use the `try` command to catch errors. For example - | - `try {move} {turn left}` + ``` + try {move} {turn left} + ``` - will attempt to move, but if that fails, turn left instead. properties: [portable] capabilities: [try] @@ -1354,7 +1411,7 @@ installs a custom handler function that can be activated to respond to keyboard inputs typed at the REPL. - | - `key : text -> key` constructs values of type `key`, for + `key : text -> key` constructs values of type `key`{=type}, for example `key "Down"` or `key "C-S-x"`. properties: [portable] capabilities: [handleinput] diff --git a/data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml b/data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml index 1318537d9..2f262b625 100644 --- a/data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml +++ b/data/scenarios/Challenges/Mazes/easy_spiral_maze.yaml @@ -7,7 +7,7 @@ objectives: - You find yourself in the middle of a large maze. - It's straightforward to get out, but the path is long and dull. - You need to send a robot to the goal square, labelled with an exclamation mark; - you win by `grab`bing the `goal`. + you win by `grab`bing the `goal`{=entity}. - Beware! The winding corridors are wider then they look! condition: | j <- robotNamed "judge"; diff --git a/data/scenarios/Challenges/Mazes/invisible_maze.yaml b/data/scenarios/Challenges/Mazes/invisible_maze.yaml index 621633dff..7e607460e 100644 --- a/data/scenarios/Challenges/Mazes/invisible_maze.yaml +++ b/data/scenarios/Challenges/Mazes/invisible_maze.yaml @@ -8,7 +8,7 @@ objectives: - There is a maze, but it can't be seen, only sensed... can you program a robot to navigate it successfully? You need to get a robot to the goal square, labelled with an exclamation mark; you win by `grab`bing - the `goal`. + the `goal`{=entity}. - In this challenge, it is guaranteed that the maze is a tree, that is, there are no loops within the maze. condition: | diff --git a/data/scenarios/Challenges/Mazes/loopy_maze.yaml b/data/scenarios/Challenges/Mazes/loopy_maze.yaml index 38e888dd2..4705d478f 100644 --- a/data/scenarios/Challenges/Mazes/loopy_maze.yaml +++ b/data/scenarios/Challenges/Mazes/loopy_maze.yaml @@ -8,7 +8,7 @@ objectives: - There is a maze, but it can't be seen, only sensed... can you program a robot to navigate it successfully? You need to get a robot to the goal square, labelled with an exclamation mark; you win by `grab`bing - the `goal`. + the `goal`{=entity}. - In this challenge, you are NOT guaranteed that the maze is a tree, that is, the maze may contain loops. condition: | diff --git a/data/scenarios/Challenges/arbitrage.yaml b/data/scenarios/Challenges/arbitrage.yaml index 3d6dab157..65ffc9124 100644 --- a/data/scenarios/Challenges/arbitrage.yaml +++ b/data/scenarios/Challenges/arbitrage.yaml @@ -10,7 +10,7 @@ objectives: - | Each shop offers different wares for trade. Recipes dictate which items may be exchanged - at any given shop. Use the `drill` on a shop to perform + at any given shop. Use the `drill`{=entity} on a shop to perform an exchange. - | As an itinerant merchant, you may exploit market asymmetry diff --git a/data/scenarios/Testing/00-ORDER.txt b/data/scenarios/Testing/00-ORDER.txt index 6910e4895..fc90b1a53 100644 --- a/data/scenarios/Testing/00-ORDER.txt +++ b/data/scenarios/Testing/00-ORDER.txt @@ -40,3 +40,4 @@ 1356-portals 144-subworlds 1379-single-world-portal-reorientation.yaml +1399-backup-command.yaml \ No newline at end of file diff --git a/data/scenarios/Testing/1399-backup-command.yaml b/data/scenarios/Testing/1399-backup-command.yaml new file mode 100644 index 000000000..bf9673287 --- /dev/null +++ b/data/scenarios/Testing/1399-backup-command.yaml @@ -0,0 +1,30 @@ +version: 1 +name: Demo backup command +description: | + Locomote backwards without access to the turn command +objectives: + - goal: + - | + `grab` the "flower". + condition: | + as base {has "flower"} +solution: | + backup; backup; grab; +robots: + - name: base + dir: [0, 1] + devices: + - tape drive + - grabber +known: [flower] +world: + default: [blank] + palette: + '.': [grass] + 'f': [grass, flower] + 'B': [grass, null, base] + upperleft: [-1, 1] + map: | + .B. + ... + .f. diff --git a/data/scenarios/Tutorials/backstory.yaml b/data/scenarios/Tutorials/backstory.yaml index 56af214b5..8ec284750 100644 --- a/data/scenarios/Tutorials/backstory.yaml +++ b/data/scenarios/Tutorials/backstory.yaml @@ -29,9 +29,11 @@ objectives: up again where you left off. - | When you're ready for your first challenge, we will try the `say` command. - Close this dialog with Esc or Ctrl-G, and type at the prompt: + Close this dialog with **Esc** or **Ctrl+G**, and type at the prompt: - | + ``` say "Ready!" + ``` condition: | try { l <- robotNamed "listener"; @@ -48,9 +50,11 @@ entities: - | When you're ready for your first tutorial challenge, type at the prompt: - | + ``` say "Ready!" + ``` - | - To open the full goal text again, you can hit Ctrl-G. + To open the full goal text again, you can hit **Ctrl+G**. properties: [known, portable] robots: - name: base diff --git a/data/scenarios/Tutorials/bind2.yaml b/data/scenarios/Tutorials/bind2.yaml index b09bb329f..c763cb435 100644 --- a/data/scenarios/Tutorials/bind2.yaml +++ b/data/scenarios/Tutorials/bind2.yaml @@ -14,7 +14,7 @@ objectives: Build a robot to retrieve and restore the mystery artifact to its proper place! - | Note: If you find yourself stuck, you can select "Start over" from - the "Quit" (CTRL+q) dialog. + the "Quit" (**Ctrl+Q**) dialog. condition: | try { p <- robotnamed "floorspot"; @@ -29,23 +29,25 @@ objectives: - | Every command returns a value. However, some simple commands, like `move`, do not have any meaningful - value to return. Swarm has a special type, `unit`, with only one value, + value to return. Swarm has a special type, `unit`{=type}, with only one value, called `()`. Since there is only one possible value of type - `unit`, returning it does not convey any information. - Thus, the type of `move` is `cmd unit`. + `unit`{=type}, returning it does not convey any information. + Thus, the type of `move` is `cmd unit`{=type}. - | Other commands do return a nontrivial value after executing. - For example, `grab` has type `cmd text`, and returns the name of the + For example, `grab` has type `cmd text`{=type}, and returns the name of the grabbed entity as a text value. - | To use the result of a command later, you need "bind notation", which consists of a variable name and a leftwards-pointing arrow before the command. For example: - | + ``` move; t <- grab; place t + ``` - | In the above example, the result returned by `grab` is assigned - to the variable name `t`, which can then be used later. + to the variable name `t`{=snippet}, which can then be used later. This is useful, for example, if you do not care what you grabbed and just want to move it to another cell, or if you are not sure of the name of the thing being grabbed. diff --git a/data/scenarios/Tutorials/build.yaml b/data/scenarios/Tutorials/build.yaml index e0a671273..ca19efc46 100644 --- a/data/scenarios/Tutorials/build.yaml +++ b/data/scenarios/Tutorials/build.yaml @@ -11,11 +11,11 @@ objectives: build robots to do the work for you. You will start in a base ('Ω') that does not move (at least not yet). - Let's start by building a gardener robot to perform a simple task. - - You can `build` a robot with `build {COMMANDS}`, - where in place of `COMMANDS` you write the sequence + - You can `build` a robot with `build {COMMANDS}`{=snippet}, + where in place of `COMMANDS`{=snippet} you write the sequence of commands for the robot to execute (separated by semicolons). - | - Build a robot to harvest the "flower" and place it next + Build a robot to harvest the `"flower"` and place it next to the water. - | TIP: Newly built robots start out facing the same diff --git a/data/scenarios/Tutorials/conditionals.yaml b/data/scenarios/Tutorials/conditionals.yaml index f3a1d7aa7..30b769743 100644 --- a/data/scenarios/Tutorials/conditionals.yaml +++ b/data/scenarios/Tutorials/conditionals.yaml @@ -5,17 +5,17 @@ description: | objectives: - goal: - | - The 4x4 gray square contains 4 `very small rock`s --- so + The 4x4 gray square contains 4 `very small rock`{=entity}s --- so small they cannot be seen! Your goal is to collect all of them and bring them back to your base; you win when you have all 4. There is one rock in each row and column, but otherwise you can't be sure where they are. Your best bet is - to sweep over the entire 4x4 square and pick up a `very small - rock` any time you detect one. + to sweep over the entire 4x4 square and pick up a `very small rock`{=entity} + any time you detect one. - | - The `ishere` command, with type `text -> cmd bool`, can be used - for detecting the presence of a specific item such as a `very small rock`. - What we need is a way to take the `bool` output from `ishere` + The `ishere` command, with type `text -> cmd bool`{=type}, can be used + for detecting the presence of a specific item such as a `very small rock`{=entity}. + What we need is a way to take the `bool`{=type} output from `ishere` and use it to decide whether to `grab` a rock or not. (Trying to execute `grab` in a cell without anything to grab will throw an exception, causing the robot to crash.) @@ -24,29 +24,33 @@ objectives: conditional expressions. However, `if` is not special syntax; it is simply a built-in function of type - | - if : bool -> {a} -> {a} -> a. + ``` + if : bool -> {a} -> {a} -> a + ``` - | It takes a boolean expression and then returns either the first or second subsequent argument, depending on whether the boolean expression is true or false, respectively. - | - The type variable `a` can stand for any type; `{a}` - indicates a *delayed* expression of type `a`. Normally, + The type variable `a`{=type} can stand for any type; `{a}`{=type} + indicates a *delayed* expression of type `a`{=type}. Normally, function arguments are evaluated strictly before the function is called. Delayed expressions, on the other hand, are not evaluated until needed. In this case, we want to make sure that only the correct branch is evaluated. To write a value - of type, say, `{int}`, we just surround a value of type `int` + of type, say, `{int}`{=type}, we just surround a value of type `int`{=type} in curly braces, like `{3}`. This is why arguments to `build` - must also be in curly braces: the type of `build` is `{cmd a} - -> cmd robot`. + must also be in curly braces: the type of `build` + is `{cmd a} -> cmd robot`{=type}. - | - TIP: Note that `if` requires a `bool`, not a `cmd bool`! So you cannot directly say - `if (ishere "very small rock") {...} {...}`. Instead you can write `b <- ishere "very small rock"; if b {...} {...}`. You might enjoy writing your own function of - type `cmd bool -> {cmd a} -> {cmd a} -> cmd a` to encapsulate this pattern. + TIP: Note that `if` requires a `bool`{=type}, not a `cmd bool`{=type}! So you cannot directly say + `if (ishere "very small rock") {...} {...}`{=snippet}. + Instead you can write `b <- ishere "very small rock"; if b {...} {...}`{=snippet}. + You might enjoy writing your own function of + type `cmd bool -> {cmd a} -> {cmd a} -> cmd a`{=type} to encapsulate this pattern. - | TIP: the two branches of an `if` must have the same type. In particular, - `if ... {grab} {}` is not - allowed, because `{grab}` has type `{cmd text}` whereas `{}` has type `{cmd unit}`. + `if ... {grab} {}`{=snippet} is not + allowed, because `{grab}` has type `{cmd text}`{=type} whereas `{}` has type `{cmd unit}`{=type}. In this case `{grab; return ()}` has the right type. condition: | try { diff --git a/data/scenarios/Tutorials/craft.yaml b/data/scenarios/Tutorials/craft.yaml index d9cb42a2b..3189e5e69 100644 --- a/data/scenarios/Tutorials/craft.yaml +++ b/data/scenarios/Tutorials/craft.yaml @@ -9,12 +9,12 @@ description: | objectives: - goal: - Robots can use the `make` command to make things, as long as - they have a `workbench` and the proper ingredients. For + they have a `workbench`{=entity} and the proper ingredients. For example, `make "circuit"` will make a circuit. - Your base has a few trees in its inventory. Select them to see the available recipes. - - Your goal is to make a "branch predictor", so you will have to make - some "branch"es first. + - Your goal is to make a `branch predictor`{=entity}, so you will have to make + some `"branch"`es first. - | Note: when used after opening quotes in the REPL, the Tab key can cycle through possible completions of a name. E.g., type: diff --git a/data/scenarios/Tutorials/crash.yaml b/data/scenarios/Tutorials/crash.yaml index 1d4b80783..b4a808ea9 100644 --- a/data/scenarios/Tutorials/crash.yaml +++ b/data/scenarios/Tutorials/crash.yaml @@ -9,16 +9,18 @@ objectives: - | In this challenge, you should start by sending a robot to walk four steps straight east into the mountain, - crashing deliberately. However, you must make sure it has a `logger`, + crashing deliberately. However, you must make sure it has a `logger`{=entity}, so we can see what command failed. The simplest way to ensure that is to have it execute the `log` command; `build` will ensure it has the devices it needs to execute its commands. For example: - | + ``` build {log "Hi!"; turn east; move; move; move; log "3"; move; log "OK"} + ``` - | - `wait` for the robot to crash, then execute `view it0` (or whichever - `itN` variable corresponds to the result of the `build` + `wait` for the robot to crash, then execute `view it0`{=snippet} (or whichever + `itN`{=snippet} variable corresponds to the result of the `build` command) to see how far it got. Further instructions should appear in the crashed robot's log and `give` you an opportunity to `salvage` the situation... diff --git a/data/scenarios/Tutorials/def.yaml b/data/scenarios/Tutorials/def.yaml index 6caa34d3e..75cef114d 100644 --- a/data/scenarios/Tutorials/def.yaml +++ b/data/scenarios/Tutorials/def.yaml @@ -9,11 +9,13 @@ objectives: has a flower in its inventory. - | However, it would be extremely tedious to simply type out all the individual - `move` and `turn` commands required. Your base has a `dictionary` device + `move` and `turn` commands required. Your base has a `dictionary`{=entity} device that can be used to define new commands. For example: - | + ``` def m4 : cmd unit = move; move; move; move end - - defines a new command `m4`, with type `cmd unit`, as four consecutive `move` commands. + ``` + - defines a new command `m4`{=snippet}, with type `cmd unit`{=type}, as four consecutive `move` commands. With judicious use of new definitions, it should be possible to complete this challenge in just a few lines of code. @@ -24,10 +26,10 @@ objectives: - | TIP: the type annotation in a definition is optional. You could also write `def m4 = move; move; move; move end`, and Swarm would infer - the type of `m4`. + the type of `m4`{=snippet}. - | TIP: writing function definitions at the prompt is annoying. - You can also put definitions in a `.sw` file and load it + You can also put definitions in a `.sw`{=path} file and load it with the `run` command. Check out https://github.com/swarm-game/swarm/tree/main/editors for help setting up an external editor with things like diff --git a/data/scenarios/Tutorials/equip.yaml b/data/scenarios/Tutorials/equip.yaml index c90bb2a28..a25996d17 100644 --- a/data/scenarios/Tutorials/equip.yaml +++ b/data/scenarios/Tutorials/equip.yaml @@ -11,14 +11,16 @@ objectives: - Before you start building new robots in the later tutorials, you need to gain the `build` capability. Try typing `build {}` - you should get an error telling you that you - need to equip a "3D printer". + need to equip a `3D printer`{=entity}. - | Fortunately, there is a 3D printer lying nearby. Go `grab` it, then `equip` it with `equip "3D printer"`. - | You win by building your first robot: - | + ``` build {} + ``` condition: | try { _ <- robotNumbered 1; diff --git a/data/scenarios/Tutorials/farming.yaml b/data/scenarios/Tutorials/farming.yaml index c756aed7a..a05987d8c 100644 --- a/data/scenarios/Tutorials/farming.yaml +++ b/data/scenarios/Tutorials/farming.yaml @@ -11,13 +11,15 @@ objectives: them in order to create a reliable supply. - | In this scenario, you are a bit farther along: in particular, - you now have a few `harvester`s, a few `lambda`s, a few `logger`s, - some `branch predictor`s which + you now have a few `harvester`{=entity}s, a few `lambda`{=entity}s, a few `logger`{=entity}s, + some `branch predictor`{=entity}s which allow robots to evaluate conditional expressions, and some - `strange loops` which enable recursive functions. For example, + `strange loop`{=entity}s which enable recursive functions. For example, one simple, useful recursive function is - | + ``` def forever = \c. c ; forever c end + ``` - Your goal is to acquire 256 lambdas. Of course, in order to accomplish this in a reasonable amount of time, it makes sense to plant a field of lambdas and then program one or more robots to @@ -39,7 +41,7 @@ objectives: language features to unlock. - | To finally complete this tutorial, there is only one thing left for you to do: - use one of your lambdas to make some delicious `curry`. + use one of your lambdas to make some delicious `curry`{=entity}. - Afterwards, you will return to the menu where you can select "Classic game" for the complete game experience. Or, play a "Creative game" if you just want to play around with diff --git a/data/scenarios/Tutorials/grab.yaml b/data/scenarios/Tutorials/grab.yaml index 61aef520a..85d13edb0 100644 --- a/data/scenarios/Tutorials/grab.yaml +++ b/data/scenarios/Tutorials/grab.yaml @@ -9,7 +9,7 @@ objectives: - There are some trees ahead of your robot; `move` to each one and `grab` it. - You can learn more by reading about the grabber device in your inventory. Remember, if the description does not fit in the - lower left info box, you can either hit `Enter` to pop out the + lower left info box, you can either hit **Enter** to pop out the description, or focus the info box in order to scroll. condition: | try { diff --git a/data/scenarios/Tutorials/lambda.yaml b/data/scenarios/Tutorials/lambda.yaml index 592f7e67a..a61501f15 100644 --- a/data/scenarios/Tutorials/lambda.yaml +++ b/data/scenarios/Tutorials/lambda.yaml @@ -12,14 +12,16 @@ objectives: the path that repeat four times; it seems like it could be really useful to have a function to repeat a command four times. - | - To write a function, you use lambda syntax: in general, `\x. blah` is the - function which takes an input (locally called `x`) and returns - `blah` as its output (`blah` can of course refer to `x`). For example: + To write a function, you use lambda syntax. As a simple example, `\x. x + 1` is the + function which takes an input (locally called `x`{=snippet}) and returns + one more than `x`{=snippet}. As another example: - | + ``` def x4 : cmd unit -> cmd unit = \c. c; c; c; c end - - That is, `x4` is defined as the function which takes a command, called `c`, - as input, and returns the command - `c; c; c; c` which consists of executing `c` four times. + ``` + - That is, `x4`{=snippet} is defined as the function which takes a command, + called `c`{=snippet}, as input, and returns the command + `c; c; c; c`{=snippet} which consists of executing `c`{=snippet} four times. condition: | try { teleport self (32,-16); diff --git a/data/scenarios/Tutorials/move.yaml b/data/scenarios/Tutorials/move.yaml index 3257cfbc0..41cc161c4 100644 --- a/data/scenarios/Tutorials/move.yaml +++ b/data/scenarios/Tutorials/move.yaml @@ -9,10 +9,10 @@ objectives: - Robots can use the `move` command to move forward one unit in the direction they are currently facing. - To complete this challenge, move your robot two spaces to the right, - to the coordinates (2,0) marked with the purple flower. - - Note that you can chain commands with semicolon, `;`. + to the coordinates `(2,0)` marked with the purple flower. + - Note that you can chain commands with semicolon, `;`{=snippet}. - You can open this popup window at any time to remind yourself of the goal - using Ctrl-G. + using **Ctrl+G**. condition: | r <- robotNamed "check1"; loc <- as r {has "Win"}; @@ -27,9 +27,9 @@ objectives: - To reuse that command without having to retype it press the upward arrow on your keyboard. This will allow you to select previous commands. - Ahead of you is a six steps long corridor. Move to its end, i.e. the - coordinates (8,0) marked with the second purple flower. + coordinates `(8,0)` marked with the second purple flower. - You can open this popup window at any time to remind yourself of the goal - using Ctrl-G. + using **Ctrl+G**. condition: | r <- robotNamed "check2"; loc <- as r {has "Win"}; @@ -39,22 +39,24 @@ objectives: goal: - Well done! In addition to `move`, you can use the `turn` command to turn your robot, for example, `turn right` or `turn east`. - - Switch to the inventory view in the upper left (by clicking on it or typing `Alt+E`) - and select the `treads` device to read about the details. + - Switch to the inventory view in the upper left (by clicking on it or typing **Alt+E**) + and select the `treads`{=entity} device to read about the details. If the bottom-left info panel is not big enough to read the - whole thing, you can hit `Enter` on the `treads` device to pop - out the description, or you can focus the info panel (with - `Alt+T` or by clicking) and scroll it with arrow keys or PgUp/PgDown. + whole thing, you can hit **Enter** on the `treads`{=entity} device to pop + out the description, or you can focus the info panel (with **Alt+T** or + by clicking) and scroll it with arrow keys or **PgUp**/**PgDown**. When you're done reading, you can come back to the REPL prompt - by clicking on it or typing `Alt+R`. - - Afterwards, move your robot to the coordinates (8,4) in the northeast corner + by clicking on it or typing **Alt+R**. + - Afterwards, move your robot to the coordinates `(8,4)` in the northeast corner marked with two flowers. - | - Remember, you can chain commands with `;`, for example: + Remember, you can chain commands with `;`{=snippet}, for example: - | - `move;move;move;move` + ``` + move;move;move;move + ``` - You can open this popup window at any time to remind yourself of the goal - using Ctrl-G. + using **Ctrl+G**. condition: | r <- robotNamed "check3"; loc <- as r {has "Win"}; @@ -62,10 +64,10 @@ objectives: - goal: - Good job! You are now ready to move and turn on your own. - To complete this challenge, move your robot to the northeast corner, - to the coordinates (8,8) marked with one flower. + to the coordinates `(8,8)` marked with one flower. - Remember you can press the upward arrow on your keyboard to select previous commands. - You can open this popup window at any time to remind yourself of the goal - using Ctrl-G. + using **Ctrl+G**. condition: | r <- robotNamed "check4"; loc <- as r {has "Win"}; diff --git a/data/scenarios/Tutorials/place.yaml b/data/scenarios/Tutorials/place.yaml index 833050345..ee05cd967 100644 --- a/data/scenarios/Tutorials/place.yaml +++ b/data/scenarios/Tutorials/place.yaml @@ -11,21 +11,28 @@ objectives: - goal: - Previously you learned how to plunder a plentiful forest for wood. Now you will learn how to plant trees to obtain as much wood as you need. - - There is a fast-growing tree (called "spruce") ahead of you. You could `grab` - it as before, but you now have a new device called a `harvester`. + - There is a fast-growing tree (called `"spruce"`) ahead of you. You could `grab` + it as before, but you now have a new device called a `harvester`{=entity}. If you `harvest` a tree rather than `grab` it, a new tree will grow in its place after some time. - You can also place items from your inventory on the ground below you using the `place` command. - - Using these commands in conjunction, you can plant new growable entities by - placing and then harvesting them. For example, `place "spruce"; harvest` will - plant a new spruce seed. + - | + Using these commands in conjunction, you can plant new growable entities by + placing and then harvesting them. For example, to plant a new spruce seed + you can write: + ``` + place "spruce"; harvest + ``` - Your goal is to collect 6 spruce trees. You can speed this up by planting more trees. - | - TIP: You can get a sneak peak at a feature we will explain later and type - `def t = move; place "spruce"; harvest; end` after which you only need to type `t` - instead of retyping the whole command or searching in your command history. + TIP: You can get a sneak peak at a feature we will explain later and type: + ``` + def t = move; place "spruce"; harvest; end + ``` + after which you only need to type `t`{=snippet} instead of retyping the whole + command or searching in your command history. condition: | try { t <- as base {count "spruce"}; diff --git a/data/scenarios/Tutorials/require.yaml b/data/scenarios/Tutorials/require.yaml index 16714fb98..9170ec40c 100644 --- a/data/scenarios/Tutorials/require.yaml +++ b/data/scenarios/Tutorials/require.yaml @@ -6,20 +6,20 @@ objectives: - goal: - The `build` command automatically equips devices on the newly built robot that it knows - will be required. For example, if you `build {move}`, some `treads` + will be required. For example, if you `build {move}`, some `treads`{=entity} will automatically be equipped on the new robot since it needs them to `move`. (To see what the `build` command will equip, - you can type `requirements ` where `` is any expression.) + you can type `requirements `{=snippet} where ``{=snippet} is any expression.) - However, sometimes you need a device but `build` can't tell that - you need it. In this case, you can use the special `require` + you need it. In this case, you can use the special `require`{=snippet} command to require a particular device. For example, if you - `build {require "3D printer"; move}`, a 3D printer will be - equipped on the new robot even though it does not execute any - commands that use a 3D printer. + `build {require "3D printer"; move}`, a `3D printer`{=entity} will be + equipped on the new robot (in addition to `treads`{=entity}) even though it does not execute any + commands that use one. - Your goal is to pick a flower on the other side of the river and bring it back to your base. You win when the base has a - `periwinkle` flower in its inventory. - - "Hint: robots will drown in the water unless they have a `boat` device + `periwinkle`{=entity} flower in its inventory. + - "Hint: robots will drown in the water unless they have a `boat`{=entity} device equipped!" condition: | try { diff --git a/data/scenarios/Tutorials/requireinv.yaml b/data/scenarios/Tutorials/requireinv.yaml index 0db079741..b27ac6ccb 100644 --- a/data/scenarios/Tutorials/requireinv.yaml +++ b/data/scenarios/Tutorials/requireinv.yaml @@ -5,19 +5,20 @@ description: | objectives: - goal: - In the previous tutorial challenge, you learned how to use - `require` to require specific devices to be equipped. + `require`{=snippet} to require specific devices to be equipped. Sometimes, instead of requiring equipped devices, you require supplies in your inventory. In this case, you can write - `require ` to require a certain number of copies of + `require `{=snippet} to require a certain number of copies of a certain entity to be placed in your inventory. - For example, `build {require 10 "flower"; move; move}` would - build a robot with 10 flowers in its inventory. + build a robot with 10 `flower`{=entity}s in its inventory. - Your goal in this challenge is to cover the entire 4x4 gray area with rocks! - | Remember that you can define commands to simplify your task, for example: - - | - `def PR = move; place "rock" end` + ``` + def PR = move; place "rock" end + ``` condition: | def repeat = \n. \c. if (n == 0) {} {c ; repeat (n-1) c} end; def ifC = \test. \then. \else. b <- test; if b then else end; diff --git a/data/scenarios/Tutorials/scan.yaml b/data/scenarios/Tutorials/scan.yaml index c2e25716c..d49650b3f 100644 --- a/data/scenarios/Tutorials/scan.yaml +++ b/data/scenarios/Tutorials/scan.yaml @@ -6,12 +6,12 @@ objectives: - goal: - When you land on an alien planet, all the entities in the world will be unfamiliar to you, but you can learn what they are using - the `scan` command, enabled by a `scanner` device. + the `scan` command, enabled by a `scanner`{=entity} device. - Send one or more robots to move next to some of the unknown entities (marked as ?), scan them (with something like `scan forward` or `scan north`), and then return to the base and execute `upload base`. - For more information about the `scan` and `upload` commands, read - the description of the `scanner` in your inventory. + the description of the `scanner`{=entity} in your inventory. condition: | try { bm <- as base {knows "mountain"}; diff --git a/data/scenarios/Tutorials/type-errors.yaml b/data/scenarios/Tutorials/type-errors.yaml index 5ae863f10..2efd18276 100644 --- a/data/scenarios/Tutorials/type-errors.yaml +++ b/data/scenarios/Tutorials/type-errors.yaml @@ -6,18 +6,18 @@ objectives: - goal: - | Let's see what happens when you enter something that does not type check. - Try typing `turn 1` at the REPL prompt. Clearly this is nonsense, and - the expression will be highlighted in red. To see what the error is, hit Enter. + Try typing `turn 1`{=snippet} at the REPL prompt. Clearly this is nonsense, and + the expression will be highlighted in red. To see what the error is, hit **Enter**. A box will pop up with a type (or parser) error. - "Some other type errors for you to try:" - | - `turn move` + `turn move`{=snippet} - | - `place tree` (without double quotes around "tree") + `place tree`{=snippet} (without double quotes around "tree") - | - `move move` + `move move`{=snippet} - The last expression might give the most confusing error. - Obviously we are just missing a `;` separating the two `move` + Obviously we are just missing a `;`{=snippet} separating the two `move` commands. However, without the semicolon, it looks like `move` is a function being applied to an argument, but of course `move` is not a function. diff --git a/data/scenarios/Tutorials/types.yaml b/data/scenarios/Tutorials/types.yaml index 9dd1baec4..d479609d1 100644 --- a/data/scenarios/Tutorials/types.yaml +++ b/data/scenarios/Tutorials/types.yaml @@ -11,24 +11,24 @@ objectives: REPL prompt (you do not need to execute it). If the expression type checks, its type will be displayed in gray text at the top right of the window. - For example, if you try typing `move`, you can see that it has - type `cmd unit`, which means that `move` is a command which + type `cmd unit`{=type}, which means that `move` is a command which returns a value of the unit type (also written `()`). - - As another example, you can see that `turn` has type `dir -> cmd unit`, + - As another example, you can see that `turn` has type `dir -> cmd unit`{=type}, meaning that `turn` is a function which takes a direction as input and results in a command. - "Here are a few more expressions for you to try (feel free to try others as well):" - | - north + `north` - | - move; move + `move; move` - | - grab + `grab` - | - make + `make` - | - 3 + `3` - | - "tree" + `"tree"` - Once you are done experimenting, execute `place "Win"` to finish this challenge and move on to the next. condition: | diff --git a/data/scenarios/Tutorials/world101.yaml b/data/scenarios/Tutorials/world101.yaml index 7301d5497..5c7cd402d 100644 --- a/data/scenarios/Tutorials/world101.yaml +++ b/data/scenarios/Tutorials/world101.yaml @@ -16,7 +16,7 @@ objectives: build more advanced devices and produce more robots, you'll need to explore, gather resources, and set up some automated production pipelines. - - At this point you may want to create an external `.sw` file + - At this point you may want to create an external `.sw`{=path} file with useful definitions you create. You can then load it via the `run` command. See https://github.com/swarm-game/swarm/tree/main/editors @@ -47,7 +47,7 @@ objectives: send out a robot to harvest something, try programming it to come back to the base when it is done. Then, execute `salvage` to get the harvester back, so you can reuse it in another robot later." - - One of the next things you will probably want is a `lambda`, so you can + - One of the next things you will probably want is a `lambda`{=entity}, so you can define and use parameterized commands. Scan some things and use the process of elimination to find one. Since lambdas regrow, once you find one, try getting it with `harvest`. diff --git a/editors/emacs/swarm-mode.el b/editors/emacs/swarm-mode.el index 619132e0c..3f9a15ae1 100644 --- a/editors/emacs/swarm-mode.el +++ b/editors/emacs/swarm-mode.el @@ -54,6 +54,7 @@ "wait" "selfdestruct" "move" + "backup" "push" "stride" "turn" diff --git a/editors/vscode/syntaxes/swarm.tmLanguage.json b/editors/vscode/syntaxes/swarm.tmLanguage.json index 21142141a..d39688224 100644 --- a/editors/vscode/syntaxes/swarm.tmLanguage.json +++ b/editors/vscode/syntaxes/swarm.tmLanguage.json @@ -58,7 +58,7 @@ }, { "name": "keyword.other", - "match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b" + "match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b" } ] }, diff --git a/src/Swarm/App.hs b/src/Swarm/App.hs index 5a56651f4..0cafec640 100644 --- a/src/Swarm/App.hs +++ b/src/Swarm/App.hs @@ -18,7 +18,7 @@ import Data.IORef (newIORef, writeIORef) import Data.Text qualified as T import Data.Text.IO qualified as T import Graphics.Vty qualified as V -import Swarm.Game.Failure.Render (prettyFailure) +import Swarm.Game.Failure (prettyFailure) import Swarm.Game.Robot (ErrorLevel (..), LogSource (ErrorTrace, Said)) import Swarm.ReadableIORef (mkReadonly) import Swarm.TUI.Controller diff --git a/src/Swarm/Doc/Pedagogy.hs b/src/Swarm/Doc/Pedagogy.hs index 3c2b50aa4..7f1808ae5 100644 --- a/src/Swarm/Doc/Pedagogy.hs +++ b/src/Swarm/Doc/Pedagogy.hs @@ -16,10 +16,9 @@ module Swarm.Doc.Pedagogy ( TutorialInfo (..), ) where -import Control.Arrow ((&&&)) import Control.Carrier.Accum.FixedStrict (evalAccum) -import Control.Lens (universe, view) -import Control.Monad (guard, (<=<)) +import Control.Lens (universe, view, (^.)) +import Control.Monad (guard) import Data.List (foldl', intercalate, sort, sortOn) import Data.List.Extra (zipFrom) import Data.Map (Map) @@ -39,6 +38,7 @@ import Swarm.Game.ScenarioInfo (ScenarioCollection, ScenarioInfoPair, flatten, l import Swarm.Language.Module (Module (..)) import Swarm.Language.Pipeline (ProcessedTerm (..)) import Swarm.Language.Syntax +import Swarm.Language.Text.Markdown (findCode) import Swarm.Language.Types (Polytype) import Swarm.TUI.Controller (getTutorials) import Swarm.Util.Effect (simpleErrorHandle) @@ -88,16 +88,16 @@ extractCommandUsages idx siPair@(s, _si) = -- | Obtain the set of all commands mentioned by -- name in the tutorial's goal descriptions. getDescCommands :: Scenario -> Set Const -getDescCommands s = - S.fromList $ mapMaybe (`M.lookup` txtLookups) backtickedWords +getDescCommands s = S.fromList $ concatMap filterConst allCode where goalTextParagraphs = concatMap (view objectiveGoal) $ view scenarioObjectives s - allWords = concatMap (T.words . T.toLower) goalTextParagraphs - getBackticked = T.stripPrefix "`" <=< T.stripSuffix "`" - backtickedWords = mapMaybe getBackticked allWords - - commandConsts = filter isConsidered allConst - txtLookups = M.fromList $ map (syntax . constInfo &&& id) commandConsts + allCode = concatMap findCode goalTextParagraphs + filterConst :: Syntax -> [Const] + filterConst sx = mapMaybe toConst $ universe (sx ^. sTerm) + toConst :: Term -> Maybe Const + toConst = \case + TConst c -> Just c + _ -> Nothing isConsidered :: Const -> Bool isConsidered c = isUserFunc c && c `S.notMember` ignoredCommands @@ -158,7 +158,11 @@ generateIntroductionsSequence = loadScenarioCollection :: IO ScenarioCollection loadScenarioCollection = simpleErrorHandle $ do entities <- loadEntities - evalAccum (mempty :: Seq SystemFailure) $ loadScenarios entities -- ignore warnings + evalAccum (mempty :: Seq SystemFailure) $ loadScenarios entities + -- Note we ignore any warnings generated by 'loadScenarios' above. + -- Any warnings will be caught when loading all the scenarios via + -- the usual code path; we do not need to do anything with them + -- here while simply rendering pedagogy info. renderUsagesMarkdown :: CoverageInfo -> Text renderUsagesMarkdown (CoverageInfo (TutorialInfo (s, si) idx _sCmds dCmds) novelCmds) = diff --git a/src/Swarm/Game/Failure.hs b/src/Swarm/Game/Failure.hs index 7d0d892a8..f2c4781a0 100644 --- a/src/Swarm/Game/Failure.hs +++ b/src/Swarm/Game/Failure.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE OverloadedStrings #-} + -- | -- SPDX-License-Identifier: BSD-3-Clause -- @@ -5,10 +7,25 @@ -- -- These failures are often not fatal and serve -- to create common infrastructure for logging. -module Swarm.Game.Failure where +module Swarm.Game.Failure ( + SystemFailure (..), + AssetData (..), + Asset (..), + Entry (..), + LoadingFailure (..), + OrderFileWarning(..), + prettyLoadingFailure, + prettyFailure, +) where import Data.Text (Text) -import Data.Yaml (ParseException) +import Data.Text qualified as T +import Data.Yaml (ParseException, prettyPrintParseException) +import Swarm.Util (quote, showLowT) +import Witch (into) + +------------------------------------------------------------ +-- Failure descriptions data SystemFailure = AssetNotLoaded Asset FilePath LoadingFailure @@ -39,3 +56,45 @@ data OrderFileWarning | MissingFiles [FilePath] | DanglingFiles [FilePath] deriving (Eq, Show) + +------------------------------------------------------------ +-- Pretty-printing + +prettyAssetData :: AssetData -> Text +prettyAssetData NameGeneration = "name generation data" +prettyAssetData AppAsset = "data assets" +prettyAssetData d = showLowT d + +prettyAsset :: Asset -> Text +prettyAsset (Data ad) = prettyAssetData ad +prettyAsset a = showLowT a + +prettyLoadingFailure :: LoadingFailure -> Text +prettyLoadingFailure = \case + DoesNotExist e -> "The " <> showLowT e <> " is missing!" + EntryNot e -> "The entry is not a " <> showLowT e <> "!" + CanNotParse p -> "Parse failure:\n" <> into @Text (indent 8 $ prettyPrintParseException p) + Duplicate thing duped -> "Duplicate " <> showLowT thing <> ": " <> quote duped + CustomMessage m -> m + where + indent n = unlines . map (replicate n ' ' ++) . lines + +prettyFailure :: SystemFailure -> Text +prettyFailure = \case + AssetNotLoaded a fp l -> + T.unwords ["Failed to acquire", prettyAsset a, "from path", quote $ into @Text fp] <> ": " <> prettyLoadingFailure l + ScenarioNotFound s -> + "Scenario not found: " <> into @Text s + OrderFileWarning orderFile w -> + "Warning: while processing " <> into @Text orderFile <> ":\n" <> T.unlines (prettyOrderFileWarning w) + CustomFailure m -> m + +prettyOrderFileWarning :: OrderFileWarning -> [Text] +prettyOrderFileWarning = \case + NoOrderFile -> [" File not found; using alphabetical order"] + MissingFiles missing -> + " Files not listed will be ignored:" + : map ((" - " <>) . into @Text) missing + DanglingFiles dangling -> + " Some listed files do not exist:" + : map ((" - " <>) . into @Text) dangling diff --git a/src/Swarm/Game/Failure/Render.hs b/src/Swarm/Game/Failure/Render.hs deleted file mode 100644 index 69c710981..000000000 --- a/src/Swarm/Game/Failure/Render.hs +++ /dev/null @@ -1,53 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- | --- SPDX-License-Identifier: BSD-3-Clause --- --- Pretty-printing failure messages -module Swarm.Game.Failure.Render where - -import Data.Text (Text) -import Data.Text qualified as T -import Data.Yaml (prettyPrintParseException) -import Swarm.Game.Failure -import Swarm.Util (quote, showLowT) -import Witch (into) - -prettyAssetData :: AssetData -> Text -prettyAssetData NameGeneration = "name generation data" -prettyAssetData AppAsset = "data assets" -prettyAssetData d = showLowT d - -prettyAsset :: Asset -> Text -prettyAsset (Data ad) = prettyAssetData ad -prettyAsset a = showLowT a - -prettyLoadingFailure :: LoadingFailure -> Text -prettyLoadingFailure = \case - DoesNotExist e -> "The " <> showLowT e <> " is missing!" - EntryNot e -> "The entry is not a " <> showLowT e <> "!" - CanNotParse p -> "Parse failure:\n" <> into @Text (indent 8 $ prettyPrintParseException p) - Duplicate thing duped -> "Duplicate " <> showLowT thing <> ": " <> quote duped - CustomMessage m -> m - where - indent n = unlines . map (replicate n ' ' ++) . lines - -prettyFailure :: SystemFailure -> Text -prettyFailure = \case - AssetNotLoaded a fp l -> - T.unwords ["Failed to acquire", prettyAsset a, "from path", quote $ into @Text fp] <> ": " <> prettyLoadingFailure l - ScenarioNotFound s -> - "Scenario not found: " <> into @Text s - OrderFileWarning orderFile w -> - "Warning: while processing " <> into @Text orderFile <> ":\n" <> T.unlines (prettyOrderFileWarning w) - CustomFailure m -> m - -prettyOrderFileWarning :: OrderFileWarning -> [Text] -prettyOrderFileWarning = \case - NoOrderFile -> [" File not found; using alphabetical order"] - MissingFiles missing -> - " Files not listed will be ignored:" - : map ((" - " <>) . into @Text) missing - DanglingFiles dangling -> - " Some listed files do not exist:" - : map ((" - " <>) . into @Text) dangling diff --git a/src/Swarm/Game/Scenario.hs b/src/Swarm/Game/Scenario.hs index 22e84d26a..dde8748c2 100644 --- a/src/Swarm/Game/Scenario.hs +++ b/src/Swarm/Game/Scenario.hs @@ -61,7 +61,6 @@ import Data.Text (Text) import Data.Text qualified as T import Swarm.Game.Entity import Swarm.Game.Failure -import Swarm.Game.Failure.Render import Swarm.Game.Location import Swarm.Game.Recipe import Swarm.Game.ResourceLoading (getDataFileNameSafe) diff --git a/src/Swarm/Game/Scenario/Objective.hs b/src/Swarm/Game/Scenario/Objective.hs index a65fac068..8c54917f4 100644 --- a/src/Swarm/Game/Scenario/Objective.hs +++ b/src/Swarm/Game/Scenario/Objective.hs @@ -17,7 +17,8 @@ import Servant.Docs qualified as SD import Swarm.Game.Achievement.Definitions import Swarm.Game.Scenario.Objective.Logic as L import Swarm.Language.Pipeline (ProcessedTerm) -import Swarm.Util (reflow) +import Swarm.Language.Syntax (Syntax) +import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.Util.Lens (makeLensesNoSigs) ------------------------------------------------------------ @@ -65,7 +66,7 @@ instance FromJSON PrerequisiteConfig where -- | An objective is a condition to be achieved by a player in a -- scenario. data Objective = Objective - { _objectiveGoal :: [Text] + { _objectiveGoal :: [Markdown.Document Syntax] , _objectiveTeaser :: Maybe Text , _objectiveCondition :: ProcessedTerm , _objectiveId :: Maybe ObjectiveLabel @@ -83,7 +84,7 @@ instance ToSample Objective where -- | An explanation of the goal of the objective, shown to the player -- during play. It is represented as a list of paragraphs. -objectiveGoal :: Lens' Objective [Text] +objectiveGoal :: Lens' Objective [Markdown.Document Syntax] -- | A very short (3-5 words) description of the goal for -- displaying on the left side of the Objectives modal. @@ -121,7 +122,7 @@ objectiveAchievement :: Lens' Objective (Maybe AchievementInfo) instance FromJSON Objective where parseJSON = withObject "objective" $ \v -> Objective - <$> (fmap . map) reflow (v .:? "goal" .!= []) + <$> (v .:? "goal" .!= []) <*> (v .:? "teaser") <*> (v .: "condition") <*> (v .:? "id") diff --git a/src/Swarm/Game/Step.hs b/src/Swarm/Game/Step.hs index f372f3921..c9d686093 100644 --- a/src/Swarm/Game/Step.hs +++ b/src/Swarm/Game/Step.hs @@ -544,6 +544,10 @@ traceLogShow = void . traceLog Logged . from . show constCapsFor :: Const -> Robot -> Maybe Capability constCapsFor Move r | r ^. robotHeavy = Just CMoveheavy +constCapsFor Backup r + | r ^. robotHeavy = Just CMoveheavy +constCapsFor Stride r + | r ^. robotHeavy = Just CMoveheavy constCapsFor c _ = constCaps c -- | Ensure that a robot is capable of executing a certain constant @@ -1127,17 +1131,11 @@ execConst c vs s k = do flagRedraw return $ Out VUnit s k Move -> do - -- Figure out where we're going - loc <- use robotLocation orient <- use robotOrientation - let nextLoc = loc `offsetBy` (orient ? zero) - checkMoveAhead nextLoc $ - MoveFailure - { failIfBlocked = ThrowExn - , failIfDrown = Destroy - } - updateRobotLocation loc nextLoc - return $ Out VUnit s k + moveInDirection $ orient ? zero + Backup -> do + orient <- use robotOrientation + moveInDirection $ applyTurn (DRelative $ DPlanar DBack) $ orient ? zero Push -> do -- Figure out where we're going loc <- use robotLocation @@ -2466,6 +2464,19 @@ execConst c vs s k = do mAch selfDestruct .= True + moveInDirection :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Heading -> m CESK + moveInDirection orientation = do + -- Figure out where we're going + loc <- use robotLocation + let nextLoc = loc `offsetBy` orientation + checkMoveAhead nextLoc $ + MoveFailure + { failIfBlocked = ThrowExn + , failIfDrown = Destroy + } + updateRobotLocation loc nextLoc + return $ Out VUnit s k + -- Make sure nothing is in the way. Note that system robots implicitly ignore -- and base throws on failure. checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails) diff --git a/src/Swarm/Language/Capability.hs b/src/Swarm/Language/Capability.hs index 432727e3e..2e464631c 100644 --- a/src/Swarm/Language/Capability.hs +++ b/src/Swarm/Language/Capability.hs @@ -34,6 +34,8 @@ data Capability CPower | -- | Execute the 'Move' command CMove + | -- | Execute the 'Backup' command + CBackup | -- | Execute the 'Push' command CPush | -- | Execute the 'Stride' command @@ -207,6 +209,7 @@ constCaps = \case Log -> Just CLog Selfdestruct -> Just CSelfdestruct Move -> Just CMove + Backup -> Just CBackup Push -> Just CPush Stride -> Just CMovemultiple Turn -> Just CTurn diff --git a/src/Swarm/Language/Syntax.hs b/src/Swarm/Language/Syntax.hs index d54a7c36f..9f71db449 100644 --- a/src/Swarm/Language/Syntax.hs +++ b/src/Swarm/Language/Syntax.hs @@ -150,6 +150,8 @@ data Const -- | Move forward one step. Move + | -- | Move backward one step. + Backup | -- | Push an entity forward one step. Push | -- | Move forward multiple steps. @@ -520,6 +522,7 @@ constInfo c = case c of , "This destroys the robot's inventory, so consider `salvage` as an alternative." ] Move -> command 0 short "Move forward one step." + Backup -> command 0 short "Move backward one step." Push -> command 1 short . doc "Push an entity forward one step." $ [ "Both entity and robot moves forward one step." diff --git a/src/Swarm/Language/Text/Markdown.hs b/src/Swarm/Language/Text/Markdown.hs new file mode 100644 index 000000000..7f558dccd --- /dev/null +++ b/src/Swarm/Language/Text/Markdown.hs @@ -0,0 +1,279 @@ +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Simple Markdown AST and related utilities. +-- +-- Parametrising 'Document' with the type of +-- inline code and code blocks allows us to +-- inspect and validate Swarm code in descriptions. +-- +-- See 'Swarm.TUI.View.Util.drawMarkdown' for +-- rendering the descriptions as brick widgets. +module Swarm.Language.Text.Markdown ( + -- ** Markdown document + Document (..), + Paragraph (..), + Node (..), + TxtAttr (..), + fromTextM, + + -- ** Token stream + StreamNode' (..), + StreamNode, + ToStream (..), + toText, + + -- ** Utilities + findCode, + chunksOf, +) where + +import Commonmark qualified as Mark +import Commonmark.Extensions qualified as Mark (rawAttributeSpec) +import Control.Applicative ((<|>)) +import Control.Arrow (left) +import Control.Monad (void) +import Data.Functor.Identity (Identity (..)) +import Data.List qualified as List +import Data.List.Split (chop) +import Data.Maybe (catMaybes) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text qualified as T +import Data.Tuple.Extra (both, first) +import Data.Vector (toList) +import Data.Yaml +import Swarm.Language.Module (moduleAST) +import Swarm.Language.Parse (readTerm) +import Swarm.Language.Pipeline (ProcessedTerm (..), processParsedTerm) +import Swarm.Language.Pretty (prettyText, prettyTypeErrText) +import Swarm.Language.Syntax (Syntax) + +-- | The top-level markdown document. +newtype Document c = Document {paragraphs :: [Paragraph c]} + deriving (Eq, Show, Functor, Foldable, Traversable) + deriving (Semigroup, Monoid) via [Paragraph c] + +-- | Markdown paragraphs that contain inline leaf nodes. +-- +-- The idea is that paragraphs do not have line breaks, +-- and so the inline elements follow each other. +-- In particular inline code can be followed by text without +-- space between them (e.g. `logger`s). +newtype Paragraph c = Paragraph {nodes :: [Node c]} + deriving (Eq, Show, Functor, Foldable, Traversable) + deriving (Semigroup, Monoid) via [Node c] + +mapP :: (Node c -> Node c) -> Paragraph c -> Paragraph c +mapP f (Paragraph ns) = Paragraph (map f ns) + +pureP :: Node c -> Paragraph c +pureP = Paragraph . (: []) + +-- | Inline leaf nodes. +-- +-- The raw node is from the raw_annotation extension, +-- and can be used for types/entities/invalid code. +data Node c + = LeafText (Set TxtAttr) Text + | LeafRaw String Text + | LeafCode c + | LeafCodeBlock String c + deriving (Eq, Show, Functor, Foldable, Traversable) + +txt :: Text -> Node c +txt = LeafText mempty + +addTextAttribute :: TxtAttr -> Node c -> Node c +addTextAttribute a (LeafText as t) = LeafText (Set.insert a as) t +addTextAttribute _ n = n + +-- | Simple text attributes that make it easier to find key info in descriptions. +data TxtAttr = Strong | Emphasis + deriving (Eq, Show, Ord) + +instance Mark.Rangeable (Paragraph c) where + ranged _ = id + +instance Mark.HasAttributes (Paragraph c) where + addAttributes _ = id + +instance Mark.Rangeable (Document c) where + ranged _ = id + +instance Mark.HasAttributes (Document c) where + addAttributes _ = id + +-- | Surround some text in double quotes if it is not empty. +quoteMaybe :: Text -> Text +quoteMaybe t = if T.null t then t else T.concat ["\"", t, "\""] + +instance Mark.IsInline (Paragraph Text) where + lineBreak = pureP $ txt "\n" + softBreak = mempty + str = pureP . txt + entity = Mark.str + escapedChar c = Mark.str $ T.pack ['\\', c] + emph = mapP $ addTextAttribute Emphasis + strong = mapP $ addTextAttribute Strong + link dest title desc = pureP (txt "[") <> desc <> pureP (txt $ "](" <> dest <> quoteMaybe title <> ")") + image dest title desc = pureP (txt "!") <> Mark.link dest title desc + code = pureP . LeafCode + rawInline (Mark.Format f) = pureP . LeafRaw (T.unpack f) + +instance Mark.IsBlock (Paragraph Text) (Document Text) where + paragraph = Document . (: []) + plain = Mark.paragraph + thematicBreak = mempty + blockQuote (Document ns) = Document $ map Mark.emph ns + codeBlock f = Mark.plain . pureP . LeafCodeBlock (T.unpack f) + heading _lvl = Mark.plain . Mark.strong + rawBlock (Mark.Format f) t = error . T.unpack $ "Unsupported raw " <> f <> " block:\n" <> t + referenceLinkDefinition = mempty + list _type _spacing = mconcat + +parseSyntax :: Text -> Either String Syntax +parseSyntax t = case readTerm t of + Left e -> Left (T.unpack e) + Right Nothing -> Left "empty code" + Right (Just s) -> case processParsedTerm s of + Left e -> Left (T.unpack $ prettyTypeErrText t e) + Right (ProcessedTerm modul _req _reqCtx) -> Right $ void $ moduleAST modul + +findCode :: Document Syntax -> [Syntax] +findCode = catMaybes . concatMap (map codeOnly . nodes) . paragraphs + where + codeOnly = \case + LeafCode s -> Just s + LeafCodeBlock _i s -> Just s + _l -> Nothing + +instance ToJSON (Paragraph Syntax) where + toJSON = String . toText + +instance ToJSON (Document Syntax) where + toJSON = String . toText + +instance FromJSON (Document Syntax) where + parseJSON v = parsePars v <|> parseDoc v + where + parseDoc = withText "markdown" fromTextM + parsePars = withArray "markdown paragraphs" $ \a -> do + (ts :: [Text]) <- mapM parseJSON $ toList a + fromTextM $ T.intercalate "\n\n" ts + +-- | Read Markdown document and parse&validate the code. +-- +-- If you want only the document with code as `Text`, +-- use the 'fromTextPure' function. +fromTextM :: MonadFail m => Text -> m (Document Syntax) +fromTextM = either fail pure . fromTextE + +fromTextE :: Text -> Either String (Document Syntax) +fromTextE t = fromTextPure t >>= traverse parseSyntax + +-- | Read Markdown document without code validation. +fromTextPure :: Text -> Either String (Document Text) +fromTextPure t = do + let spec = Mark.rawAttributeSpec <> Mark.defaultSyntaxSpec <> Mark.rawAttributeSpec + let runSimple = left show . runIdentity + runSimple $ Mark.commonmarkWith spec "markdown" t + +-------------------------------------------------------------- +-- DIY STREAM +-------------------------------------------------------------- + +-- | This is the naive and easy way to get text from markdown document. +toText :: ToStream a => a -> Text +toText = streamToText . toStream + +-- | Token stream that can be easily converted to text or brick widgets. +-- +-- TODO: #574 Code blocks should probably be handled separately. +data StreamNode' t + = TextNode (Set TxtAttr) t + | CodeNode t + | RawNode String t + | ParagraphBreak + deriving (Eq, Show, Functor) + +type StreamNode = StreamNode' Text + +unStream :: StreamNode' t -> (t -> StreamNode' t, t) +unStream = \case + TextNode a t -> (TextNode a, t) + CodeNode t -> (CodeNode, t) + RawNode a t -> (RawNode a, t) + ParagraphBreak -> error "Logic error: Paragraph break can not be unstreamed!" + +-- | Get chunks of nodes not exceeding length and broken at word boundary. +-- +-- The split will end when no more nodes (then words) can fit or on 'ParagraphBreak'. +chunksOf :: Int -> [StreamNode] -> [[StreamNode]] +chunksOf n = chop (splitter True n) + where + nodeLength :: StreamNode -> Int + nodeLength = T.length . snd . unStream + splitter :: Bool -> Int -> [StreamNode] -> ([StreamNode], [StreamNode]) + splitter start i = \case + [] -> ([], []) + (ParagraphBreak : ss) -> ([ParagraphBreak], ss) + (tn : ss) -> + let l = nodeLength tn + in if l <= i + then first (tn :) $ splitter False (i - l) ss + else let (tn1, tn2) = cut start i tn in ([tn1], tn2 : ss) + cut :: Bool -> Int -> StreamNode -> (StreamNode, StreamNode) + cut start i tn = + let (con, t) = unStream tn + in case splitWordsAt i (T.words t) of + ([], []) -> (con "", con "") + ([], ws@(ww : wws)) -> + both (con . T.unwords) $ + -- In case single word (e.g. web link) does not fit on line we must put + -- it there and guarantee progress (otherwise chop will cycle) + if start then ([ww], wws) else ([], ws) + splitted -> both (con . T.unwords) splitted + +splitWordsAt :: Int -> [Text] -> ([Text], [Text]) +splitWordsAt i = \case + [] -> ([], []) + (w : ws) -> + let l = T.length w + in if l < i + then first (w :) $ splitWordsAt (i - l - 1) ws + else ([], w : ws) + +streamToText :: [StreamNode] -> Text +streamToText = T.concat . map nodeToText + where + nodeToText = \case + TextNode _a t -> t + RawNode _s t -> t + CodeNode stx -> stx + ParagraphBreak -> "\n" + +-- | Convert elements to one dimensional stream of nodes, +-- that is easy to format and layout. +-- +-- If you want to split the stream at line length, use +-- the 'chunksOf' function afterward. +class ToStream a where + toStream :: a -> [StreamNode] + +instance ToStream (Node Syntax) where + toStream = \case + LeafText a t -> TextNode a <$> T.lines t + LeafCode t -> CodeNode <$> T.lines (prettyText t) + LeafRaw s t -> RawNode s <$> T.lines t + LeafCodeBlock _i t -> ParagraphBreak : (CodeNode <$> T.lines (prettyText t)) <> [ParagraphBreak] + +instance ToStream (Paragraph Syntax) where + toStream = concatMap toStream . nodes + +instance ToStream (Document Syntax) where + toStream = List.intercalate [ParagraphBreak] . map toStream . paragraphs diff --git a/src/Swarm/Language/Typecheck.hs b/src/Swarm/Language/Typecheck.hs index fd5c394a7..2a5b8229a 100644 --- a/src/Swarm/Language/Typecheck.hs +++ b/src/Swarm/Language/Typecheck.hs @@ -713,6 +713,7 @@ inferConst c = case c of Noop -> [tyQ| cmd unit |] Selfdestruct -> [tyQ| cmd unit |] Move -> [tyQ| cmd unit |] + Backup -> [tyQ| cmd unit |] Push -> [tyQ| cmd unit |] Stride -> [tyQ| int -> cmd unit |] Turn -> [tyQ| dir -> cmd unit |] diff --git a/src/Swarm/TUI/Attr.hs b/src/Swarm/TUI/Attr.hs index 9251afd15..bbc3537fe 100644 --- a/src/Swarm/TUI/Attr.hs +++ b/src/Swarm/TUI/Attr.hs @@ -35,6 +35,7 @@ module Swarm.TUI.Attr ( notifAttr, infoAttr, boldAttr, + italicAttr, dimAttr, magentaAttr, cyanAttr, @@ -86,6 +87,7 @@ swarmAttrMap = , (notifAttr, fg V.yellow `V.withStyle` V.bold) , (dimAttr, V.defAttr `V.withStyle` V.dim) , (boldAttr, V.defAttr `V.withStyle` V.bold) + , (italicAttr, V.defAttr `V.withStyle` V.italic) , -- Basic colors (redAttr, fg V.red) , (greenAttr, fg V.green) @@ -161,6 +163,7 @@ highlightAttr , notifAttr , infoAttr , boldAttr + , italicAttr , dimAttr , defAttr :: AttrName @@ -168,6 +171,7 @@ highlightAttr = attrName "highlight" notifAttr = attrName "notif" infoAttr = attrName "info" boldAttr = attrName "bold" +italicAttr = attrName "italics" dimAttr = attrName "dim" defAttr = attrName "def" diff --git a/src/Swarm/TUI/Controller.hs b/src/Swarm/TUI/Controller.hs index eb60a2a31..a89956ee5 100644 --- a/src/Swarm/TUI/Controller.hs +++ b/src/Swarm/TUI/Controller.hs @@ -211,7 +211,7 @@ handleMainMenuEvent menu = \case getTutorials :: ScenarioCollection -> ScenarioCollection getTutorials sc = case M.lookup tutorialsDirname (scMap sc) of Just (SICollection _ c) -> c - _ -> error "No tutorials exist!" + _ -> error $ "No tutorials exist: " ++ show sc -- | If we are in a New Game menu, advance the menu to the next item in order. -- diff --git a/src/Swarm/TUI/Launch/Model.hs b/src/Swarm/TUI/Launch/Model.hs index 18aedb551..52cc319fa 100644 --- a/src/Swarm/TUI/Launch/Model.hs +++ b/src/Swarm/TUI/Launch/Model.hs @@ -15,7 +15,7 @@ import Control.Carrier.Throw.Either (runThrow) import Control.Lens (makeLenses) import Data.Functor.Identity (Identity (Identity)) import Data.Text (Text) -import Swarm.Game.Failure.Render (prettyFailure) +import Swarm.Game.Failure (prettyFailure) import Swarm.Game.Scenario.Status (ParameterizableLaunchParams (LaunchParams), ScenarioInfoPair, SerializableLaunchParams) import Swarm.Game.State (LaunchParams, ValidatedLaunchParams, getRunCodePath, parseCodeFile) import Swarm.TUI.Model.Name diff --git a/src/Swarm/TUI/Launch/Prep.hs b/src/Swarm/TUI/Launch/Prep.hs index 13001fd17..df2ded05c 100644 --- a/src/Swarm/TUI/Launch/Prep.hs +++ b/src/Swarm/TUI/Launch/Prep.hs @@ -18,7 +18,7 @@ import Control.Lens ((.=), (^.)) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Functor.Identity (runIdentity) import Data.Text qualified as T -import Swarm.Game.Failure.Render (prettyFailure) +import Swarm.Game.Failure (prettyFailure) import Swarm.Game.Scenario.Status (ParameterizableLaunchParams (..), ScenarioInfoPair, getLaunchParams, scenarioStatus) import Swarm.Game.State (Seed, ValidatedLaunchParams, getRunCodePath, parseCodeFile) import Swarm.TUI.Launch.Model diff --git a/src/Swarm/TUI/Model/StateUpdate.hs b/src/Swarm/TUI/Model/StateUpdate.hs index 0a42c8cb9..668994f67 100644 --- a/src/Swarm/TUI/Model/StateUpdate.hs +++ b/src/Swarm/TUI/Model/StateUpdate.hs @@ -42,8 +42,7 @@ import Data.Time (ZonedTime, getZonedTime) import Swarm.Game.Achievement.Attainment import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Persistence -import Swarm.Game.Failure (SystemFailure) -import Swarm.Game.Failure.Render (prettyFailure) +import Swarm.Game.Failure (SystemFailure, prettyFailure) import Swarm.Game.Log (ErrorLevel (..), LogSource (ErrorTrace)) import Swarm.Game.Scenario (loadScenario, scenarioAttrs, scenarioWorlds) import Swarm.Game.Scenario.Scoring.Best diff --git a/src/Swarm/TUI/View/Objective.hs b/src/Swarm/TUI/View/Objective.hs index b5d578d51..30bdfae98 100644 --- a/src/Swarm/TUI/View/Objective.hs +++ b/src/Swarm/TUI/View/Objective.hs @@ -18,6 +18,7 @@ import Data.Map.Strict qualified as M import Data.Maybe (listToMaybe) import Data.Vector qualified as V import Swarm.Game.Scenario.Objective +import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.TUI.Attr import Swarm.TUI.Model.Goal import Swarm.TUI.Model.Name @@ -87,11 +88,11 @@ drawGoalListItem _isSelected e = case e of Header gs -> withAttr boldAttr $ str $ show gs Goal gs obj -> getCompletionIcon obj gs <+> titleWidget where - textSource = obj ^. objectiveTeaser <|> obj ^. objectiveId <|> listToMaybe (obj ^. objectiveGoal) + textSource = obj ^. objectiveTeaser <|> obj ^. objectiveId <|> listToMaybe (Markdown.toText <$> obj ^. objectiveGoal) titleWidget = maybe (txt "?") (withEllipsis End) textSource singleGoalDetails :: GoalEntry -> Widget Name singleGoalDetails = \case - Goal _gs obj -> displayParagraphs $ obj ^. objectiveGoal + Goal _gs obj -> layoutParagraphs $ drawMarkdown <$> obj ^. objectiveGoal -- Only Goal entries are selectable, so we should never see this: _ -> emptyWidget diff --git a/src/Swarm/TUI/View/Util.hs b/src/Swarm/TUI/View/Util.hs index b4b3dc5aa..5c7a37af0 100644 --- a/src/Swarm/TUI/View/Util.hs +++ b/src/Swarm/TUI/View/Util.hs @@ -22,6 +22,8 @@ import Swarm.Game.ScenarioInfo (scenarioItemName) import Swarm.Game.State import Swarm.Game.Terrain import Swarm.Language.Pretty (prettyText) +import Swarm.Language.Syntax (Syntax) +import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.Language.Types (Polytype) import Swarm.TUI.Attr import Swarm.TUI.Model @@ -114,6 +116,26 @@ generateModal s mt = Modal mt (dialog (Just $ str title) buttons (maxModalWindow drawType :: Polytype -> Widget Name drawType = withAttr infoAttr . padLeftRight 1 . txt . prettyText +-- | Draw markdown document with simple code/bold/italic attributes. +-- +-- TODO: #574 Code blocks should probably be handled separately. +drawMarkdown :: Markdown.Document Syntax -> Widget Name +drawMarkdown d = do + Widget Greedy Fixed $ do + ctx <- getContext + let w = ctx ^. availWidthL + let docLines = Markdown.chunksOf w $ Markdown.toStream d + render $ vBox $ map (hBox . map mTxt) docLines + where + mTxt = \case + Markdown.TextNode as t -> foldr applyAttr (txt t) as + Markdown.CodeNode t -> withAttr highlightAttr $ txt t + Markdown.RawNode _f t -> withAttr highlightAttr $ txt t + Markdown.ParagraphBreak -> txt "" + applyAttr a = withAttr $ case a of + Markdown.Strong -> boldAttr + Markdown.Emphasis -> italicAttr + drawLabeledTerrainSwatch :: TerrainType -> Widget Name drawLabeledTerrainSwatch a = tile <+> str materialName @@ -147,10 +169,15 @@ locationToString :: Location -> String locationToString (Location x y) = unwords $ map show [x, y] --- | Display a list of text-wrapped paragraphs with one blank line after --- each. +-- | Display a list of text-wrapped paragraphs with one blank line after each. displayParagraphs :: [Text] -> Widget Name -displayParagraphs = vBox . map (padBottom (Pad 1) . txtWrap) +displayParagraphs = layoutParagraphs . map txtWrap + +-- | Display a list of paragraphs with one blank line after each. +-- +-- For the common case of `[Text]` use 'displayParagraphs'. +layoutParagraphs :: [Widget Name] -> Widget Name +layoutParagraphs ps = vBox $ padBottom (Pad 1) <$> ps data EllipsisSide = Beginning | End diff --git a/src/Swarm/Util/Effect.hs b/src/Swarm/Util/Effect.hs index 25439cfa2..ea7effcd4 100644 --- a/src/Swarm/Util/Effect.hs +++ b/src/Swarm/Util/Effect.hs @@ -13,8 +13,7 @@ import Control.Monad.Trans.Except (ExceptT) import Data.Either.Extra (eitherToMaybe) import Data.Sequence (Seq) import Data.Sequence qualified as Seq -import Swarm.Game.Failure (SystemFailure) -import Swarm.Game.Failure.Render (prettyFailure) +import Swarm.Game.Failure (SystemFailure, prettyFailure) import Witch (into) import Witherable diff --git a/src/Swarm/Web.hs b/src/Swarm/Web.hs index dc61e9698..6d48062a3 100644 --- a/src/Swarm/Web.hs +++ b/src/Swarm/Web.hs @@ -23,7 +23,7 @@ module Swarm.Web where import Brick.BChan -import CMarkGFM qualified as CMark (commonmarkToHtml) +import Commonmark qualified as Mark (commonmark, renderHtml) import Control.Arrow (left) import Control.Concurrent (forkIO) import Control.Concurrent.MVar @@ -37,7 +37,6 @@ import Data.IntMap qualified as IM import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T -import Data.Text.Lazy qualified as L import Data.Text.Lazy.Encoding (encodeUtf8) import Data.Tree (Tree (Node), drawTree) import Network.HTTP.Types (ok200) @@ -101,8 +100,8 @@ api = Proxy docsBS :: ByteString docsBS = encodeUtf8 - . L.fromStrict - . CMark.commonmarkToHtml [] [] + . either (error . show) (Mark.renderHtml @()) + . Mark.commonmark "" . T.pack . SD.markdownWith ( SD.defRenderingOptions diff --git a/swarm.cabal b/swarm.cabal index 081f9ccf1..ee4090c89 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -88,7 +88,6 @@ library Swarm.Doc.Gen Swarm.Doc.Pedagogy Swarm.Game.Failure - Swarm.Game.Failure.Render Swarm.Game.Achievement.Attainment Swarm.Game.Achievement.Definitions Swarm.Game.Achievement.Description @@ -151,6 +150,7 @@ library Swarm.Language.Pretty Swarm.Language.Requirement Swarm.Language.Syntax + Swarm.Language.Text.Markdown Swarm.Language.Typecheck Swarm.Language.Typecheck.Unify Swarm.Language.Typed @@ -203,8 +203,9 @@ library brick >= 1.5 && < 1.10, bytestring >= 0.10 && < 0.12, clock >= 0.8.2 && < 0.9, - cmark-gfm >= 0.2 && < 0.3, colour >= 2.3.6 && < 2.4, + commonmark >= 0.2 && < 0.3, + commonmark-extensions >= 0.2 && < 0.3, containers >= 0.6.2 && < 0.7, directory >= 1.3 && < 1.4, dotgen >= 0.4 && < 0.5, diff --git a/test/integration/Main.hs b/test/integration/Main.hs index cd75ab601..898ef38ea 100644 --- a/test/integration/Main.hs +++ b/test/integration/Main.hs @@ -309,6 +309,7 @@ testScenarioSolution rs ui _ci _em = , testSolution Default "Testing/144-subworlds/subworld-mapped-robots" , testSolution Default "Testing/144-subworlds/subworld-located-robots" , testSolution Default "Testing/1379-single-world-portal-reorientation" + , testSolution Default "Testing/1399-backup-command" ] ] where