-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Saving and loading games #50
Comments
Thanks for making this issue! I think a more directly related issue is Generate world by tiles (#29). I could then save loaded tiles and generate new ones in whatever is the current world generation function. If the save was done per square, then changing world generation could introduce problems (:mountain:), but with tiles, I do not care what would have been on the unexplored ones in the previous version. TLDR I would argue for the world state to be the seed and complete tiles, so versioning world generation is not a headache. |
Hmm, thanks, that's definitely worth considering! That would certainly simplify the need to keep track of versions for world generation. On the other hand, it might create weird seams at tile boundaries where e.g. half a lake on a previously loaded tile is suddenly right next to half a mountain on a new tile. But I'm not sure how important that is. |
I wonder if we could dramatically simplify this by implementing #29, #7 and give each entity a unique character and actually try storing the game as YAML. - robots:
...
- seed: 0
- world:
- tile:
- x: 0
- y: 0
- map: >
+------------------------+
| * AAA TTT |
| ~~ * AAAÅÅ TT | Most users likely will not change that many tiles, so the map will not take much space. I guess with #7, robot definitions should not be an unbound number of lines either, but I do not know how many robots would need to keep a non-default state and how long that would be. I still see this as viable option with some pros:
|
I suppose it could be worthwhile to try a text-based solution. At the very least it opens up a fun new avenue for cheating ;-). As for giving each entity a unique character, I think that would be rather restrictive (for example with #186 we now have multiple kind of mountain/vein entities with the same display, which is on purpose). But I don't think we have to do that: we could store a separate "entity palette" which lists characters and what entity they correspond to in this save file. We can try hard to use the same characters that are used for the actual display, so the saved map will mostly look the same as in the game. But in cases where multiple entities have the same character, we can just arbitrarily generate new unique characters for some of them. Note that for the game state of robots we have to store their entire CEK machine state, not just their definitions. |
Maybe this could be an option? https://hackage.haskell.org/package/store-0.7.12/docs/Data-Store.html |
As for versioning, https://hackage.haskell.org/package/DataVersion seems like an interesting option. I am not sure if this is too crazy/overkill. |
We seem to have continued moving in the potential direction of using
|
For a simpler example, the robot JSON instance which we can see using the web interface is too verbose. The problem is mainly the inventory and installed devices. What I would like is for it to look like the scenario format. Currently, new entities are not created during the game, but we will always be able to create unique names for them. It is just something we should keep in mind if we wanted to design the JSON format this way instead of inlining the entities. 🙂 |
As an alternative idea, we could let JSON be fully inlined, but in YAML we would use aliases. Something like: installed:
- *entities.copper_wire I do not think the expanded JSON is worth it, but it would make some JSON queries simpler. 🤷 |
@byorgey shall we update the Issue description and go with YAML? 🙂 It would be a lot simpler for us. Also, we could use it for scenarios and the web API. That way we can incrementally create the We could make this a Meta Issue and link other Issues that we need to finish first. Honestly this task looks so big that it scares me and I hope that splitting it into smaller tasks will make it less scary. 😅 |
given the programming focus of the game, a fun suggestion could be to use Dhall |
Actually this whole objection doesn't apply, I hadn't realised user-editability was a goal. |
@valyagolev @sullyj3 Dhall is certainly an interesting suggestion, but I do not immediately see what it would be good for. 🤔 I mostly want to use YAML for saves because of the ubiquitous support and compatibility with JSON. By using Secondly, we already use YAML for scenarios and I hope to get some value out of using a similar or even the exact same format. Specifically, I think it would be nice to copy-paste some part of your save file to a scenario - maybe you could use it for a workflow similar to Now if someone wants to use Dhall and pass it to Swarm, then that should be really easy: swarm --scenario <(dhall-to-yaml-ng my-scenario.dhall) |
I do have doubts Dhall is the right way here, but it does have a few benefits here. One of them is much simpler migrations. Basically we can migrate data... in Dhall. We could save data by using functions to create entities, and then we will just have to change those functions if our data format is updated. If we want for the players to be able to change the save files, Dhall also has greater potential for safe compression. YAML aliases are easy to get wrong, and not even notice until it's too late. Dhall can be much more readable and safe to edit. I'm also a bit curious about people using advanced Dhall to create e.g. interesting proc-generated scenarios. So yeah I think it's a fun option and it has benefits. |
If we do not duplicate information in the save file too much, then using aliases will not be necessary. 🤷 If the save file grows linearly with your game, then it's size should be manageable and a compression with zip all you ever need. I don’t expect the players will change the save file too often, not unless they are hard core cheaters. 😆 But it would be great if the format was readable so it can be used for debugging. The good thing is you can use Dhall to create parametrised scenarios and similar things. You just expand it to YAML for Swarm as I showed before. 😉 If that turns out to work better then YAML, then we can start by allowing different file formats (.json, .yml.zip, .dhall) and after that we can implement saving in Dhall. More importantly… 🚲To be clear what is blocking us here is not being unable to make up our minds about save file format. 🤣 It is that some parts of the game are missing serialisation entirely, namely world (update) function needs to be changed to even be serialisable and to not get exponential explosion the instances have to use outer environment ( I would like to split this into smaller tasks so we can iteratively improve this in our free time. In the end the problem is that someone needs to do it. 😅 PS: If anyone has implemented a Dhall serialisation for Swarm, then we would be thrilled to accept it. We do not lock ourselves out of using it with current YAML plan, in fact because they share JSON structure, it would be easy to migrate in future. 🙂 PSS: I hope this clarifies it a bit and does not turn you away @valyagolev and @sullyj3. 😁 TLDR; we are trying to do less work by using YAML atm., and Dhall output looks like more work, but Dhall input works already in a way. |
- make `WorldUpdate` and `RobotUpdate` serializable - part of #50
I think #1138 is a good concrete next step towards this issue, since saving multiple independent map patches will be helpful for creating save files as well. e.g. we could analyze the map, see where it has changed from the default, and use some heuristics to identify rectangular patches with modified cells to save. |
DSL for programming worlds, towards #1320 and #29 (and, indirectly, toward #50, since the world DSL should make a nice target for world saves) . Eventually this should be able to recreate all the world description/building features we have, though there is still a long way to go. But currently we can at least recreate the "classic" procedurally-generated world. I think this is a solid foundation we can merge as a first step, and then work on adding more features in subsequent PRs. Below are some notes that should help in reviewing. Note that the large number of files changed is due in large part to the elimination of the `default` field in scenario descriptions; see the "changed files" section below for an overview of the important/interesting changes. Issues split off from this one: #1394 #1395 #1396 #1397 Major changes ============ - New `data/worlds` subdirectory - All `.world` files are parsed at load time and saved in a `WorldMap` which gets threaded through, similar to `EntityMap` (perhaps we should think about passing around a single record instead) - Standard "classic" world - Used to be `testWorld2`, defined in Haskell code; now it is defined via the DSL in `worlds/classic.world`. This should make it much easier to experiment with variations. - We can now automatically extract entities mentioned in a world DSL term with `extractEntities`. There used to be an explicit list in `testWorld2Entities`, used to check pedagogy, generate documentation, etc., but it turns out it had (predictably) gotten out of date! This can't happen anymore. - It is now referenced in several tutorials (backstory, farming, world101, speedruns, etc.) - The `default` field of world descriptions is no more: one can use `dsl` to just specify a constant - Note in `Swarm.Game.State`, `dslWF` and `arrayWF` are combined using the `Monoid` instance to create `wf`. - `Erasable` - It used to be the case that if some kind of default terrain + entity was specified (e.g. stone + water), any `map` would completely override the default. However, we want to move towards combining everything with a `Monoid` instance. But by default this means the default entity would show through anywhere the `map` did not specify an entity. So we need a way to explicitly "erase" an entity from a lower layer. - If `e` is a `Semigroup`, then `Maybe e` is a `Monoid` where `Nothing` acts as an identity element. Likewise, `Erasable e` is a `Monoid` but adds two new elements: `ENothing` to be an identity, and `EErase` to be an *annihilator*. i.e. combining with `EErase` is like multiplying by zero. - We can now specify `erase` as an entity to override entity underneath. - There are several Haskell files with only changes related to `Erasable`, relating to e.g. the world editor, `PCells`, etc.; I'm not 100% sure I've always done the right thing here. DSL overview =========== - Integer, float, and Boolean literals. Note that `3` is *always* an `int`, and `3.0` is a `float`. It makes things much easier to not have to deal with `3` possibly being either `int` or `float`, though it does make things slightly more annoying for programmers. - Standard boolean, arithmetic, and comparison operators - `if ... then ... else ...` - `<>` operator for combining via `Semigroup` instance - Cell literals are enclosed in curly braces. Unlike the previous awkward world description syntax with one, two, or three-element lists denoting terrain, terrain + entity, or terrain + entity + robot, there can now be any number of elements in any order. - `{foo}` will be resolved as either terrain, an entity, or a robot, whichever is successful. So if the names are unambiguous one can just write `{tree}` or `{stone}`. - It is possible to explicitly indicate the type of cell value with syntax like `{entity: tree}` or `{terrain: stone}`. - Multiple items separated by commas is syntax sugar for combining with `<>`. e.g. `{tree, entity: boulder, stone} = {tree} <> {entity: boulder} <> {stone}`. - Ability to refer to the `seed` - Refer to the current `x` or `y` coordinates or the `hash` of the current coordinates - `let`-expressions for multiple variables: `let x1 = e1, x2 = e2, ... in ...` - `overlay [e1, e2, ...]` layers `e1` on the bottom, `e2` on top of that, etc., using the `Semigroup` instance for world functions - `"foo"` imports the DSL term in `worlds/foo.world` - `perlin` function to generate perlin noise - `mask` function to mask with a condition Changed files =========== - `Swarm.Util`: moved the `acquire` function here and gave it a more descriptive name. - `Swarm.Doc.Gen`: can now extract mentioned entities directly. - `Swarm.Game.Failure`: added new failure modes - `Swarm.Game.Scenario.Topography.WorldDescription`: get rid of `defaultTerrain` field, add `worldProg` for DSL. - `Swarm.Game.State`: see comment. - `Swarm.Game.World`: a bit of reorganization. Added a bunch of modules under this. - `Swarm.Game.World.Coords`: moved some code here from `Swarm.Game.World`. - `Swarm.Game.World.Gen`: moved some things here from `Swarm.Game.WorldGen` (also deleted a bunch of irrelevant code), and also added the `extractEntities` function to get all entities mentioned by a DSL term. - `Swarm.Game.World.Syntax`: raw, untyped syntax for world DSL terms. - `Swarm.Game.World.Parse`: parser for world DSL terms. Fairly standard. - `Swarm.Game.World.Typecheck`: takes raw, untyped terms produced by the parser and both typechecks and elaborates them into a simpler core language. An interesting feature is that the core language is *type-indexed*, so that the Haskell type system is actually ensuring that our typechecker is correct; every typechecked world DSL term value has a type which is indexed by a Haskell type corresponding to the type of the underlying DSL term. For example, `{entity: tree}` would have a type like `TTerm [] (World CellVall)` etc. Once terms make it through the typechecker, there cannot possibly be any bugs in the rest of the pipeline which would result in a crash, because the Haskell type system. (There could of course be *semantic* bugs.) Understanding exactly how the typechecker works is not too important. Of interest may be the `resolveCell` function, which determines how we decide what `Cell` is represented by a cell expression in curly braces. - `Swarm.Game.World.Abstract`: compile elaborated, typechecked world DSL terms down into an extremely simple core language with only constants and function application. This gives us very fast evaluation of world DSL terms. Understanding this module is not really necessary but there is a link to a blog post for those who are interested in how it works. - `Swarm.Game.World.Compile`: a further processing/compilation step after `Swarm.Game.World.Abstract`. Currently we don't actually use this, since it doesn't seem like it makes a big efficiency difference. - `Swarm.Game.World.Interpret`: interpreter for abstracted world DSL terms. - `Swarm.Game.World.Eval`: just puts together the pieces of the pipeline to evaluate a typechecked world DSL term. - `Swarm.Game.World.Load`: just loading world DSL terms from disk.
It should be possible to save the current state of your game, and then later to load from a saved game. There is nothing theoretically difficult about this, but it will take a good deal of engineering. Off the top of my head, a save would need to include.
old_entities.yaml
file) so that we can load them.GameState
and/orUIState
.For loading entities and recipes, we use
.yaml
files, so that they can be easily human-editable.However, for saving the entire game state we definitely don't want to do that. Instead we should use some kind of binary serialization framework so save files will be as compact as possible.Since we can already load scenarios from.yaml
files, it makes sense at this point to just double down on that and make save files use the same format.Once we enable this feature, we'll have to be very careful about versioning things such as entities and recipes, world generation, etc., and keep around old versions of things (as much as is reasonable) so that we can correctly load save files produced by older versions of the game.
Note, a related issue is #7 .
The text was updated successfully, but these errors were encountered: