Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.
Olaf Szmidt edited this page Sep 15, 2017 · 23 revisions

This part of the code has not yet been merged, but is fully supported by the AIMMO Unity project. The relevant pull requests are (in order) #220 and #224.

Extensible Level Generation Architecture

The intention of the level generation is to be as separate as possible from the rest of the AI:MMO implementation. The reason behind is that level requirements might change a lot on the way, so we want a module coupled as loosely as possible as part of the game logic.

Base Generator

The main class that gets exposed to the Django application is the JSONLevelGenerator, which implements the interface exposed by the BaseGenerator.

The BaseGenerator has the following template:

  • contructor(setting)
    • a set of basic settings that the map uses at generation
    • see DEFAULT_LEVEL_SETTINGS in simulation.world_map
  • get_game_state(avatar_manager)
    • exposes a game state used by the turn manager daemon
    • for details see Game State
  • check_complete(game_state)
    • returns false by default, should be overrided when inheriting
    • function to check if a map is "complete"
    • the turn manager runs the action for each avatar, then runs check_complete afterwards
  • get_map (@abstract)
    • returns the generated map

EmptyMapGenerator

Generates a centered empty map by the width and height. Can also generate an empty map by given corners.

JSONLevelGenerator

The JSON level generators generates a map in stages, receiving a JSON input map and a list of decoders that can modify the exposed map to the internal game classes. A complete reference of how the internals work can be found in World Map and Game Objects wikis.

Workflow:

  • setup the metadata: map dimensions, etc.
  • register the JSON that represents the map
  • register the decoders that transform the JSONs into WorldMap objects
  • decode the map applying the decoder to each of the JSONs

To register a level extend this class, this class is extended. All the levels described in the .txt and .json formats found in the levels folder are automatically imported.

Level Formats

Back-end levels (in folders maps and models)

The back-end levels are an easy way to convert a map described by one map and one(multiple) model(s) to a JSON level. The JSON format is the one fed to the Map Generator.

An example map.txt look like this:

1 1 1 1 2 2 
0 1 1 1 2 2
0 0 1 1 2 2
0 0 0 1 2 2
0 0 0 0 2 2
0 0 0 0 0 2

The .txt files is a one-to-one representation of a map that is transformed in a JSON by applying a model to it. A model is a list of JSONS that are used to transform a .txt map into a JSON. A model supports transforms, which are classes method calls. An example model looks like:

[
  {
    "code": "2",
    "id" : "class:CellTransform.compute_id",
    "x" : "class:CellTransform.get_x",
    "y" : "class:CellTransform.get_y",
    "sprite" : {
      "width" : "400",
      "height" : "400",
      "path" : "Grass-400x400-isometric-top"
    }
  },
  {
    "code": "1",
    "id" : "class:CellTransform.compute_id",
    "x" : "class:CellTransform.get_x",
    "y" : "class:CellTransform.get_y",
    "sprite" : {
      "width" : "512",
      "height" : "1024",
      "path" : "Obstacle-512x1024-isometric-top"
    }
  },
  [...]
]
Terminology

A transform is an instance of a class that can be called inside a model. A function can be called by prepending "class:" before the class name and function name. (e.g. class:CellTransform.get_x) E.g.:

{
   "code": "1",
   "id" : "class:CellTransform.compute_id",
   "x" : "class:CellTransform.get_x",
   "y" : "class:CellTransform.get_y"
}

The parser gets a level formatted as a 2D grid from numbers and transforms each number into a json representing that particular object.

A map is a *.txt file composed out of numbers. Each numbers represent a cell in the grid that will be eventually generated.

A model is an array of jsons. Each json has an associated code. By that associated code, the numbers in the map get translated into an json. To see how the final exported version of a map looks like, run levels.py.

The parser structure
  • parse_model
    • gets the model name as a string and parsers the model from the folder models
  • parse_map:
    • changes the parser's associated map with map at the given path
  • register model/s
    • adds a new model to the model list
  • register transform
    • register an instance of a transform, so it can be used from *.json file
  • map_apply_transfroms
    • transforms a map formated as a list of numbers into a map formatted as list of jsons
  • register_transforms(@abstract)
    • register all transforms that can be used by the parser

Unity levels (in folder json)

The Unity levels get generated using the AI:MMO Unity Level Builder tool from this repository. These are already JSON (loosely) formatted and ready for exporting. A such example of a level looks like:

[
   {
      "x":-1.0000,
      "y":2.0000,
      "code":"ObstacleGenerator",
      "sprite":{
         "width":400,
         "height":400,
         "path":"Grass-400x400-isometric-top",

      }
   },
   {
      "x":0.0000,
      "y":0.0000,
      "code":"ObstacleGenerator",
      "sprite":{
         "width":512,
         "height":1485,
         "path":"brick-512x1485-isometric-windows-left",

      }
   },
   [...]
]

Level Exposure to the Map Generator

We use the builder design pattern to generate levels from both formats. All the levels are automatically prepared for export by walking through the folders models, maps and json. A code snippet can be found below:

LEVELS = {}

TXT_LEVELS = os.listdir(_MAPS_FOLDER)
TXT_LEVELS.sort()
for lvl in range(1, len(TXT_LEVELS) + 1):
    lvl_id = "level" + str(lvl)
    LEVELS[lvl_id] = RawLevelGenerator()
       .by_parser(CellParser())
       .by_map(TXT_LEVELS[lvl - 1])
       .by_models(["objects.json"])
       .generate_json()

JSON_LEVELS = os.listdir(_JSON_FOLDER)
JSON_LEVELS.sort()
for lvl in range(len(TXT_LEVELS) + 1, len(JSON_LEVELS) + len(TXT_LEVELS) + 1):
    lvl_id = "level" + str(lvl)
    LEVELS[lvl_id] = RawJSONLevelGenerator()
        .by_json_level_name(JSON_LEVELS[lvl - len(TXT_LEVELS) - 1])
        .generate_json()

The map generator imports the LEVELS variable and uses it to generate classes that are exposed as generators to the Django application. We generate all the classes completely automatically in the function:

def generate_level_class(level_nbr, LEVELS=LEVELS, COMPLETION_CHECKS=COMPLETION_CHECKS):`
    [...]
    def get_map_by_level(level_id):
        [...]

    ret_class = type(level_name, (JsonLevelGenerator,), {
        "get_map": get_map_by_level(level_id),
        "check_complete": COMPLETION_CHECKS[level_id]
    })

    return ret_class

for cur_level in xrange(1, len(LEVELS) + 1):
    gen_class = generate_level_class(cur_level)

# append the new generated classes to the current module
setattr(current_module, gen_class.__name__, gen_class)

The Django application asks for a Level generator through the API. The Flusk Microservice then choses the correct Generator class as follows:

     api_url = os.environ['GAME_API_URL']
    if hasattr(custom_map, settings['GENERATOR']):
        # our level generators are inside the custom_map module
        generator = getattr(custom_map, settings['GENERATOR'])(settings)
    else:
        generator = getattr(map_generator, settings['GENERATOR'])(settings)

Translating JSON to internal objects

Our output from levels.py look like this:

   {
      "x":-1.0000,
      "y":2.0000,
      "code":"ObstacleGenerator",
      "sprite":{
         "width":400,
         "height":400,
         "path":"Grass-400x400-isometric-top"
      }
   }

We want to convert this to internal objects. We have special classes for this, which are called "decoders". A decoder is a class that receives a JSON and returns a World Map.

We have several decoders, each one being registered for a specific code. In the JSON above, the code would be "ObstacleGenerator". A simple decoder example is the one for the Object:

class ObstacleDecoder(Decoder):
    def decode(self, json, world_map):
        x, y = int(json["x"]), int(json["y"])
        world_map.get_cell(Location(x, y)).cell_content = Obstacle(get_sprite(json))

You can see that the x and y coordinates are parsed from the JSON. Also, the sprite description is parsed and passed along to the cell content. The sprite is used to render the right textures things in the front-end.

Final note (a.k.a. keeping the Unity-Game communication consistent)

A model should be a self-documenting specification for how the back-end maps are generated. It is useful to try to have the exact same specification for the models as the specification for the files that come from Unity exports.

Clone this wiki locally