How to Create new puzzle

This tutorial explains step by step how to create MyHeroPuzzle

Let's create a new puzzle that is similar to the existing HeroPuzzle from scratch.

Our hero must build his power and defeat enemies in different floor sections. Once the hero enters a floor section, he must complete it before moving to another one. We will place one directional cells for this purpose.

The code of our new puzzle will reside in file puzzle/myhero.py.

Our puzzle will have no constraints on the level configuration, so let's start to code it like this:

from . import *

class MyHeroPuzzle(Puzzle):
	def assert_config(self):
		pass

This puzzle includes special cell types like finish, portals and directional cells. This should be declared like this:

	def has_finish(self):
		return True

	def has_portal(self):
		return True

	def has_dirs(self):
		return True

The goal of our puzzle is to kill all enemies. This should be declared like this:

	def is_goal_to_kill_enemies(self):
		return True

Now it is a time to implement a random generation of floor sections (with enemies and powerups), directional cells and portals:

	def generate_room(self):
		self.set_area_from_config(default_size=(5, 5), request_odd_size=True, align_to_center=True)

		num_floors = (self.area.size_y + 1) / 2
		num_slots = self.area.size_x - 1

		self.set_area_border_walls()
		if self.area.x1 > self.room.x1:
			self.map[self.area.x1 - 1, (self.area.y1 + self.area.y2) // 2] = CELL_FLOOR

		for cell in self.area.cells:
			if (cell[1] - self.area.y1) % 2 == 1:
				if cell[0] != self.area.x1:
					self.map[cell] = CELL_WALL
			elif cell[0] == self.area.x1:
				self.map[cell] = CELL_DIR_R
			elif cell[0] == self.area.x2:
				self.Globals.create_portal(cell, (self.area.x1, cell[1]))
			else:
				slot_type = randint(0, 2)
				if slot_type == 0:
					self.Globals.create_enemy(cell, randint(10, 50))
				elif slot_type == 1:
					op = choice('×÷+-')
					factor = (2, 3)[randint(0, 1)] if op in ('×', '÷') else (50, 100)[randint(0, 1)]
					drop_might.instantiate(cell, op, factor)

		self.map[self.room.x1, self.room.y2] = CELL_FINISH

In the future we may want to add more useful features, like support for rooms (foor or nine), support for loading precreated maps, a solver like in some other puzzles and more. But for now this will be enough.

Here is complete myhero.py implementation:

from . import *

class MyHeroPuzzle(Puzzle):
	def assert_config(self):
		return bool(char.power)

	def has_finish(self):
		return True

	def has_portal(self):
		return True

	def has_dirs(self):
		return True

	def is_goal_to_kill_enemies(self):
		return True

	def generate_room(self):
		self.set_area_from_config(default_size=(5, 5), request_odd_size=True, align_to_center=True)

		num_floors = (self.area.size_y + 1) / 2
		num_slots = self.area.size_x - 1

		self.set_area_border_walls()
		if self.area.x1 > self.room.x1:
			self.map[self.area.x1 - 1, (self.area.y1 + self.area.y2) // 2] = CELL_FLOOR

		for cell in self.area.cells:
			if (cell[1] - self.area.y1) % 2 == 1:
				if cell[0] != self.area.x1:
					self.map[cell] = CELL_WALL
			elif cell[0] == self.area.x1:
				self.map[cell] = CELL_DIR_R
			elif cell[0] == self.area.x2:
				self.Globals.create_portal(cell, (self.area.x1, cell[1]))
			else:
				slot_type = randint(0, 2)
				if slot_type == 0:
					self.Globals.create_enemy(cell, randint(10, 50))
				elif slot_type == 1:
					op = choice('×÷+-')
					factor = (2, 3)[randint(0, 1)] if op in ('×', '÷') else (50, 100)[randint(0, 1)]
					drop_might.instantiate(cell, op, factor)

		self.map[self.room.x1, self.room.y2] = CELL_FINISH

Place this file myhero.py in puzzle/ directory.

Finally, add new puzzle level into file levels.py that includes "myhero_puzzle": {} line:

	{
		"n": 0.1,
		"theme": "classic",
		"music": "stoneage/08_the_golden_valley.mp3",
		"char_power": 40,
		"goal": "Complete MyHero puzzle",
		"myhero_puzzle": {},
	},

Finally run ./dungeon and enjoy your new puzzle.