Skip to content
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

Open
byorgey opened this issue Sep 24, 2021 · 17 comments
Open

Saving and loading games #50

byorgey opened this issue Sep 24, 2021 · 17 comments
Labels
C-Project A larger project, more suitable for experienced contributors. G-CESK machine This issue has to do with the CESK machine (the Swarm language interpreter). G-Entities An issue having to do with game entities. G-Game State An issue having to do with the game state record. G-Robots An issue having to do with robots. G-Scenarios An issue having to do with scenario design, the way scenarios are described and loaded, etc. G-World An issue having to do with world design, world generation, etc. S-Critical This is an issue that seriously affects playability or user experience. Z-Feature A new feature to be added to the game. Z-User Experience This issue seeks to make the game more enjoyable to play.

Comments

@byorgey
Copy link
Member

byorgey commented Sep 24, 2021

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.

  • A version number, so that we can do something reasonable with saves created by a previous version of the game.
  • The current state of the world:
    • The seed used to generate the world
    • Which tiles have been loaded
    • The contents of those tiles (either the complete contents, or perhaps just a catalog of the cells whose entity contents have changed from their initial value)
  • All (active and inactive) robots, including their CEK machine state.
  • Note, when saving entities (either in the world map, or in an inventory, or a robot entity), if the hash matches the hash of a standard entity, we only have to save the hash. Otherwise we must serialize the entire entity.
    • Note that for this to work well on old save files, we need to keep old/outdated entities around (perhaps in a separate old_entities.yaml file) so that we can load them.
    • Alternatively (this might be simpler), every save file could just contain a complete description of every entity which needs to be referenced?
  • Probably various other fields of the GameState and/or UIState.

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 .

@byorgey byorgey added Z-User Experience This issue seeks to make the game more enjoyable to play. Z-Feature A new feature to be added to the game. C-Project A larger project, more suitable for experienced contributors. S-Critical This is an issue that seriously affects playability or user experience. G-CESK machine This issue has to do with the CESK machine (the Swarm language interpreter). G-Entities An issue having to do with game entities. G-World An issue having to do with world design, world generation, etc. G-Game State An issue having to do with the game state record. G-Robots An issue having to do with robots. labels Sep 24, 2021
@xsebek
Copy link
Member

xsebek commented Sep 25, 2021

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.
Thus, if I save the game on v1.41.0 and load it on v1.42.0, I could send robots to explore tiles with new features from version 1.42.0.

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.

@byorgey
Copy link
Member Author

byorgey commented Sep 25, 2021

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.

@xsebek xsebek mentioned this issue Oct 3, 2021
@xsebek
Copy link
Member

xsebek commented Oct 17, 2021

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:

@byorgey
Copy link
Member Author

byorgey commented Oct 18, 2021

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.

@sullyj3
Copy link

sullyj3 commented Oct 21, 2021

Maybe this could be an option?

https://hackage.haskell.org/package/store-0.7.12/docs/Data-Store.html

@byorgey
Copy link
Member Author

byorgey commented Oct 21, 2021

Ah, yes, that seems like it could be a good option if we go with a binary format (as opposed to the text-based format @xsebek is advocating for above). I didn't know about that package, thanks for the link @sullyj3 !

@byorgey
Copy link
Member Author

byorgey commented Apr 4, 2022

As for versioning, https://hackage.haskell.org/package/DataVersion seems like an interesting option. I am not sure if this is too crazy/overkill.

@byorgey
Copy link
Member Author

byorgey commented Aug 11, 2022

We seem to have continued moving in the potential direction of using ToJSON/FromJSON instances to save things in .yaml files. This makes sense as we have already invested a bunch in reading scenarios in that format. I think one concrete next step would be to focus on the ability to round-trip values through ToJSON and then back via FromJSON (we should add some tests to this effect). Many instances currently just derive instances via Generic but I suspect that will need to change in some cases.

  • For example, Env containues Values which contain VRefs that reference a Store. Perhaps it works fine to just serialize the Store and the Values verbatim. However, we could also use it as an opportunity to do a bit of garbage collection (which we don't currently do at all).

@xsebek
Copy link
Member

xsebek commented Aug 11, 2022

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.
Getting a list of entities with unique names separately would IMHO make everything more readable.


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. 🙂

@xsebek
Copy link
Member

xsebek commented Aug 11, 2022

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. 🤷

@xsebek xsebek pinned this issue Oct 7, 2022
@xsebek
Copy link
Member

xsebek commented Oct 7, 2022

@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 ToJSON(E) instances and use them before having full game saves.

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. 😅

@valyagolev
Copy link
Collaborator

given the programming focus of the game, a fun suggestion could be to use Dhall

@sullyj3
Copy link

sullyj3 commented Oct 8, 2022

Is the point of Dhall not to reduce the maintenance burden for manually written configuration? For a save game, the program serializes it, the program deserializes it, and at no point should a human need to edit it. This seems to obviate the need for functions and Dhall, and a simpler solution like yaml makes more sense to me

Actually this whole objection doesn't apply, I hadn't realised user-editability was a goal.

@xsebek
Copy link
Member

xsebek commented Oct 9, 2022

@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 aeson API, we get JSON and YAML for free, so we can easily output JSON over the web API for tooling.

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 docker commit.

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)

@valyagolev
Copy link
Collaborator

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.

@xsebek
Copy link
Member

xsebek commented Oct 9, 2022

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 (ToJSONE - think world palette) and finally it needs to be all carefully put together.

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.

@byorgey
Copy link
Member Author

byorgey commented Mar 24, 2023

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.

mergify bot pushed a commit that referenced this issue Aug 17, 2023
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.
@byorgey byorgey added the G-Scenarios An issue having to do with scenario design, the way scenarios are described and loaded, etc. label Jan 29, 2024
@byorgey byorgey changed the title Saving/loading games Saving and loading games Oct 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Project A larger project, more suitable for experienced contributors. G-CESK machine This issue has to do with the CESK machine (the Swarm language interpreter). G-Entities An issue having to do with game entities. G-Game State An issue having to do with the game state record. G-Robots An issue having to do with robots. G-Scenarios An issue having to do with scenario design, the way scenarios are described and loaded, etc. G-World An issue having to do with world design, world generation, etc. S-Critical This is an issue that seriously affects playability or user experience. Z-Feature A new feature to be added to the game. Z-User Experience This issue seeks to make the game more enjoyable to play.
Projects
No open projects
Status: Deferred
Development

No branches or pull requests

4 participants