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 that allow movement to the right but not back to the left 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 one-directional cells. This should be declared like this:

	def has_finish(self):
		return True

	def has_portal(self):
		return True

	def has_odirs(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

It's time to implement random generation of floor sections, which include enemies and powerups. Each floor section is enclosed by walls. A one-way cell to the right (CELL_ODIRR) serves as an entry lock, and a portal at the end of the section returns the player to its starting point:

	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_ODIRR
			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_odirs(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_ODIRR
			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.