diff --git a/app/Main.hs b/app/Main.hs index 6d4d3f69b..84565af7d 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -117,6 +117,7 @@ cliParser = , Just Recipes <$ switch (long "recipes" <> help "Generate recipes page (uses data from recipes.yaml)") , Just Capabilities <$ switch (long "capabilities" <> help "Generate capabilities page (uses entity map)") , Just Commands <$ switch (long "commands" <> help "Generate commands page (uses constInfo, constCaps and inferConst)") + , Just Scenario <$ switch (long "scenario" <> help "Generate scenario schema page") ] seed :: Parser (Maybe Int) seed = optional $ option auto (long "seed" <> short 's' <> metavar "INT" <> help "Seed to use for world generation") diff --git a/data/scenarios/README.md b/data/scenarios/README.md index 5c7ba1b42..23f3dccd9 100644 --- a/data/scenarios/README.md +++ b/data/scenarios/README.md @@ -58,250 +58,18 @@ your editor to highlight the errors as you are writing. If you are using Visual Studio Code or VSCodium, you need to have the [YAML extension](https://open-vsx.org/extension/redhat/vscode-yaml) -installed. - -To point the editor to the right schema for scenarios in this repository, -you can use this `settings.json`: -```JSON -{ - "yaml.schemas": { - "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/scenario.json": [ - "data/scenarios/*.yaml", - "data/scenarios/**/*.yaml" - ], - "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entities.json": [ - "data/entities.yaml" - ], - "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/recipes.json": [ - "data/recipes.yaml" - ] - } -} -``` +installed. The appropriate [`settings.json`](https://github.com/swarm-game/swarm/blob/main/.vscode/settings.json) is already configured for you in the cloned `swarm` repo. #### CLI You can also check the files from the command line: ```Bash -# install latest jsonschema executable version (tested with 4.9.1) -pip install jsonschema +# install latest check-jsonschema executable version +pip install check-jsonschema # try it on provided scenarios -yq eval scenarios/creative.yaml -o json | jsonschema data/schema/scenario.json -# try that it works on empty JSON -echo {} | jsonschema data/schema/scenario.json -# {}: 'name' is a required property -# {}: 'world' is a required property -# {}: 'robots' is a required property +scripts/validate-json-schemas.sh ``` -### YAML conventions - -Objects (key-value mappings) are described below using tables. Note -that a blank "Default?" column means the key is required; other keys -are optional and take on the indicated default value when they are not -present. The order of keys in a key-value mapping does not matter. - -YAML is untyped, but we try to give a more precise idea of the -expected types in the tables below. -- `foo list` means a list where all the elements are of type `foo`. -- A type like `int × int` means a pair of int values. YAML does not actually have - tuples, only lists, so in practice, an `int × int` value is written - as a list with exactly two elements. Likewise, `int × string` - denotes a list with exactly two elements, the first being an `int` - and the second being a `string`. -- `object` denotes a generic key-value mapping. Whenever `object` is - used, you will find a link to a more specific description of the - keys and values expected. - -### Top level - -At the top level, a scenario file contains a key-value mapping, -described by the following table. - -| Key | Default? | Type | Description | -|----------------|----------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `version` | | `int` | The version number of the scenario schema. Currently, this should always be 1. | -| `name` | | `string` | The name of the scenario. For official scenarios, this is what shows up in the new game menu. | -| `description` | `""` | `string` | A short description of the scenario. This shows up next to the new game menu when the scenario is selected. | -| `author` | `null` | `string` | The author of the scenario (optional). Typically this is a person's name, but it can be any string. It is displayed under the scenario description in the new game menu. | -| `creative` | `False` | `boolean` | Whether the scenario should start out in creative mode. | -| `seed` | `null` | `int` | An optional seed that will be used to seed the random number generator. If a procedurally generated world is used, the seed hence determines the world. Hence, if the seed is specified, the procedurally generated world will be exactly the same every time, for every player. If omitted, a random seed will be used every time the scenario is loaded. | -| `entities` | `[]` | [`entity`](#entities) list | An optional list of custom entities, to be used in addition to the built-in entities. See [Entities](#entities). | -| `recipes` | `[]` | [`recipe`](#recipes) list | An optional list of custom recipes, to be used in addition to the built-in recipes. They can refer to built-in entities as well as custom entities. See [Recipes](#recipes). | -| `known` | `[]` | `string list` | A list of names of standard or custom entities which should have the `Known` property added to them; that is, robots should know what they are without having to scan them. | -| `world` | | `object` | A description of the world. See [World](#world). | -| `robots` | | [`robot`](#robots) list | A list of robots that will inhabit the world. See [Robots](#robots). | -| `objectives` | `[]` | [`objective`](#objectives) list | An optional list of objectives, aka winning conditions. The player has to complete the objectives in sequence to win. See [Objectives](#objectives). | -| `solution` | `null` | `string` | The (optional) text of a Swarm program that, when run on the base robot, completes all the objectives. For scenarios which are officially part of the Swarm repository, such a solution will be tested as part of CI testing. For scenarios loaded directly from a file, any provided solution is simply ignored. | -| `stepsPerTick` | `null` | `int` | When present, this specifies the maximum number of CESK machine steps each robot is allowed to take per game tick. It is rather obscure and technical and only used in a few automated tests; most scenario authors should not need this. | - -### Entities - -The top-level `entities` field contains a list of entity descriptions. -Each entity description is a key-value mapping described by the following -table. - -| Key | Default? | Type | Description | -|----------------|----------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | | `string` | The name of the entity. This is what will show up in the inventory and how the entity can be referred to. | -| `display` | | `object` | [Display](#display) information for the entity. | -| `plural` | `null` | `string` | An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name. | -| `description` | | `string list` | A description of the entity, as a list of paragraphs. | -| `orientation` | `null` | `int × int` | A 2-tuple of integers specifying an orientation vector for the entity. Currently unused. | -| `growth` | `null` | `int × int` | For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown. | -| `combustion` | | `object` | [Combustion](#combustion) information for the entity. | -| `yields` | `null` | `string` | The name of the entity which will be added to a robot's inventory when it executes `grab` or `harvest` on this entity. If omitted, the entity will simply yield itself. | -| `properties` | `[]` | `string list` | A list of properties of this entity. See [Entity properties](#entity-properties). | -| `capabilities` | `[]` | `string list` | A list of capabilities provided by entity, when it is equipped as a device. See [Capabilities](#capabilities). | - -#### Entity properties - -The properties an entity may possess are listed below. Each entity -may possess any number of properties. - -- `unwalkable`: robots cannot `move` into a cell containing this - entity. If they try, the `move` command will throw an exception. - -- `portable`: robots can pick this up using `grab` or `harvest`. - Trying to execute `grab` or `harvest` on an entity that is not - `portable` will throw an exception. - -- `growable`: when `harvest`ed, the entity will regrow from a seed. - -- `infinite`: when `grab`bed or `harvest`ed, the entity will - immediately respawn. - -- `known`: robots know what this is without having to `scan` it first, - hence it does not show up as a question mark. - -#### Capabilities - -Each capability enables the evaluation of execution of one or more -commands or language constructs. Rather than listing all possible -capabilities here, which would be annoying to keep up-to-date, see the -(automatically generated) [Commands cheat -sheet](https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet) -on the Swarm wiki. - -### Combustion - -The *combustion* property specifies whether and how an entity may combust, described by the following table. - -| Key | Default? | Type | Description | -|------------------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ignition` | `0.5` | `number` | The rate of ignition by a neighbor, per tick. | -| `duration` | `null` | `int × int` | For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for combustion. | -| `product` | `ash` | `string` | What entity, if any, is left over after combustion | - -### Display - -A *display* specifies how an entity or a robot (robots are essentially -special kinds of entities) is displayed in the world. It consists of -a key-value mapping described by the following table. - -| Key | Default? | Type | Description | -|------------------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `char` | `' '` | `string` | The default character that should be used to draw the robot or entity. | -| `orientationMap` | `{}` | | A map to override display character for any of the (lowercase) cardinal directions | -| `curOrientation` | `null` | | TODO currently unused | -| `attr` | `entity` | `string` | The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found at https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/Attr.hs. | -| `priority` | `1` | `int` | When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority `1`, and robots have priority `10`. | -| `invisible` | `False` | `boolean` | Whether the entity or robot should be invisible. Invisible entities and robots are not drawn, but can still be interacted with in otherwise normal ways. System robots are invisible by default. | - - -### Recipes - -The top-level `recipes` field contains a list of recipe descriptions. -Each recipe is a key-value mapping describing a process that takes some -inputs and produces some outputs, which robots can access using `make` -and `drill`. - -| Key | Default? | Type | Description | -|------------|----------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `in` | | `(int × string) list` | A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed. | -| `out` | | `(int × string) list` | A list of outputs produced by the recipe. It is a list of [count, entity name] tuples just like `in`. | -| `required` | `[]` | `(int × string) list` | A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of [count, entity name] tuples just like `in` and `out`. | -| `time` | 1 | `int` | The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will `wait` for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes. | -| `weight` | 1 | `int` | Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a `widget`, one with weight `1` and the other with weight `9`. When a robot executes `make "widget"`, the first recipe will be chosen 10% of the time, and the second recipe 90% of the time. | - -### World - -The top-level `world` field contains a key-value mapping describing the -world, that is, a description of the terrain and entities that exist -at various locations. - -| Key | Default? | Type | Description | -|--------------|----------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `dsl` | `null` | `string` | An expression of the [Swarm world description DSL](../worlds/README.md). If specified, this will be used as the base layer for the world. | -| `offset` | `False` | `boolean` | Whether the `base` robot's position should be moved to the nearest "good" location, currently defined as a location near a tree, in a 16x16 patch which contains at least one each of `tree`, `copper ore`, `bit (0)`, `bit (1)`, `rock`, `lambda`, `water`, and `sand`. The `classic` scenario uses `offset: True` to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game. See https://github.com/swarm-game/swarm/blob/main/src/Swarm/Game/WorldGen.hs#L204 . | -| `scrollable` | `True` | `boolean` | Whether players are allowed to scroll the world map. | -| `palette` | `{}` | `object` | The `palette` maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See [Cells](#cells) for the contents of the tuples representing a cell. | -| `map` | `""` | `string` | A rectangular string, using characters from the `palette`, exactly specifying the contents of a rectangular portion of the world. Leading spaces are ignored. The rest of the world is either filled by the `default` cell, or by procedural generation otherwise. Note that this is optional; if omitted, the world will simply be filled with the `default` cell or procedurally generated. | -| `upperleft` | `[0,0]` | `int × int` | A 2-tuple of `int` values specifying the (x,y) coordinates of the upper left corner of the `map`. | - -#### Cells - -Each cell of the world is specified by a list of terrain, optional entity -and robots present (if any). For example, `[grass]`, `[grass, tree]`, -or `[grass, null, base]`. - -- The first (required) item specifies the terrain. Currently, valid - terrain values are `stone`, `dirt`, `grass`, `ice`, or `blank`. -- The second item (if present) specifies the name of an entity which - should be present in the cell. This may be a built-in entity, or a - custom entity specified in the `entities` section. `null` may be - used to explicitly specify no entity in the cell. -- The third item and later (if present) specifies the names of the robots - which should be present in the cell. These must be names of robots - specified in the `robots` section. A copy of each robot will be - created at each location in the `map` where it is drawn. - - Although multiple robots may be in a single location in general, - there is currently no way to specify more than one robot for a - cell in the world description. - -If a 1-tuple is used, it specifies a terrain value with no entity or -robot. A 2-tuple specifies a terrain value and entity, but no robot. - -### Robots - -The top-level `robots` field contains a list of robot descriptions. -Each robot description is a key-value mapping described by the following -table. - -| Key | Default? | Type | Description | -|---------------|----------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | | `string` | The name of the robot. This shows up in the list of robots in the game (F2), and is also how the robot will be referred to in the [world](#world) `palette`. | -| `description` | `[]` | `string list` | A description of the robot, given as a list of paragraphs. This is currently not used for much (perhaps not at all?). | -| `loc` | `null` | `int × int` | An optional (x,y) starting location for the robot. If the `loc` field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a *template* which can be referenced from a [cell](#cells) in the [world](#world) `palette`. Concrete robots will then be created wherever the corresponding palette character is used in the world `map`. | -| `dir` | `[0,0]` | `int × int` | An optional starting orientation of the robot, expressed as a vector. Every time the robot executes a `move` command, this vector will be added to its position. Typically, this is a unit vector in one of the four cardinal directions, although there is no particular reason that it has to be. When omitted, the robot's direction will be the zero vector. | -| `display` | default | `map` | [Display](#display) information for the robot. If this field is omitted, the [default robot display](#display) will be used. | -| `program` | `null` | `string` | This is the text of a Swarm program which the robot should initially run, and must be syntax- and type-error-free. If omitted, the robot will simply be idle. | -| `devices` | `[]` | `string list` | A list of entity names which should be *equipped* as the robot's devices, i.e. entities providing capabilities to run commands and interpret language constructs. | -| `inventory` | `[]` | `(int × string) list` | A list of [count, entity name] pairs, specifying the entities in the robot's starting inventory, and the number of each. | -| `system` | `False` | `boolean` | Whether the robot is a "system" robot. System robots can do anything, without regard for devices and capabilities. System robots are invisible by default. | -| `heavy` | `False` | `boolean` | Whether the robot is heavy. Heavy robots require `tank treads` to `move` (rather than just `treads` for other robots). | - -#### Base robot - -There must be at most one **base** robot in the world. Since concrete robots can be created -either via the `loc` attribute or via the map and palette, use the following guide to -ensure the base robot is the one you intended: - -1. Always list the intended **base** as the first robot definition in your scenario. -2. The first robot with a `loc` attribute will become the base, even if other robots are defined earlier. -3. Without any located robots, if multiple robots are instantiated on the map from - the first robot definition, the first robot in - [row-major order](https://en.wikipedia.org/wiki/Row-_and_column-major_order) - shall be the base. - -### Objectives - -The top-level `objectives` field contains a list of objectives that -must be completed in sequence. Each objective is a key-value mapping -described by the following table. +### YAML schema -| Key | Default? | Type | Description | -|-------------|----------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `goal` | `[]` | `string list` | A list of paragraphs describing the objective. This text is shown to the player in a popup dialog box as soon as the scenario starts, or the previous objective is completed, and the player can recall the popup at any time with `Ctrl-G`. | -| `condition` | | `string` | The condition is the text of a Swarm program of type `cmd bool`, which will be run once per game tick on a freshly generated system robot. It is run hypothetically, that is, it is run in a copy of the current game state which is thrown away once the program has run to completion. The condition is met when this program returns `true`. | +See the autogenerated [`SCHEMA.md`](doc-fragments/SCHEMA.md). \ No newline at end of file diff --git a/data/scenarios/doc-fragments/SCHEMA.md b/data/scenarios/doc-fragments/SCHEMA.md new file mode 100644 index 000000000..d61ddaba1 --- /dev/null +++ b/data/scenarios/doc-fragments/SCHEMA.md @@ -0,0 +1,356 @@ +## Swarm YAML schema + +### YAML conventions + +Objects (key-value mappings) are described below using tables. Note +that a blank "**Default?**" column means the key is required; other keys +are optional and take on the indicated default value when they are not +present. The order of keys in a key-value mapping does not matter. + +YAML is untyped, but we try to give a more precise idea of the +expected types in the tables below. +- `foo list` means a list where all the elements are of type `foo`. +- Some values are tuples. The types and meaning of such tuple element + are presented in tables with an "**Index**" column. + +### Top level + +At the top level, a scenario file contains a key-value mapping described +by the following table. + +| Key | Default? | Type | Description | +|----------------|----------|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `attrs` | | [attribute](#attributes "Link to object properties") list | A list of local attribute definitions | +| `author` | | `string` | The author of the scenario (optional). Typically this is a person's name, but it can be any string. It is displayed under the scenario description in the new game menu. | +| `creative` | `False` | `boolean` | Whether the scenario should start out in creative mode. | +| `description` | | `string` | A short description of the scenario. This shows up next to the new game menu when the scenario is selected. | +| `entities` | `[]` | [entity](#entity "Link to object properties") list | An optional list of custom entities, to be used in addition to the built-in entities. | +| `known` | `[]` | `string` list | A list of names of standard or custom entities which should have the Known property added to them; that is, robots should know what they are without having to scan them. | +| `name` | | `string` | The name of the scenario. For official scenarios, this is what shows up in the new game menu. | +| `objectives` | `[]` | [objective](#objective "Link to object properties") list | An optional list of objectives, aka winning conditions. The player has to complete the objectives in sequence to win. | +| `recipes` | `[]` | [recipe](#recipe "Link to object properties") list | An optional list of custom recipes, to be used in addition to the built-in recipes. They can refer to built-in entities as well as custom entities. | +| `robots` | | [robot](#robot "Link to object properties") list | A list of robots that will inhabit the world. | +| `seed` | | `number` | An optional seed that will be used to seed the random number generator. If a procedurally generated world is used, the seed hence determines the world. Hence, if the seed is specified, the procedurally generated world will be exactly the same every time, for every player. If omitted, a random seed will be used every time the scenario is loaded. | +| `solution` | | `string` | The (optional) text of a Swarm program that, when run on the base robot, completes all the objectives. For scenarios which are officially part of the Swarm repository, such a solution will be tested as part of CI testing. | +| `stepsPerTick` | | `number` | When present, this specifies the maximum number of CESK machine steps each robot is allowed to take per game tick. It is rather obscure and technical and only used in a few automated tests; most scenario authors should not need this. | +| `structures` | | [named-structure](#named-structure "Link to object properties") list | Structure definitions | +| `subworlds` | | [world](#world "Link to object properties") list | A list of subworld definitions | +| `version` | | `number` | The version number of the scenario schema. Currently, this should always be `1`. | +| `world` | | [world](#world "Link to object properties") | | + +### Attributes + +Scenario-local attribute definitions + +| Key | Default? | Type | Description | +|---------|----------|---------------|-----------------------| +| `bg` | | `string` | Background color | +| `fg` | | `string` | Foreground color | +| `name` | | `string` | Name of attribute | +| `style` | | `string` list | Style properties list | + +### Entity + +Description of an entity in the Swarm game + +| Key | Default? | Type | Description | +|----------------|----------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `capabilities` | `[]` | `string` list | A list of capabilities provided by entity, when it is equipped as a device. See [Capabilities](https://github.com/swarm-game/swarm/wiki/Capabilities-cheat-sheet). | +| `combustion` | | [combustion](#combustion "Link to object properties") | Properties of combustion. | +| `description` | | `string` list | A description of the entity, as a list of paragraphs. | +| `display` | | [display](#display "Link to object properties") | Display information for the entity. | +| `growth` | | `array` | For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown. | +| `name` | | `string` | The name of the entity. This is what will show up in the inventory and how the entity can be referred to. | +| `orientation` | | `array` | A 2-tuple of integers specifying an orientation vector for the entity. Currently unused. | +| `plural` | | `string` | An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name. | +| `properties` | `[]` | `string` list | A list of properties of this entity. | +| `yields` | | `string` | The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself. | + +#### Entity properties + +The properties an entity may possess are listed below. Each entity may +possess any number of properties. + +- `unwalkable`: robots cannot `move` into a cell containing this + entity. If they try, the `move` command will throw an exception. + +- `portable`: robots can pick this up using `grab` or `harvest`. + Trying to execute `grab` or `harvest` on an entity that is not + `portable` will throw an exception. + +- `growable`: when `harvest`ed, the entity will regrow from a seed. + +- `infinite`: when `grab`bed or `harvest`ed, the entity will + immediately respawn. + +- `known`: robots know what this is without having to `scan` it first, + hence it does not show up as a question mark. + +#### Capabilities + +Each capability enables the evaluation of execution of one or more +commands or language constructs. Rather than listing all possible +capabilities here, which would be annoying to keep up-to-date, see the +(automatically generated) [Commands cheat +sheet](https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet) on +the Swarm wiki. + +### Combustion + +Properties of entity combustion + +| Key | Default? | Type | Description | +|------------|--------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `duration` | `[100, 200]` | [range](#numeric-range "Link to object properties") | For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time that the combustion shall persist. | +| `ignition` | `0.5` | `number` | Rate of ignition by a neighbor, per tick. | +| `product` | `"ash"` | `string` or `null` | What entity, if any, is left over after combustion | + +### Robot + +Description of a robot in the Swarm game + +| Key | Default? | Type | Description | +|---------------|-------------|--------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `description` | | `string` | A description of the robot. This is currently not used for much, other than scenario documentation. | +| `devices` | `[]` | `string` list | A list of entity names which should be equipped as the robot's devices, i.e. entities providing capabilities to run commands and interpret language constructs. | +| `dir` | `[0, 0]` | `array` | An optional starting orientation of the robot, expressed as a vector. Every time the robot executes a `move` command, this vector will be added to its position. Typically, this is a unit vector in one of the four cardinal directions, although there is no particular reason that it has to be. When omitted, the robot's direction will be the zero vector. | +| `display` | `"default"` | [display](#display "Link to object properties") | Display information for the robot. If this field is omitted, the default robot display will be used. | +| `heavy` | `False` | `boolean` | Whether the robot is heavy. Heavy robots require `tank treads` to `move` (rather than just `treads` for other robots). | +| `inventory` | `[]` | [inventory](#inventory "Link to object properties") | A list of `[count, entity name]` pairs, specifying the entities in the robot's starting inventory, and the number of each. | +| `loc` | | [cosmic-loc](#cosmic-location "Link to object properties") or [planar-loc](#planar-location "Link to object properties") | An optional starting location for the robot. If the `loc` field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map. | +| `name` | | `string` | The name of the robot. This shows up in the list of robots in the game (`F2`), and is also how the robot will be referred to in the world palette. | +| `program` | | `string` | This is the text of a Swarm program which the robot should initially run, and must be syntax- and type-error-free. If omitted, the robot will simply be idle. | +| `system` | `False` | `boolean` | Whether the robot is a "system" robot. System robots can do anything, without regard for devices and capabilities. System robots are invisible by default. | +| `unwalkable` | `[]` | `string` list | A list of entities that this robot cannot walk across. | + +#### Base robot + +There must be at most one **base** robot in the world. Since concrete +robots can be created either via the `loc` attribute or via the map and +palette, use the following guide to ensure the base robot is the one you +intended: + +1. Always list the intended **base** as the first robot definition in + your scenario. +2. The first robot with a `loc` attribute will become the base, even if + other robots are defined earlier. +3. Without any located robots, if multiple robots are instantiated on + the map from the first robot definition, the first robot in + [row-major + order](https://en.wikipedia.org/wiki/Row-_and_column-major_order) + shall be the base. + +### Cosmic location + +Planar location plus subworld + +| Key | Default? | Type | Description | +|------------|----------|------------------------------------------------------------|------------------| +| `loc` | | [planar-loc](#planar-location "Link to object properties") | | +| `subworld` | | `string` | Name of subworld | + +### Display + +Swarm entity display. A display specifies how an entity or a robot +(robots are essentially special kinds of entities) is displayed in the +world. It consists of a key-value mapping described by the following +table. + +| Key | Default? | Type | Description | +|------------------|---------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `attr` | `"entity"` | `string` | The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found [here](https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/View/Attribute/Attr.hs). | +| `char` | `" "` | `string` | The default character that should be used to draw the robot or entity. | +| `curOrientation` | | `array` | Currently unused | +| `invisible` | `False` | `boolean` | Whether the entity or robot should be invisible. Invisible entities and robots are not drawn, but can still be interacted with in otherwise normal ways. System robots are by default invisible. | +| `orientationMap` | `fromList []` | [orientation-map](#orientation-map "Link to object properties") | | +| `priority` | `1` | `number` | When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority `1`, and robots have priority `10`. | + +### Recipe + +Recipe describes a process that takes some inputs and produces some +outputs, which robots can access using `make` and `drill`. + +| Key | Default? | Type | Description | +|------------|----------|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `in` | | [inventory](#inventory "Link to object properties") | A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed. | +| `out` | | [inventory](#inventory "Link to object properties") | A list of outputs produced by the recipe. It is a list of `[count, entity name]` tuples just like `in`. | +| `required` | `[]` | [inventory](#inventory "Link to object properties") | A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of \[count, entity name\] tuples just like in and out. | +| `time` | `1` | `number` | The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will wait for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes. | +| `weight` | `1` | `number` | Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a widget, one with weight `1` and the other with weight `9`. When a robot executes `make "widget"`, the first recipe will be chosen 10% of the time, and the second recipe 90% of the time. | + +### Inventory + +List of [entity-count](#entity-count "Link to object properties") + +### Entity count + +One row in an inventory list + +| Index | Type | Description | +|-------|----------|-------------| +| `0` | `number` | Quantity | +| `1` | `string` | Entity name | + +### World + +Description of the world in the Swarm game + +| Key | Default? | Type | Description | +|--------------|----------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | | `string` list | Default world cell content | +| `dsl` | | `string` | A term in the Swarm world description DSL. The world it describes will be layered underneath the world described by the rest of the fields. | +| `map` | `""` | `string` | A rectangular string, using characters from the palette, exactly specifying the contents of a rectangular portion of the world. Leading spaces are ignored. The rest of the world is either filled by the default cell, or by procedural generation otherwise. Note that this is optional; if omitted, the world will simply be filled with the default cell or procedurally generated. | +| `name` | | `string` | Name of this subworld | +| `offset` | `False` | `boolean` | Whether the base robot's position should be moved to the nearest "good" location, currently defined as a location near a `tree`, in a 16x16 patch which contains at least one each of `tree`, `copper ore`, `bit (0)`, `bit (1)`, `rock`, `lambda`, `water`, and `sand`. The classic scenario uses `offset: True` to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game (see [code](https://github.com/swarm-game/swarm/blob/e06e04f622a3762a10e7c942c1cbd2c1e396144f/src/Swarm/Game/World/Gen.hs#L79)). | +| `palette` | | `object` | The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See [Cells](#cells) for the contents of the tuples representing a cell. | +| `placements` | | [placement](#placement "Link to object properties") list | Structure placements | +| `portals` | | [portal](#portal "Link to object properties") list | A list of portal definitions that reference waypoints. | +| `scrollable` | `True` | `boolean` | Whether players are allowed to scroll the world map. | +| `structures` | | [named-structure](#named-structure "Link to object properties") list | Structure definitions | +| `upperleft` | `[0, 0]` | `array` | A 2-tuple of `int` values specifying the `(x,y)` coordinates of the upper left corner of the map. | +| `waypoints` | | [explicit-waypoint](#waypoint "Link to object properties") list | Single-location waypoint definitions | + +#### Cells + +Each cell of the world is specified by a list of terrain, optional +entity and robots present (if any). For example, `[grass]`, +`[grass, tree]`, or `[grass, null, base]`. + +- The first (required) item specifies the terrain. Currently, valid + terrain values are `stone`, `dirt`, `grass`, `ice`, or `blank`. + +- The second item (if present) specifies the name of an entity which + should be present in the cell. This may be a built-in entity, or a + custom entity specified in the `entities` section. `null` may be + used to explicitly specify no entity in the cell. + +- The third item and later (if present) specifies the names of the + robots which should be present in the cell. These must be names of + robots specified in the `robots` section. A copy of each robot will + be created at each location in the `map` where it is drawn. + + Although multiple robots may be in a single location in general, + there is currently no way to specify more than one robot for a cell + in the world description. + +If a 1-tuple is used, it specifies a terrain value with no entity or +robot. A 2-tuple specifies a terrain value and entity, but no robot. + +### Named structure + +Structure definitions + +| Key | Default? | Type | Description | +|-------------|----------|-----------------------------------------------------|---------------------------| +| `name` | | `string` | Name of this substructure | +| `structure` | | [structure](#structure "Link to object properties") | | + +### Structure + +Structure properties + +| Key | Default? | Type | Description | +|--------------|----------|----------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `map` | | `string` | Cell-based representation of the structure using palette entries | +| `mask` | | `string` | A special palette character that indicates that map cell should be transparent | +| `palette` | | `object` | Structure properties | +| `placements` | | [placement](#placement "Link to object properties") list | Structure placements | +| `structures` | | [named-structure](#named-structure "Link to object properties") list | Nested structure definitions | +| `waypoints` | | [explicit-waypoint](#waypoint "Link to object properties") list | Single-location waypoint definitions | + +### Waypoint + +Explicit waypoint definition + +| Key | Default? | Type | Description | +|--------|----------|------------------------------------------------------------|---------------| +| `loc` | | [planar-loc](#planar-location "Link to object properties") | | +| `name` | | `string` | Waypoint name | + +### Objective + +Scenario goals and their prerequisites. The top-level objectives field +contains a list of objectives that must be completed in sequence. Each +objective has a goal description and a condition. + +| Key | Default? | Type | Description | +|----------------|----------|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `condition` | | `string` | A swarm program that will be hypothetically run each tick to check if the condition is fulfilled. | +| `goal` | | `string` list | The goal description as a list of paragraphs that the player can read. | +| `hidden` | | `boolean` | Whether this goal should be suppressed from the Goals dialog prior to achieving it | +| `id` | | `string` | A short identifier for referencing as a prerequisite | +| `optional` | | `boolean` | Whether completion of this objective is required to achieve a 'Win' of the scenario | +| `prerequisite` | | [prerequisite](#prerequisite "Link to object properties") | | +| `teaser` | | `string` | A compact (2-3 word) summary of the goal | + +### Orientation map + +Mapping from cardinal directions to display characters + +| Key | Default? | Type | Description | +|---------|----------|----------|-------------| +| `east` | | `string` | | +| `north` | | `string` | | +| `south` | | `string` | | +| `west` | | `string` | | + +### Placement + +Structure placement + +| Key | Default? | Type | Description | +|----------|----------|------------------------------------------------------------------------|------------------------------| +| `offset` | | [planar-loc](#planar-location "Link to object properties") | | +| `orient` | | [structure-orient](#structure-orientation "Link to object properties") | | +| `src` | | `string` | Name of structure definition | + +### Planar location + +x and y coordinates of a location in a particular world + +| Index | Type | Description | +|-------|----------|--------------| +| `0` | `number` | X coordinate | +| `1` | `number` | Y coordinate | + +### Portal + +Portal definition + +| Key | Default? | Type | Description | +|--------------|----------|---------------------------------------------------------|-----------------------------------------------------------| +| `consistent` | | `boolean` | Whether this portal is spatially consistent across worlds | +| `entrance` | | `string` | Name of entrance waypoint | +| `exitInfo` | | [portal-exit](#portal-exit "Link to object properties") | | +| `reorient` | | `string` | Passing through this portal changes a robot's orientation | + +### Portal exit + +Properties of a portal's exit + +| Key | Default? | Type | Description | +|----------------|----------|----------|-----------------------| +| `exit` | | `string` | Name of exit waypoint | +| `subworldName` | | `string` | Name of exit subworld | + +### Prerequisite + +Prerequisite conditions for an objective. + +### Numeric range + +Min/max range of a value + +| Index | Type | Description | +|-------|----------|-------------| +| `0` | `number` | minimum | +| `1` | `number` | maximum | + +### Structure orientation + +Structure orientation properties + +| Key | Default? | Type | Description | +|--------|----------|-----------|-------------| +| `flip` | | `boolean` | | +| `up` | | `string` | | diff --git a/data/scenarios/doc-fragments/base-robot.md b/data/scenarios/doc-fragments/base-robot.md new file mode 100644 index 000000000..b47ec7cff --- /dev/null +++ b/data/scenarios/doc-fragments/base-robot.md @@ -0,0 +1,12 @@ +#### Base robot + +There must be at most one **base** robot in the world. Since concrete robots can be created +either via the `loc` attribute or via the map and palette, use the following guide to +ensure the base robot is the one you intended: + +1. Always list the intended **base** as the first robot definition in your scenario. +2. The first robot with a `loc` attribute will become the base, even if other robots are defined earlier. +3. Without any located robots, if multiple robots are instantiated on the map from + the first robot definition, the first robot in + [row-major order](https://en.wikipedia.org/wiki/Row-_and_column-major_order) + shall be the base. diff --git a/data/scenarios/doc-fragments/capabilities.md b/data/scenarios/doc-fragments/capabilities.md new file mode 100644 index 000000000..9143e20d9 --- /dev/null +++ b/data/scenarios/doc-fragments/capabilities.md @@ -0,0 +1,8 @@ +#### Capabilities + +Each capability enables the evaluation of execution of one or more +commands or language constructs. Rather than listing all possible +capabilities here, which would be annoying to keep up-to-date, see the +(automatically generated) [Commands cheat +sheet](https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet) +on the Swarm wiki. \ No newline at end of file diff --git a/data/scenarios/doc-fragments/cells.md b/data/scenarios/doc-fragments/cells.md new file mode 100644 index 000000000..5b1ddaedb --- /dev/null +++ b/data/scenarios/doc-fragments/cells.md @@ -0,0 +1,23 @@ +#### Cells + +Each cell of the world is specified by a list of terrain, optional entity +and robots present (if any). For example, `[grass]`, `[grass, tree]`, +or `[grass, null, base]`. + +- The first (required) item specifies the terrain. Currently, valid + terrain values are `stone`, `dirt`, `grass`, `ice`, or `blank`. +- The second item (if present) specifies the name of an entity which + should be present in the cell. This may be a built-in entity, or a + custom entity specified in the `entities` section. `null` may be + used to explicitly specify no entity in the cell. +- The third item and later (if present) specifies the names of the robots + which should be present in the cell. These must be names of robots + specified in the `robots` section. A copy of each robot will be + created at each location in the `map` where it is drawn. + + Although multiple robots may be in a single location in general, + there is currently no way to specify more than one robot for a + cell in the world description. + +If a 1-tuple is used, it specifies a terrain value with no entity or +robot. A 2-tuple specifies a terrain value and entity, but no robot. diff --git a/data/scenarios/doc-fragments/entity-properties.md b/data/scenarios/doc-fragments/entity-properties.md new file mode 100644 index 000000000..21bcf4ac7 --- /dev/null +++ b/data/scenarios/doc-fragments/entity-properties.md @@ -0,0 +1,19 @@ +#### Entity properties + +The properties an entity may possess are listed below. Each entity +may possess any number of properties. + +- `unwalkable`: robots cannot `move` into a cell containing this + entity. If they try, the `move` command will throw an exception. + +- `portable`: robots can pick this up using `grab` or `harvest`. + Trying to execute `grab` or `harvest` on an entity that is not + `portable` will throw an exception. + +- `growable`: when `harvest`ed, the entity will regrow from a seed. + +- `infinite`: when `grab`bed or `harvest`ed, the entity will + immediately respawn. + +- `known`: robots know what this is without having to `scan` it first, + hence it does not show up as a question mark. diff --git a/data/scenarios/doc-fragments/header.md b/data/scenarios/doc-fragments/header.md new file mode 100644 index 000000000..b31cacc8e --- /dev/null +++ b/data/scenarios/doc-fragments/header.md @@ -0,0 +1,15 @@ +## Swarm YAML schema + +### YAML conventions + +Objects (key-value mappings) are described below using tables. Note +that a blank "**Default?**" column means the key is required; other keys +are optional and take on the indicated default value when they are not +present. The order of keys in a key-value mapping does not matter. + +YAML is untyped, but we try to give a more precise idea of the +expected types in the tables below. +- `foo list` means a list where all the elements are of type `foo`. +- Some values are tuples. The types and meaning of such tuple element + are presented in tables with an "**Index**" column. + diff --git a/data/schema/attribute.json b/data/schema/attribute.json index 16adc5037..f13906b78 100644 --- a/data/schema/attribute.json +++ b/data/schema/attribute.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/attribute.json", - "title": "Scenario-local attributes", - "description": "Local attribute definitions", + "title": "Attributes", + "description": "Scenario-local attribute definitions", "type": "object", "additionalProperties": false, "properties": { diff --git a/data/schema/combustion.json b/data/schema/combustion.json index c02f6edcf..e3d428cd3 100644 --- a/data/schema/combustion.json +++ b/data/schema/combustion.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/combustion.json", - "title": "Swarm entity combustion", - "description": "Properties of combustion", + "title": "Combustion", + "description": "Properties of entity combustion", "type": "object", "additionalProperties": false, "properties": { @@ -13,16 +13,8 @@ }, "duration": { "type": "array", - "items": [ - { - "name": "minimum", - "type": "number" - }, - { - "name": "maximum", - "type": "number" - } - ], + "default": [100, 200], + "$ref": "range.json", "description": "For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time that the combustion shall persist." }, "product": { diff --git a/data/schema/cosmic-loc.json b/data/schema/cosmic-loc.json index 00b30d789..e8ac785eb 100644 --- a/data/schema/cosmic-loc.json +++ b/data/schema/cosmic-loc.json @@ -10,6 +10,6 @@ "type": "string", "description": "Name of subworld" }, - "loc": {"$ref": "./planar-loc.json"} + "loc": {"$ref": "planar-loc.json"} } } diff --git a/data/schema/display.json b/data/schema/display.json index e25f6d4c5..305214855 100644 --- a/data/schema/display.json +++ b/data/schema/display.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/display.json", - "title": "Swarm entity display", - "description": "How to display an entity or robot in the Swarm game", + "title": "Display", + "description": "Swarm entity display. A display specifies how an entity or a robot (robots are essentially special kinds of entities) is displayed in the world. It consists of a key-value mapping described by the following table.", "type": "object", "additionalProperties": false, "properties": { @@ -13,8 +13,7 @@ }, "orientationMap": { "default": {}, - "type": "object", - "description": "Currently unused" + "$ref": "orientation-map.json" }, "curOrientation": { "default": null, @@ -47,12 +46,12 @@ "blue", "water" ], - "description": "The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found at https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/Attr.hs." + "description": "The name of the attribute that should be used to style the robot or entity. A list of currently valid attributes can be found [here](https://github.com/swarm-game/swarm/blob/main/src/Swarm/TUI/View/Attribute/Attr.hs)." }, "priority": { "default": 1, "type": "number", - "description": "When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority 1, and robots have priority 10." + "description": "When multiple entities and robots occupy the same cell, the one with the highest priority is drawn. By default, entities have priority `1`, and robots have priority `10`." }, "invisible": { "default": false, diff --git a/data/schema/entities.json b/data/schema/entities.json index fbeb8677f..86fe8daa1 100644 --- a/data/schema/entities.json +++ b/data/schema/entities.json @@ -1,110 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entities.json", - "title": "Swarm entities", + "title": "Entities", "description": "Description of entities in the Swarm game", "type": "array", "items": { - "description": "Description of an entity in the Swarm game", - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The name of the entity. This is what will show up in the inventory and how the entity can be referred to." - }, - "display": { - "type": "object", - "$ref": "./display.json", - "description": "Display information for the entity." - }, - "plural": { - "default": "null", - "type": "string", - "description": "An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name." - }, - "description": { - "type": "array", - "items": [ - { - "type": "string" - } - ], - "description": "A description of the entity, as a list of paragraphs." - }, - "orientation": { - "default": "null", - "type": "array", - "items": [ - { - "name": "X coordinate", - "type": "number" - }, - { - "name": "Y coordinate", - "type": "number" - } - ], - "description": "A 2-tuple of integers specifying an orientation vector for the entity. Currently unused." - }, - "growth": { - "default": "null", - "type": "array", - "items": [ - { - "name": "minimum", - "type": "number" - }, - { - "name": "maximum", - "type": "number" - } - ], - "description": "For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown." - }, - "combustion": { - "type": "object", - "$ref": "./combustion.json", - "description": "Properties of combustion." - }, - "yields": { - "default": "null", - "type": "string", - "description": "The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself." - }, - "properties": { - "default": "[]", - "type": "array", - "items": [ - { - "type": "string", - "examples": [ - "unwalkable", - "portable", - "infinite", - "known", - "growable" - ] - } - ], - "description": "A list of properties of this entity. See Entity properties." - }, - "capabilities": { - "default": "[]", - "type": "array", - "items": [ - { - "type": "string" - } - ], - "description": "A list of capabilities provided by entity, when it is equipped as a device. See Capabilities." - } - }, - "required": [ - "name", - "display", - "description" - ] + "$ref": "entity.json" } - } diff --git a/data/schema/entity-count.json b/data/schema/entity-count.json new file mode 100644 index 000000000..f04a13742 --- /dev/null +++ b/data/schema/entity-count.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entity-count.json", + "title": "Entity count", + "description": "One row in an inventory list", + "type": "array", + "items": [ + { + "name": "Quantity", + "type": "number" + }, + { + "name": "Entity name", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/data/schema/entity.json b/data/schema/entity.json new file mode 100644 index 000000000..2af23964c --- /dev/null +++ b/data/schema/entity.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/entity.json", + "title": "Entity", + "description": "Description of an entity in the Swarm game", + "footers": [ + "doc-fragments/entity-properties.md", + "doc-fragments/capabilities.md" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the entity. This is what will show up in the inventory and how the entity can be referred to." + }, + "display": { + "type": "object", + "$ref": "display.json", + "description": "Display information for the entity." + }, + "plural": { + "default": null, + "type": "string", + "description": "An explicit plural form of the name of the entity. If omitted, standard heuristics will be used for forming the English plural of its name." + }, + "description": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A description of the entity, as a list of paragraphs." + }, + "orientation": { + "default": null, + "type": "array", + "items": [ + { + "name": "X coordinate", + "type": "number" + }, + { + "name": "Y coordinate", + "type": "number" + } + ], + "description": "A 2-tuple of integers specifying an orientation vector for the entity. Currently unused." + }, + "growth": { + "default": null, + "type": "array", + "items": [ + { + "name": "minimum", + "type": "number" + }, + { + "name": "maximum", + "type": "number" + } + ], + "description": "For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown." + }, + "combustion": { + "type": "object", + "$ref": "combustion.json", + "description": "Properties of combustion." + }, + "yields": { + "default": null, + "type": "string", + "description": "The name of the entity which will be added to a robot's inventory when it executes grab or harvest on this entity. If omitted, the entity will simply yield itself." + }, + "properties": { + "default": [], + "type": "array", + "items": { + "type": "string", + "examples": [ + "unwalkable", + "portable", + "infinite", + "known", + "growable" + ] + }, + "description": "A list of properties of this entity." + }, + "capabilities": { + "default": [], + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of capabilities provided by entity, when it is equipped as a device. See [Capabilities](https://github.com/swarm-game/swarm/wiki/Capabilities-cheat-sheet)." + } + }, + "required": [ + "name", + "display", + "description" + ] +} diff --git a/data/schema/explicit-waypoint.json b/data/schema/explicit-waypoint.json index 5865d4304..a5238bacb 100644 --- a/data/schema/explicit-waypoint.json +++ b/data/schema/explicit-waypoint.json @@ -10,6 +10,6 @@ "description": "Waypoint name", "type": "string" }, - "loc": {"$ref": "./planar-loc.json"} + "loc": {"$ref": "planar-loc.json"} } } diff --git a/data/schema/inventory.json b/data/schema/inventory.json index 7095ba620..a95c16c7f 100644 --- a/data/schema/inventory.json +++ b/data/schema/inventory.json @@ -1,20 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/inventory.json", - "title": "Swarm entity inventory", - "description": "A list of [count, entity name] pairs, specifying the number of each entity.", + "title": "Inventory", "type": "array", "items": { - "type": "array", - "items": [ - { - "title": "Entity count", - "type": "number" - }, - { - "title": "Entity name", - "type": "string" - } - ] + "$ref": "entity-count.json" } } \ No newline at end of file diff --git a/data/schema/named-structure.json b/data/schema/named-structure.json new file mode 100644 index 000000000..2fbdafa66 --- /dev/null +++ b/data/schema/named-structure.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/named-structure.json", + "title": "Named structure", + "description": "Structure definitions", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of this substructure" + }, + "structure": { + "$ref": "structure.json" + } + } +} diff --git a/data/schema/objective.json b/data/schema/objective.json index ccc8c5dc5..482bdff7e 100644 --- a/data/schema/objective.json +++ b/data/schema/objective.json @@ -1,18 +1,16 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/objective.json", - "title": "Scenario goals and their prerequisites", - "description": "The top-level objectives field contains a list of objectives that must be completed in sequence. Each objective has a goal description and a condition.", + "title": "Objective", + "description": "Scenario goals and their prerequisites. The top-level objectives field contains a list of objectives that must be completed in sequence. Each objective has a goal description and a condition.", "type": "object", "additionalProperties": false, "properties": { "goal": { "type": "array", - "items": [ - { - "type": "string" - } - ], + "items": { + "type": "string" + }, "description": "The goal description as a list of paragraphs that the player can read." }, "condition": { @@ -35,6 +33,8 @@ "description": "A compact (2-3 word) summary of the goal", "type": "string" }, - "prerequisite": {} + "prerequisite": { + "$ref": "prerequisite.json" + } } } diff --git a/data/schema/orientation-map.json b/data/schema/orientation-map.json new file mode 100644 index 000000000..332c872f7 --- /dev/null +++ b/data/schema/orientation-map.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/orientation-map.json", + "title": "Orientation map", + "description": "Mapping from cardinal directions to display characters", + "type": "object", + "additionalProperties": false, + "properties": { + "east": { + "type": "string" + }, + "north": { + "type": "string" + }, + "west": { + "type": "string" + }, + "south": { + "type": "string" + } + } +} diff --git a/data/schema/placement.json b/data/schema/placement.json index 7b3cfe747..7897305e7 100644 --- a/data/schema/placement.json +++ b/data/schema/placement.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/placement.json", - "title": "Swarm structure placement", + "title": "Placement", "description": "Structure placement", "type": "object", "additionalProperties": false, @@ -11,20 +11,10 @@ "description": "Name of structure definition" }, "offset": { - "$ref": "./planar-loc.json" + "$ref": "planar-loc.json" }, "orient": { - "description": "Orientation of structure", - "type": "object", - "additionalProperties": false, - "properties": { - "up": { - "type": "string" - }, - "flip": { - "type": "boolean" - } - } + "$ref": "structure-orient.json" } } } diff --git a/data/schema/portal-exit.json b/data/schema/portal-exit.json new file mode 100644 index 000000000..ebd26bb17 --- /dev/null +++ b/data/schema/portal-exit.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/portal-exit.json", + "title": "Portal exit", + "description": "Properties of a portal's exit", + "type": "object", + "additionalProperties": false, + "properties": { + "exit": { + "type": "string", + "description": "Name of exit waypoint" + }, + "subworldName": { + "type": "string", + "description": "Name of exit subworld" + } + } +} diff --git a/data/schema/portal.json b/data/schema/portal.json index e8c402ff9..18faf8ebd 100644 --- a/data/schema/portal.json +++ b/data/schema/portal.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/portal.json", - "title": "Portals", + "title": "Portal", "description": "Portal definition", "type": "object", "additionalProperties": false, @@ -19,19 +19,7 @@ "type": "boolean" }, "exitInfo": { - "description": "Exit definition", - "type": "object", - "additionalProperties": false, - "properties": { - "exit": { - "type": "string", - "description": "Name of exit waypoint" - }, - "subworldName": { - "type": "string", - "description": "Name of exit subworld" - } - } + "$ref": "portal-exit.json" } } } diff --git a/data/schema/prerequisite.json b/data/schema/prerequisite.json new file mode 100644 index 000000000..8ec162e22 --- /dev/null +++ b/data/schema/prerequisite.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/objective.json", + "title": "Prerequisite", + "description": "Prerequisite conditions for an objective.", + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "properties": { + "not": { + "description": "An inverted boolean", + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "previewable": { + "description": "Whether the goal appears in the dialog before it is 'active'", + "type": "boolean" + }, + "logic": { + "description": "Boolean logic tree", + "type": "object" + } + } + } + ] +} diff --git a/data/schema/range.json b/data/schema/range.json new file mode 100644 index 000000000..cfdab8fde --- /dev/null +++ b/data/schema/range.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/range.json", + "title": "Numeric range", + "description": "Min/max range of a value", + "type": "array", + "items": [ + { + "name": "minimum", + "type": "number" + }, + { + "name": "maximum", + "type": "number" + } + ] +} diff --git a/data/schema/recipe.json b/data/schema/recipe.json new file mode 100644 index 000000000..7d206d4a8 --- /dev/null +++ b/data/schema/recipe.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/recipe.json", + "title": "Recipe", + "description": "Recipe describes a process that takes some inputs and produces some outputs, which robots can access using `make` and `drill`.", + "type": "object", + "examples": [ + { + "in": [ + [ + 2, + "copper wire" + ] + ], + "out": [ + [ + 1, + "strange loop" + ] + ] + } + ], + "additionalProperties": false, + "properties": { + "in": { + "$ref": "inventory.json", + "description": "A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed." + }, + "out": { + "$ref": "inventory.json", + "description": "A list of outputs produced by the recipe. It is a list of `[count, entity name]` tuples just like `in`." + }, + "required": { + "default": [], + "$ref": "inventory.json", + "description": "A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of [count, entity name] tuples just like in and out." + }, + "time": { + "default": 1, + "type": "number", + "description": "The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will wait for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes." + }, + "weight": { + "default": 1, + "type": "number", + "description": "Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a widget, one with weight `1` and the other with weight `9`. When a robot executes `make \"widget\"`, the first recipe will be chosen 10% of the time, and the second recipe 90% of the time." + } + } +} \ No newline at end of file diff --git a/data/schema/recipes.json b/data/schema/recipes.json index 31d546b24..be6e663f8 100644 --- a/data/schema/recipes.json +++ b/data/schema/recipes.json @@ -1,54 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/recipes.json", - "title": "Swarm recipes", + "title": "Recipes", "description": "How to make (or drill) entities in the Swarm game", "type": "array", "items": { - "title": "Swarm recipe", - "description": "Recipe describes a process that takes some inputs and produces some outputs, which robots can access using make and drill.", - "type": "object", - "examples": [ - { - "in": [ - [ - 2, - "copper wire" - ] - ], - "out": [ - [ - 1, - "strange loop" - ] - ] - } - ], - "additionalProperties": false, - "properties": { - "in": { - "$ref": "./inventory.json", - "description": "A list of ingredients consumed by the recipe. Each ingredient is a tuple consisting of an integer and an entity name, indicating the number of copies of the given entity that are needed." - }, - "out": { - "$ref": "./inventory.json", - "description": "A list of outputs produced by the recipe. It is a list of [count, entity name] tuples just like in." - }, - "required": { - "default": [], - "$ref": "./inventory.json", - "description": "A list of catalysts required by the recipe. They are neither consumed nor produced, but must be present in order for the recipe to be carried out. It is a list of [count, entity name] tuples just like in and out." - }, - "time": { - "default": 1, - "type": "number", - "description": "The number of ticks the recipe takes to perform. For recipes which take more than 1 tick, the robot will wait for a number of ticks until the recipe is complete. For example, this is used for many drilling recipes." - }, - "weight": { - "default": 1, - "type": "number", - "description": "Whenever there are multiple recipes that match the relevant criteria, one of them will be chosen at random, with probability proportional to their weights. For example, suppose there are two recipes that both output a widget, one with weight 1 and the other with weight 9. When a robot executes make \"widget\", the first recipe will be chosen 10% of the time, and the second recipe 90% of the time." - } - } + "$ref": "recipe.json" } } \ No newline at end of file diff --git a/data/schema/robot.json b/data/schema/robot.json index b33a953a8..a35cd143e 100644 --- a/data/schema/robot.json +++ b/data/schema/robot.json @@ -1,24 +1,27 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/robot.json", - "title": "Swarm robot", + "title": "Robot", "description": "Description of a robot in the Swarm game", + "footers": [ + "doc-fragments/base-robot.md" + ], "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", - "description": "The name of the robot. This shows up in the list of robots in the game (F2), and is also how the robot will be referred to in the world palette." + "description": "The name of the robot. This shows up in the list of robots in the game (`F2`), and is also how the robot will be referred to in the world palette." }, "description": { "type": "string", "description": "A description of the robot. This is currently not used for much, other than scenario documentation." }, "loc": { - "description": "An optional starting location for the robot. If the loc field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map.", + "description": "An optional starting location for the robot. If the `loc` field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map.", "oneOf": [ - {"$ref": "./cosmic-loc.json"}, - {"$ref": "./planar-loc.json"} + {"$ref": "cosmic-loc.json"}, + {"$ref": "planar-loc.json"} ] }, "dir": { @@ -38,7 +41,7 @@ }, "display": { "default": "default", - "$ref": "./display.json", + "$ref": "display.json", "description": "Display information for the robot. If this field is omitted, the default robot display will be used." }, "program": { @@ -56,8 +59,8 @@ }, "inventory": { "default": [], - "$ref": "./inventory.json", - "description": "A list of [count, entity name] pairs, specifying the entities in the robot's starting inventory, and the number of each." + "$ref": "inventory.json", + "description": "A list of `[count, entity name]` pairs, specifying the entities in the robot's starting inventory, and the number of each." }, "system": { "default": false, @@ -67,7 +70,7 @@ "heavy": { "default": false, "type": "boolean", - "description": "Whether the robot is heavy. Heavy robots require tank treads to move (rather than just treads for other robots)." + "description": "Whether the robot is heavy. Heavy robots require `tank treads` to `move` (rather than just `treads` for other robots)." }, "unwalkable": { "default": [], diff --git a/data/schema/scenario.json b/data/schema/scenario.json index 56889e34b..07f881f58 100644 --- a/data/schema/scenario.json +++ b/data/schema/scenario.json @@ -1,13 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/scenario.json", - "title": "Swarm scenario", - "description": "Scenario for the swarm game", + "title": "Top level", + "description": "At the top level, a scenario file contains a key-value mapping described by the following table.", "type": "object", "additionalProperties": false, "properties": { "version": { - "description": "The version number of the scenario schema. Currently, this should always be 1.", + "description": "The version number of the scenario schema. Currently, this should always be `1`.", "type": "number" }, "name": { @@ -33,66 +33,68 @@ "type": "number" }, "entities": { - "description": "An optional list of custom entities, to be used in addition to the built-in entities. See description of Entities.", + "description": "An optional list of custom entities, to be used in addition to the built-in entities.", "default": [], - "$ref": "./entities.json" + "items": { + "$ref": "entity.json" + } }, "recipes": { - "description": "An optional list of custom recipes, to be used in addition to the built-in recipes. They can refer to built-in entities as well as custom entities. See description of Recipes.", + "description": "An optional list of custom recipes, to be used in addition to the built-in recipes. They can refer to built-in entities as well as custom entities.", "default": [], - "$ref": "./recipes.json" + "items": { + "$ref": "recipe.json" + } }, "known": { "description": "A list of names of standard or custom entities which should have the Known property added to them; that is, robots should know what they are without having to scan them.", "default": [], "type": "array", - "items": [ - { - "type": "string" - } - ] + "items": { + "type": "string" + } }, "world": { - "$ref": "./world.json" + "$ref": "world.json" }, "attrs": { "description": "A list of local attribute definitions", "type": "array", "items": { - "$ref": "./attribute.json" + "$ref": "attribute.json" } }, "subworlds": { "description": "A list of subworld definitions", "type": "array", "items": { - "$ref": "./world.json" + "$ref": "world.json" } }, "structures": { "description": "Structure definitions", "type": "array", "items": { - "$ref": "./structure.json" + "$ref": "named-structure.json" } }, "robots": { - "description": "A list of robots that will inhabit the world. See the description of Robots.", + "description": "A list of robots that will inhabit the world.", "type": "array", "items": { - "$ref": "./robot.json" + "$ref": "robot.json" } }, "objectives": { - "description": "An optional list of objectives, aka winning conditions. The player has to complete the objectives in sequence to win. See the description of Objectives.", + "description": "An optional list of objectives, aka winning conditions. The player has to complete the objectives in sequence to win.", "default": [], "type": "array", "items": { - "$ref": "./objective.json" + "$ref": "objective.json" } }, "solution": { - "description": "The (optional) text of a Swarm program that, when run on the base robot, completes all the objectives. For scenarios which are officially part of the Swarm repository, such a solution will be tested as part of CI testing. For scenarios loaded directly from a file, any provided solution is simply ignored.", + "description": "The (optional) text of a Swarm program that, when run on the base robot, completes all the objectives. For scenarios which are officially part of the Swarm repository, such a solution will be tested as part of CI testing.", "default": null, "type": "string" }, diff --git a/data/schema/structure-orient.json b/data/schema/structure-orient.json new file mode 100644 index 000000000..d97b4ba1d --- /dev/null +++ b/data/schema/structure-orient.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/structure-orient.json", + "title": "Structure orientation", + "description": "Structure orientation properties", + "type": "object", + "additionalProperties": false, + "properties": { + "up": { + "type": "string" + }, + "flip": { + "type": "boolean" + } + } +} diff --git a/data/schema/structure.json b/data/schema/structure.json index 429c99e3e..a2fdbcf81 100644 --- a/data/schema/structure.json +++ b/data/schema/structure.json @@ -1,53 +1,42 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/structure.json", - "title": "Structures", - "description": "Structure definitions", + "title": "Structure", + "description": "Structure properties", "type": "object", "additionalProperties": false, "properties": { - "name": { + "map": { "type": "string", - "description": "Name of this substructure" + "description": "Cell-based representation of the structure using palette entries" }, - "structure": { + "mask": { + "type": "string", + "description": "A special palette character that indicates that map cell should be transparent" + }, + "palette": { "description": "Structure properties", - "type": "object", - "additionalProperties": false, - "properties": { - "map": { - "type": "string", - "description": "Cell-based representation of the structure using palette entries" - }, - "mask": { - "type": "string", - "description": "A speceial palette character that indicates that map cell should be transparent" - }, - "palette": { - "description": "Structure properties", - "type": "object" - }, - "waypoints": { - "description": "Single-location waypoint definitions", - "type": "array", - "items": { - "$ref": "./explicit-waypoint.json" - } - }, - "placements": { - "description": "Structure placements", - "type": "array", - "items": { - "$ref": "./placement.json" - } - }, - "structures": { - "description": "Nested structure definitions", - "type": "array", - "items": { - "$ref": "#" - } - } + "type": "object" + }, + "waypoints": { + "description": "Single-location waypoint definitions", + "type": "array", + "items": { + "$ref": "explicit-waypoint.json" + } + }, + "placements": { + "description": "Structure placements", + "type": "array", + "items": { + "$ref": "placement.json" + } + }, + "structures": { + "description": "Nested structure definitions", + "type": "array", + "items": { + "$ref": "named-structure.json" } } } diff --git a/data/schema/world.json b/data/schema/world.json index 60ae1e9b7..3144d5ce9 100644 --- a/data/schema/world.json +++ b/data/schema/world.json @@ -1,8 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/world.json", - "title": "Swarm world", + "title": "World", "description": "Description of the world in the Swarm game", + "footers": [ + "doc-fragments/cells.md" + ], "type": "object", "additionalProperties": false, "properties": { @@ -21,21 +24,21 @@ "description": "Structure definitions", "type": "array", "items": { - "$ref": "./structure.json" + "$ref": "named-structure.json" } }, "placements": { "description": "Structure placements", "type": "array", "items": { - "$ref": "./placement.json" + "$ref": "placement.json" } }, "waypoints": { "description": "Single-location waypoint definitions", "type": "array", "items": { - "$ref": "./explicit-waypoint.json" + "$ref": "explicit-waypoint.json" } }, "dsl": { @@ -46,7 +49,7 @@ "offset": { "default": false, "type": "boolean", - "description": "Whether the base robot's position should be moved to the nearest \"good\" location, currently defined as a location near a tree, in a 16x16 patch which contains at least one each of tree, copper ore, bit (0), bit (1), rock, lambda, water, and sand. The classic scenario uses offset: True to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game. See https://github.com/swarm-game/swarm/blob/main/src/Swarm/Game/WorldGen.hs#L204 ." + "description": "Whether the base robot's position should be moved to the nearest \"good\" location, currently defined as a location near a `tree`, in a 16x16 patch which contains at least one each of `tree`, `copper ore`, `bit (0)`, `bit (1)`, `rock`, `lambda`, `water`, and `sand`. The classic scenario uses `offset: True` to make sure that the it is not unreasonably difficult to obtain necessary resources in the early game (see [code](https://github.com/swarm-game/swarm/blob/e06e04f622a3762a10e7c942c1cbd2c1e396144f/src/Swarm/Game/World/Gen.hs#L79))." }, "scrollable": { "default": true, @@ -54,16 +57,15 @@ "description": "Whether players are allowed to scroll the world map." }, "palette": { - "default": {}, "type": "object", "examples": [{"T": ["grass", "tree"]}], - "description": "The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See Cells for the contents of the tuples representing a cell." + "description": "The palette maps single character keys to tuples representing contents of cells in the world, so that a world containing entities and robots can be drawn graphically. See [Cells](#cells) for the contents of the tuples representing a cell." }, "portals": { "description": "A list of portal definitions that reference waypoints.", "type": "array", "items": { - "$ref": "./portal.json" + "$ref": "portal.json" } }, "map": { @@ -87,7 +89,7 @@ "type": "number" } ], - "description": "A 2-tuple of int values specifying the (x,y) coordinates of the upper left corner of the map." + "description": "A 2-tuple of `int` values specifying the `(x,y)` coordinates of the upper left corner of the map." } } } diff --git a/scripts/regenerate-schema-docs.sh b/scripts/regenerate-schema-docs.sh new file mode 100755 index 000000000..0bc3b9ffd --- /dev/null +++ b/scripts/regenerate-schema-docs.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cd $SCRIPT_DIR/.. + +stack build --fast && stack exec -- swarm generate cheatsheet --scenario \ No newline at end of file diff --git a/src/Swarm/Doc/Gen.hs b/src/Swarm/Doc/Gen.hs index 86cab0386..d232db89d 100644 --- a/src/Swarm/Doc/Gen.hs +++ b/src/Swarm/Doc/Gen.hs @@ -42,6 +42,8 @@ import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Tuple (swap) import Swarm.Doc.Pedagogy +import Swarm.Doc.Schema.Render +import Swarm.Doc.Util import Swarm.Game.Display (displayChar) import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E @@ -95,7 +97,7 @@ data EditorType = Emacs | VSCode | Vim deriving (Eq, Show, Enum, Bounded) -- | An enumeration of the kinds of cheat sheets we can produce. -data SheetType = Entities | Commands | Capabilities | Recipes +data SheetType = Entities | Commands | Capabilities | Recipes | Scenario deriving (Eq, Show, Enum, Bounded) -- | A configuration record holding the URLs of the various cheat @@ -138,6 +140,7 @@ generateDocs = \case entities <- loadEntities recipes <- loadRecipes entities sendIO $ T.putStrLn $ recipePage address recipes + Scenario -> genScenarioSchemaDocs TutorialCoverage -> renderTutorialProgression >>= putStrLn . T.unpack WebAPIEndpoints -> putStrLn swarmApiMarkdown @@ -223,12 +226,6 @@ generateSpecialKeyNames = -- GENERATE TABLES: COMMANDS, ENTITIES AND CAPABILITIES TO MARKDOWN TABLE -- ---------------------------------------------------------------------------- -wrap :: Char -> Text -> Text -wrap c = T.cons c . flip T.snoc c - -codeQuote :: Text -> Text -codeQuote = wrap '`' - escapeTable :: Text -> Text escapeTable = T.concatMap (\c -> if c == '|' then T.snoc "\\" c else T.singleton c) @@ -243,12 +240,6 @@ listToRow mw xs = wrap '|' . T.intercalate "|" $ zipWith format mw xs maxWidths :: [[Text]] -> [Int] maxWidths = map (maximum . map T.length) . transpose -addLink :: Text -> Text -> Text -addLink l t = T.concat ["[", t, "](", l, ")"] - -tshow :: (Show a) => a -> Text -tshow = T.pack . show - -- --------- -- COMMANDS -- --------- diff --git a/src/Swarm/Doc/Schema/Arrangement.hs b/src/Swarm/Doc/Schema/Arrangement.hs new file mode 100644 index 000000000..8e1697efa --- /dev/null +++ b/src/Swarm/Doc/Schema/Arrangement.hs @@ -0,0 +1,42 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Graph-based heuristics for arranging the +-- order of sections in the schema docs +module Swarm.Doc.Schema.Arrangement (sortAndPruneSchemas) where + +import Data.Graph +import Data.Set qualified as Set +import Swarm.Doc.Schema.Parse +import Swarm.Doc.Schema.Refined +import Swarm.Doc.Schema.SchemaType + +-- | Sort the schemas in topological order. +-- +-- Only includes schema files that are reachable from +-- the root schema +-- (i.e. exclude @entities.json@ and @recipes.json@, +-- which are used independently to validate @entities.yaml@ +-- and @recipes.yaml@). +sortAndPruneSchemas :: + SchemaIdReference -> + [SchemaData] -> + [SchemaData] +sortAndPruneSchemas rootSchemaKey schemas = + reverse . flattenSCCs . stronglyConnComp $ reachableEdges + where + rawEdgeList = map getNodeEdgesEntry schemas + (graph, _nodeFromVertex, vertexFromKey) = graphFromEdges rawEdgeList + reachableVertices = Set.fromList $ maybe [] (reachable graph) $ vertexFromKey rootSchemaKey + + reachableEdges = filter f rawEdgeList + f (_, k, _) = maybe False (`Set.member` reachableVertices) . vertexFromKey $ k + +getNodeEdgesEntry :: + SchemaData -> + (SchemaData, SchemaIdReference, [SchemaIdReference]) +getNodeEdgesEntry sd@(SchemaData fp schem _) = + ( sd + , fromFilePath fp + , Set.toList $ extractReferences $ content schem + ) diff --git a/src/Swarm/Doc/Schema/Parse.hs b/src/Swarm/Doc/Schema/Parse.hs new file mode 100644 index 000000000..8f465c739 --- /dev/null +++ b/src/Swarm/Doc/Schema/Parse.hs @@ -0,0 +1,52 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- There are no modern, comprehensive JSON Schema parsing +-- libraries in Haskell, as explained in +-- . +-- +-- Therefore, a bespoke parser for a small subset of JSON Schema is implemented here, +-- simply for rendering Markdown documentation from Swarm's schema. +module Swarm.Doc.Schema.Parse where + +import Control.Applicative ((<|>)) +import Data.Aeson +import Data.Map (Map) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import Swarm.Doc.Schema.Refined +import Text.Pandoc + +-- | Includes everything needed to +-- render the schema to markdown +data SchemaData = SchemaData + { schemaPath :: FilePath + , schemaContent :: ToplevelSchema + , markdownFooters :: [Pandoc] + } + +data Members + = ObjectProperties (Map Text SwarmSchema) + | ListMembers (ItemDescription SwarmSchema) + deriving (Eq, Ord, Show) + +data ToplevelSchema = ToplevelSchema + { title :: Text + , description :: Maybe Pandoc + , content :: SwarmSchema + , members :: Maybe Members + , footerPaths :: [FilePath] + } + deriving (Eq, Ord, Show) + +instance FromJSON ToplevelSchema where + parseJSON x = do + rawSchema :: rawSchema <- parseJSON x + swarmSchema <- toSwarmSchema rawSchema + + theTitle <- maybe (fail "Schema requires a title") return $ _title rawSchema + let theFooters = fromMaybe [] $ _footers rawSchema + maybeMembers = + ObjectProperties <$> properties swarmSchema + <|> ListMembers <$> itemsDescription swarmSchema + return $ ToplevelSchema theTitle (objectDescription swarmSchema) swarmSchema maybeMembers theFooters diff --git a/src/Swarm/Doc/Schema/Refined.hs b/src/Swarm/Doc/Schema/Refined.hs new file mode 100644 index 000000000..78dcb4b49 --- /dev/null +++ b/src/Swarm/Doc/Schema/Refined.hs @@ -0,0 +1,145 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Refined JSON schema after converting +-- all JSON Value types to their specific sum types +module Swarm.Doc.Schema.Refined where + +import Control.Applicative ((<|>)) +import Data.Aeson +import Data.List.Extra (replace) +import Data.Map (Map) +import Data.Map qualified as M +import Data.Maybe (fromMaybe, mapMaybe) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text qualified as T +import GHC.Generics (Generic) +import Swarm.Doc.Schema.SchemaType +import System.FilePath (takeBaseName) +import Text.Pandoc +import Text.Pandoc.Builder + +-- * Basic + +schemaJsonOptions :: Options +schemaJsonOptions = + defaultOptions + { fieldLabelModifier = replace "S" "$" . drop 1 -- drops leading underscore + } + +-- | A single record that encompasses all possible objects +-- in a JSON schema. All fields are optional. +data SchemaRaw = SchemaRaw + { _description :: Maybe Text + , _default :: Maybe Value + , _title :: Maybe Text + , _type :: Maybe (SingleOrList Text) + , _name :: Maybe Text + , _properties :: Maybe (Map Text SwarmSchema) + , _items :: Maybe (ItemDescription SwarmSchema) + , _examples :: Maybe [Value] + , _Sref :: Maybe Text + , _oneOf :: Maybe [SchemaRaw] + , _footers :: Maybe [FilePath] + , _additionalProperties :: Maybe Bool + } + deriving (Eq, Ord, Show, Generic) + +instance FromJSON SchemaRaw where + parseJSON = genericParseJSON schemaJsonOptions + +extractSchemaType :: SchemaRaw -> Maybe SchemaType +extractSchemaType rawSchema = + mkReference <$> _Sref rawSchema + <|> getTypeFromItems + <|> Simple <$> _type rawSchema + <|> Alternatives . mapMaybe extractSchemaType <$> _oneOf rawSchema + where + mkReference = Reference . SchemaIdReference . T.pack . takeBaseName . T.unpack + + getTypeFromItems :: Maybe SchemaType + getTypeFromItems = do + itemsThing <- _items rawSchema + case itemsThing of + ItemList _ -> Nothing + ItemType x -> Just $ ListOf $ schemaType x + +-- * Refined + +data ItemDescription a + = ItemList [a] + | ItemType a + deriving (Eq, Ord, Show) + +instance (FromJSON a) => FromJSON (ItemDescription a) where + parseJSON x = + ItemList <$> parseJSON x + <|> ItemType <$> parseJSON x + +getSchemaReferences :: SchemaType -> [SchemaIdReference] +getSchemaReferences = \case + Simple _ -> [] + Alternatives xs -> concatMap getSchemaReferences xs + Reference x -> pure x + ListOf x -> getSchemaReferences x + +-- | A subset of all JSON schemas, conforming to internal Swarm conventions. +-- +-- Conveniently, this extra representation layer +-- is able to enforce (via 'toSwarmSchema') that all "object" +-- definitions in the schema contain the @"additionalProperties": true@ attribute. +data SwarmSchema = SwarmSchema + { schemaType :: SchemaType + , defaultValue :: Maybe Value + , objectDescription :: Maybe Pandoc + , properties :: Maybe (Map Text SwarmSchema) + , itemsDescription :: Maybe (ItemDescription SwarmSchema) + , examples :: [Value] + } + deriving (Eq, Ord, Show) + +instance FromJSON SwarmSchema where + parseJSON x = do + rawSchema :: rawSchema <- parseJSON x + toSwarmSchema rawSchema + +getMarkdown :: MonadFail m => Text -> m Pandoc +getMarkdown desc = case runPure (readMarkdown def desc) of + Right d -> return d + Left err -> fail $ T.unpack $ renderError err + +toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema +toSwarmSchema rawSchema = do + theType <- maybe (fail "Unspecified sub-schema type") return maybeType + markdownDescription <- mapM getMarkdown $ _description rawSchema + + if null (_properties rawSchema) || not (fromMaybe True (_additionalProperties rawSchema)) + then return () + else fail "All objects must specify '\"additionalProperties\": true'" + + return + SwarmSchema + { schemaType = theType + , defaultValue = _default rawSchema + , objectDescription = markdownDescription <|> doc . plain . text <$> _name rawSchema + , examples = fromMaybe [] $ _examples rawSchema + , properties = _properties rawSchema + , itemsDescription = _items rawSchema + } + where + maybeType = extractSchemaType rawSchema + +-- * Utilities + +-- | Recursively extract references to other schemas +extractReferences :: SwarmSchema -> Set SchemaIdReference +extractReferences s = thisRefList <> otherRefLists + where + thisRefList = Set.fromList . getSchemaReferences $ schemaType s + + otherSchemas = maybe [] M.elems $ properties s + otherRefLists = Set.unions $ map extractReferences otherSchemas diff --git a/src/Swarm/Doc/Schema/Render.hs b/src/Swarm/Doc/Schema/Render.hs new file mode 100644 index 000000000..57d60475b --- /dev/null +++ b/src/Swarm/Doc/Schema/Render.hs @@ -0,0 +1,194 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Render a markdown document fragment +-- from the Scenario JSON schema files. +module Swarm.Doc.Schema.Render where + +import Control.Arrow (left, (&&&)) +import Control.Monad.Except (runExceptT) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Except (except) +import Data.Aeson +import Data.List (intersperse) +import Data.Map (Map) +import Data.Map.Strict qualified as M +import Data.Maybe (fromMaybe) +import Data.Scientific (FPFormat (..), Scientific, formatScientific) +import Data.Text qualified as T +import Data.Text.IO qualified as TIO +import Data.Vector qualified as V +import Swarm.Doc.Schema.Arrangement +import Swarm.Doc.Schema.Parse +import Swarm.Doc.Schema.Refined +import Swarm.Doc.Schema.SchemaType +import Swarm.Doc.Util +import Swarm.Util (applyWhen, brackets, quote, showT) +import System.Directory (listDirectory) +import System.FilePath (splitExtension, (<.>), ()) +import Text.Pandoc +import Text.Pandoc.Builder +import Text.Pandoc.Walk (query) + +scenariosDir :: FilePath +scenariosDir = "data/scenarios" + +docFragmentsDir :: FilePath +docFragmentsDir = scenariosDir "doc-fragments" + +schemasDir :: FilePath +schemasDir = "data/schema" + +schemaExtension :: String +schemaExtension = ".json" + +propertyColumnHeadings :: [T.Text] +propertyColumnHeadings = + [ "Key" + , "Default?" + , "Type" + , "Description" + ] + +listColumnHeadings :: [T.Text] +listColumnHeadings = + [ "Index" + , "Type" + , "Description" + ] + +makeTitleMap :: [SchemaData] -> Map SchemaIdReference T.Text +makeTitleMap = M.fromList . map (fromFilePath . schemaPath &&& title . schemaContent) + +makePandocTable :: Map SchemaIdReference T.Text -> SchemaData -> Pandoc +makePandocTable titleMap (SchemaData _ (ToplevelSchema theTitle theDescription _schema theMembers _) parsedFooters) = + setTitle (text "JSON Schema for Scenarios") $ + doc (header 3 (text theTitle)) + <> fromMaybe mempty theDescription + <> maybe mempty mkTable theMembers + <> mconcat parsedFooters + where + renderItems someStuff = case someStuff of + ItemType x -> plain $ text "List of " <> listToText titleMap (schemaType x) + ItemList xs -> + makePropsTable False listColumnHeadings titleMap + . M.fromList + $ zip (map tshow [0 :: Int ..]) xs + + mkTable x = doc $ case x of + ObjectProperties props -> makePropsTable True propertyColumnHeadings titleMap props + ListMembers someStuff -> renderItems someStuff + +genPropsRow :: Bool -> Map SchemaIdReference T.Text -> (T.Text, SwarmSchema) -> [Blocks] +genPropsRow includeDefaultColumn titleMap (k, x) = + firstColumn : applyWhen includeDefaultColumn (defaultColumn :) tailColumns + where + firstColumn = plain $ code k + defaultColumn = maybe mempty (plain . code . renderValue) $ defaultValue x + tailColumns = + [ plain . listToText titleMap $ schemaType x + , fromList $ maybe [] (query id) $ objectDescription x + ] + +makePropsTable :: + Bool -> + [T.Text] -> + Map SchemaIdReference T.Text -> + Map T.Text SwarmSchema -> + Blocks +makePropsTable includeDefaultColumn headingsList titleMap = + simpleTable headerRow . map (genPropsRow includeDefaultColumn titleMap) . M.toList + where + headerRow = map (plain . text) headingsList + +type FileStemAndExtension = (FilePath, String) + +recombineExtension :: FileStemAndExtension -> FilePath +recombineExtension (filenameStem, fileExtension) = + filenameStem <.> fileExtension + +genMarkdown :: [SchemaData] -> Either T.Text T.Text +genMarkdown schemaThings = + left renderError $ + runPure $ + writeMarkdown (def {writerExtensions = extensionsFromList [Ext_pipe_tables]}) pd + where + titleMap = makeTitleMap schemaThings + pd = + mconcat $ + map (makePandocTable titleMap) $ + sortAndPruneSchemas (fromFilePath "scenario") schemaThings + +parseSchemaFile :: FileStemAndExtension -> IO (Either T.Text ToplevelSchema) +parseSchemaFile stemAndExtension = + left (prependPath . T.pack) <$> eitherDecodeFileStrict fullPath + where + prependPath = ((T.unwords ["in", quote (T.pack filename)] <> ": ") <>) + filename = recombineExtension stemAndExtension + fullPath = schemasDir filename + +loadFooterContent :: (FilePath, ToplevelSchema) -> IO SchemaData +loadFooterContent (fp, schem) = do + xs <- mapM (TIO.readFile . (scenariosDir )) $ footerPaths schem + parsedFooters <- mapM getMarkdown xs + return $ + SchemaData + fp + schem + parsedFooters + +genScenarioSchemaDocs :: IO () +genScenarioSchemaDocs = do + dirContents <- listDirectory schemasDir + let inputFiles = filter ((== schemaExtension) . snd) $ map splitExtension dirContents + xs <- mapM (sequenceA . (recombineExtension &&& parseSchemaFile)) inputFiles + + result <- runExceptT $ do + schemaTuples <- except $ traverse sequenceA xs + things <- liftIO $ mapM loadFooterContent schemaTuples + myMarkdown <- except $ genMarkdown things + docHeader <- liftIO $ TIO.readFile "data/scenarios/doc-fragments/header.md" + liftIO . writeFile (docFragmentsDir "SCHEMA.md") . T.unpack $ docHeader <> myMarkdown + + case result of + Left e -> print $ unwords ["Failed:", T.unpack e] + Right _ -> return () + +renderValue :: Value -> T.Text +renderValue = \case + Object obj -> showT obj + Array arr -> brackets . T.intercalate ", " . map renderValue $ V.toList arr + String t -> quote t + Number num -> T.pack $ formatNumberCompact num + Bool b -> showT b + Null -> "null" + +fragmentHref :: Map SchemaIdReference T.Text -> SchemaIdReference -> T.Text +fragmentHref titleMap r@(SchemaIdReference ref) = + T.cons '#' . T.toLower . T.replace " " "-" $ x + where + x = M.findWithDefault ref r titleMap + +listToText :: Map SchemaIdReference T.Text -> SchemaType -> Inlines +listToText titleMap = \case + Simple xs -> renderAlternatives $ map code $ getList xs + Alternatives xs -> renderAlternatives $ map (listToText titleMap) xs + Reference r@(SchemaIdReference x) -> schemaLink r x + ListOf x -> listToText titleMap x <> text " list" + where + renderAlternatives = mconcat . intersperse (text " or ") + schemaLink r = link (fragmentHref titleMap r) "Link to object properties" . text + +-- | +-- Strips trailing zeros and decimal point from a floating-point number +-- when possible. +-- +-- Obtained from here: https://stackoverflow.com/a/35980995/105137 +formatNumberCompact :: Scientific -> String +formatNumberCompact v + | v == 0 = "0" + | abs v < 1e-5 || abs v > 1e10 = formatScientific Exponent Nothing v + | v - fromIntegral (floor v :: Integer) == 0 = formatScientific Fixed (Just 0) v + | otherwise = formatScientific Generic Nothing v diff --git a/src/Swarm/Doc/Schema/SchemaType.hs b/src/Swarm/Doc/Schema/SchemaType.hs new file mode 100644 index 000000000..4f85397ef --- /dev/null +++ b/src/Swarm/Doc/Schema/SchemaType.hs @@ -0,0 +1,38 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Representation of the "type" of a schema. +module Swarm.Doc.Schema.SchemaType where + +import Control.Applicative ((<|>)) +import Data.Aeson +import Data.Text (Text) +import Data.Text qualified as T +import System.FilePath (takeBaseName) + +newtype SingleOrList a = SingleOrList + { getList :: [a] + } + deriving (Eq, Ord, Show) + +instance (FromJSON a) => FromJSON (SingleOrList a) where + parseJSON x = + fmap SingleOrList $ + pure <$> parseJSON x <|> parseJSON x + +data SchemaType + = -- | A basic built-in type + Simple (SingleOrList Text) + | -- | Any one of multiple possible schema types + Alternatives [SchemaType] + | -- | A reference to a schema defined elsewhere + Reference SchemaIdReference + | -- | Members of a list, all of the given schema type + ListOf SchemaType + deriving (Eq, Ord, Show) + +newtype SchemaIdReference = SchemaIdReference Text + deriving (Eq, Ord, Show) + +fromFilePath :: FilePath -> SchemaIdReference +fromFilePath = SchemaIdReference . T.pack . takeBaseName diff --git a/src/Swarm/Doc/Util.hs b/src/Swarm/Doc/Util.hs new file mode 100644 index 000000000..005457dfa --- /dev/null +++ b/src/Swarm/Doc/Util.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Utilities for generating doc markup +module Swarm.Doc.Util where + +import Data.Text (Text) +import Data.Text qualified as T + +wrap :: Char -> Text -> Text +wrap c = T.cons c . flip T.snoc c + +codeQuote :: Text -> Text +codeQuote = wrap '`' + +addLink :: Text -> Text -> Text +addLink l t = T.concat ["[", t, "](", l, ")"] + +tshow :: (Show a) => a -> Text +tshow = T.pack . show diff --git a/swarm.cabal b/swarm.cabal index c4415e447..5d02fd0f1 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -103,6 +103,12 @@ library Swarm.Constant Swarm.Doc.Gen Swarm.Doc.Pedagogy + Swarm.Doc.Schema.Arrangement + Swarm.Doc.Schema.Parse + Swarm.Doc.Schema.Refined + Swarm.Doc.Schema.Render + Swarm.Doc.Schema.SchemaType + Swarm.Doc.Util Swarm.Game.Failure Swarm.Game.Achievement.Attainment Swarm.Game.Achievement.Definitions @@ -264,12 +270,15 @@ library minimorph >= 0.3 && < 0.4, transformers >= 0.5 && < 0.7, mtl >= 2.2.2 && < 2.4, + pandoc >= 3.0 && < 3.2, + pandoc-types >= 1.23 && < 1.24, murmur3 >= 1.0.4 && < 1.1, natural-sort >= 0.1.2 && < 0.2, palette >= 0.3 && < 0.4, parser-combinators >= 1.2 && < 1.4, prettyprinter >= 1.7.0 && < 1.8, random >= 1.2.0 && < 1.3, + scientific >= 0.3.6 && < 0.3.8, servant >= 0.19 && < 0.21, servant-docs >= 0.12 && < 0.14, servant-server >= 0.19 && < 0.21,