-
Notifications
You must be signed in to change notification settings - Fork 71
Levels
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.
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.
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
Generates a centered empty map by the width and height. Can also generate an empty map by given corners.
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.
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"
}
},
[...]
]
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
.
- 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
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",
}
},
[...]
]
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)
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.
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.