Star

linkCreating a Level Generator for an RPG Game

Procedurally generated maps in video games has become a hot topic in the recent years among the gaming community. the algorithms for generating these maps are extremely complected, and requires months of development to build such algorithms. Instead of symbolically written programs, what if we can use generative models to generate these maps?

In this Tutorial we will show you how you can generate game maps for a simple game where the map can be expressed using a list of strings.

linkWhat is a level?

A level can be represented in this hypothetical game using a list of strings shown below.

1linkglob level = [ 'BBBBBBBBBB',

2link 'B....E...B',

3link 'B.P......B',

4link 'B.....B..B',

5link 'B.....BE.B',

6link 'B....BBBBB',

7link 'B....B...B',

8link 'B.....E..B',

9link 'BBBBBBBBBB' ];

In this level each character represent a different element in the map,

linkBuilding a level map generator?

The straightforward approach to build a map generator is to ask from the LLM to directly generate such a map as a list of strings. MTLLM allows you to do this by defining a function or a method. However, here we would discuss a more object oriented way of programming with LLMs which allow the model to 'think' using objects.

linkLevel Configuration

level_manager.jac
1linkobj Level {

2link has name: str,

3link difficulty: int;

4link has width: int,

5link height: int,

6link num_wall: int,

7link num_enemies: int;

8link has time_countdown: int,

9link n_retries_allowed: int;

10link}

Each level should have a basic configuration which describes the level in an abstract format. This level object embeds the difficulty of the level and the number of enemies and obstacles including other level configuration parameters.

However, filling in the values for fields requires a cognitive capacity, for which will use an LLM later on.

linkBuilding the Map Elements

level_manager.jac
1linkobj Position {

2link has x: int,

3link y: int;

4link}

As the map we are aiming to generate is a 2D map the position of each object on the map can be designated using the Position custom type. It is simply representing a cartesian 2D coordinate system.

level_manager.jac
1linkobj Wall {

2link has start_pos: Position,

3link end_pos: Position;

4link}

The wall object represents a straight wall, as all obstacles in the 2D map can be represented by a collection of intersecting wall objects. Here each wall object will have a start position as well as a stop position

level_manager.jac
1linkobj Map {

2link has level: Level;

3link has walls: list[Wall],

4link small_obstacles: list[Position];

5link has enemies: list[Position];

6link has player_pos: Position;

7link}

This Map object will hold the exact positions of all objects in the map. This is the object that we will generate using MT-LLM. Each field of this object is one of or a derivative of the custom types which we described above.

linkThe Level Manager

To manage all the generations we can define a Level manager object which can hold a directory of previous levels configurations and maps, which can be used to feed the LLM to give context about the play style of the player. We will be using the OpenAI GPT-4o as the LLM in this tutorial.

level_manager.jac
1linkimport:py from mtllm.llms, OpenAI;

2linkglob llm = OpenAI(model_name="gpt-4o");

3link

4linkobj LevelManager {

5link has current_level: int = 0,

6link current_difficulty: int = 1,

7link prev_levels: list[Level] = [],

8link prev_level_maps: list[Map] = [];

9link

10link '''Generates the Next level configuration based upon previous playthroughs'''

11link can create_next_level( last_levels: list[Level],

12link difficulty: int,

13link level_width: int,

14link level_height: int) -> Level by llm(temperature=1.0);

15link

16link '''Get the Next Level'''

17link can get_next_level -> tuple(Level, Map);

18link

19link '''Generate the Map as a List of Strings'''

20link can get_map(map: Map) -> list[str];

21link}

We have three methods defined under the level manager. Each will handle a separate set of tasks.

The implementation of the above methods are as follows.

level_manager.jac
1link:obj:LevelManager:can:get_next_level {

2link self.current_level += 1;

3link # Keeping Only the Last 3 Levels

4link if len(self.prev_levels) > 3 {

5link self.prev_levels.pop(0);

6link self.prev_level_maps.pop(0);

7link }

8link # Generating the New Level

9link new_level = self.create_next_level(

10link self.prev_levels,

11link self.current_difficulty,

12link 20, 20

13link );

14link self.prev_levels.append(new_level);

15link # Generating the Map of the New Level

16link new_level_map = Map(level=new_level by llm());

17link self.prev_level_maps.append(new_level_map);

18link # Increasing the Difficulty for end of every 2 Levels

19link if self.current_level % 2 == 0 {

20link self.current_difficulty += 1;

21link }

22link return (new_level, new_level_map);

23link}

In the get_next_level method there are two llm calls which we will discuss in this tutorial while other parts are related the functionality of the game.

level_manager.jac
1link:obj:LevelManager:can:get_map {

2link map_tiles = [['.' for _ in range(map.level.width)] for _ in range(map.level.height)];

3link for wall in map.walls {

4link for x in range(wall.start_pos.x, wall.end_pos.x + 1) {

5link for y in range(wall.start_pos.y, wall.end_pos.y + 1) {

6link map_tiles[y-1][x-1] = 'B';

7link }

8link }

9link }

10link for obs in map.small_obstacles {

11link map_tiles[obs.y-1][obs.x-1] = 'B';

12link }

13link

14link for enemy in map.enemies {

15link map_tiles[enemy.y-1][enemy.x-1] = 'E';

16link }

17link map_tiles[map.player_pos.y-1][map.player_pos.x-1] = 'P';

18link map_tiles = [['B'] + row + ['B'] for row in map_tiles];

19link map_tiles = [['B' for _ in range(map.level.width + 2)]] + map_tiles + [['B' for _ in range(map.level.width + 2)]];

20link return [''.join(row) for row in map_tiles];

21link}

linkRunning the Program

1linkwith entry {

2link level_manager = LevelManager();

3link for _ in range(2) {

4link (new_level, new_level_map) = level_manager.get_next_level();

5link print(new_level);

6link print('\n'.join(LevelManager.get_map(new_level_map)));

7link }

8link}

This program will now generate two consecutive maps and print them on the terminal. by running this jac file using jac run level_manager.jac you can simply test your program.

linkA full scale game demo

For the sake of this tutorial we have not included the entire development of an actual game. The full game is available on our jac-lang repo. A sample demonstration of the game can be viewed below.

Demo Video

Creating a Level Generator for an RPG GameWhat is a level?Building a level map generator?Level ConfigurationBuilding the Map ElementsThe Level ManagerRunning the ProgramA full scale game demo

Home

Quick Startchevron_right
Building Blockschevron_right
Tutorialschevron_right
API Referencechevron_right
Tips and Trickschevron_right

FAQs