Reverse-engineered from CASTLE1.EXE (1993) — Rick Saada / Epic MegaGames
MS-DOS NE EXE · Microsoft C · Segment 11 (0x1c640) · Map: DS:0xce2, 64×64, 3 bytes/cell
Configure settings & press Generate
Generation Parameters
Level Statistics
Rooms
—
Corridors
—
Floor %
—
Attempts
—
Generation Log
#Waiting…
Tile Legend
Wall
Room floor
Corridor
Door
Stairs ↓
Stairs ↑
Player ↓
Player ↑
Reverse-Engineering Notes
Map Data Structure
The map is a flat 64×64 array stored at DS:0xCE2,
initialized via rep stosb clearing 0x3000 (12,288) bytes — exactly
64×64×3. Each cell uses 3 bytes:
[+0] tile type (bit 0 = floor, bit 1+4 = stair types) [+1] flags (0x20 = room interior, 0x0C = corridor) [+2] entity ID occupying cell
Indexing: bx = row×192 + col×3
(seen as shl row,6 then ×3)
Room Placement Loop
Found at seg11:0x0BCE. The algorithm attempts to place
rooms up to max_rooms times. For each room:
1. Pick random position: x = rand(W/3) + rand(W/3), same for Y
2. Pick random size: w = rand(8)+4, h = rand(8)+4
3. CanPlaceRoom at 0x164E scans for any non-zero tile in the
target area plus a 1-tile buffer — up to 10 retries
4. On success: call DrawRoom at 0x0E30
Corridor & Stair Logic
After placing each room, a rand(100) check against monster density
(derived from [0x3D88] level descriptor) decides if a corridor connects
this room to the previous one via DrawCorridor at 0x1A88.
Corridors set flag 0x0C (bits 2+3) vs rooms using 0x20 (bit 5).
Doors are placed at room-corridor junctions.
Stairs: down stairs = 0x12, up stairs = 0x13. Levels 1–3 skip
up stairs; level 6 and 20 are branch points in the stair logic.
Python Implementation
A self-contained implementation of the reconstructed algorithm. Drop it into any project —
no dependencies beyond the standard library. Prints an ASCII map to stdout.
cotw_dungeon.py
# Castle of the Winds — Dungeon Generation (reconstructed from seg11 disassembly)
# Algorithm by Rick Saada (SaadaSoft), 1993. Reverse-engineered for archival purposes.import random
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
# ── Tile constants (matching original byte encoding) ──────────────────────────
TILE_WALL = 0x00
TILE_FLOOR = 0x01
TILE_STAIR_DN = 0x12# bits 1+4 (DS:0xCE2 tile type byte)
TILE_STAIR_UP = 0x13# bits 0+1+4
FLAG_ROOM = 0x20# room interior (tile flags byte)
FLAG_CORR = 0x0C# corridor
FLAG_DOOR = 0x40# door junction
MAP_W, MAP_H = 64, 64# ── MS C rand() LCG — matches MSVC runtime seen in CotW data segment ──────────classMSCRand:
def__init__(self, seed: int = 12345):
self.state = seed & 0xFFFFFFFFdefnext(self) -> int:
self.state = (self.state * 214013 + 2531011) & 0xFFFFFFFFreturn (self.state >> 16) & 0x7FFFdefrng(self, n: int) -> int:
return self.next() % n
@dataclassclassRoom:
col: int; row: int; w: int; h: int
cx: int = field(init=False)
cy: int = field(init=False)
def__post_init__(self):
self.cx = self.col + self.w // 2
self.cy = self.row + self.h // 2classDungeon:
def__init__(self, w: int = MAP_W, h: int = MAP_H):
self.w, self.h = w, h
# 3 arrays mirror the original DS:0xCE2 layout: [type, flags, entity]
self.tile_type = bytearray(w * h)
self.tile_flags = bytearray(w * h)
self.tile_entity = bytearray(w * h)
defidx(self, r: int, c: int) -> int:
return r * self.w + c
defclear(self):
# rep stosb DS:0xCE2, 0, 0x3000 (seg11:0x1B04)for arr in (self.tile_type, self.tile_flags, self.tile_entity):
arr[:] = bytearray(len(arr))
defcan_place(self, col: int, row: int, w: int, h: int) -> bool:
# seg11:0x164E — check area + 1-tile border for any non-zero tilefor r in range(max(0, row - 1), min(self.h, row + h + 1)):
for c in range(max(0, col - 1), min(self.w, col + w + 1)):
if self.tile_type[self.idx(r, c)] != 0:
return Falsereturn Truedefdraw_room(self, col: int, row: int, w: int, h: int):
# seg11:0x0E30 — tile |= FLOOR; flags |= FLAG_ROOMfor r in range(row, row + h):
for c in range(col, col + w):
i = self.idx(r, c)
self.tile_type[i] |= TILE_FLOOR
self.tile_flags[i] |= FLAG_ROOM
defdraw_corridor(self, c1: int, r1: int, c2: int, r2: int):
# seg11:0x1A88 — L-shaped corridor: horizontal then vertical# Only sets tiles that are currently wall (rooms take priority)for c in range(min(c1, c2), max(c1, c2) + 1):
i = self.idx(r1, c)
if not self.tile_type[i]:
self.tile_type[i] |= TILE_FLOOR
self.tile_flags[i] |= FLAG_CORR
for r in range(min(r1, r2), max(r1, r2) + 1):
i = self.idx(r, c2)
if not self.tile_type[i]:
self.tile_type[i] |= TILE_FLOOR
self.tile_flags[i] |= FLAG_CORR
defflood_fill_components(self) -> list[int]:
# BFS to label every floor tile with a component ID
comp = [-1] * (self.w * self.h)
num = 0
dirs = [(-1,0),(1,0),(0,-1),(0,1)]
for start_r in range(self.h):
for start_c in range(self.w):
si = self.idx(start_r, start_c)
if not self.tile_type[si] or comp[si] != -1:
continue
q = deque([(start_r, start_c)])
comp[si] = num
while q:
r, c = q.popleft()
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if0 <= nr < self.h and0 <= nc < self.w:
ni = self.idx(nr, nc)
if self.tile_type[ni] and comp[ni] == -1:
comp[ni] = num
q.append((nr, nc))
num += 1return comp
defensure_connected(self, rooms: list[Room]):
# Flood-fill, then bridge any room not reachable from room[0]
comp = self.flood_fill_components()
connected = {comp[self.idx(rooms[0].cy, rooms[0].cx)]}
for i, b in enumerate(rooms[1:], 1):
b_comp = comp[self.idx(b.cy, b.cx)]
if b_comp not in connected:
# Connect to the nearest already-connected room
best = min(
(a for a in rooms
if comp[self.idx(a.cy, a.cx)] in connected),
key=lambda a: abs(a.cx - b.cx) + abs(a.cy - b.cy)
)
self.draw_corridor(best.cx, best.cy, b.cx, b.cy)
connected.add(b_comp)
defgenerate(
level: int = 1,
max_rooms: int = 12,
corr_chance: int = 70, # % chance to draw corridor between consecutive rooms
seed: Optional[int] = None,
) -> Dungeon:
rng = MSCRand(seed if seed is not None else random.randint(0, 0xFFFF))
d = Dungeon()
d.clear()
rooms: list[Room] = []
prev: Optional[Room] = Nonefor _ in range(max_rooms * 3):
if len(rooms) >= max_rooms:
break# rand(W/3) + rand(W/3) — biases towards map centre (seg11:0x0BCE)
col = rng.rng(MAP_W // 3) + rng.rng(MAP_W // 3) + 1
row = rng.rng(MAP_H // 3) + rng.rng(MAP_H // 3) + 1
w = rng.rng(8) + 4
h = rng.rng(8) + 4if col + w >= MAP_W - 1or row + h >= MAP_H - 1:
continueif not d.can_place(col, row, w, h):
continue
d.draw_room(col, row, w, h)
room = Room(col, row, w, h)
rooms.append(room)
if prev and rng.rng(100) < corr_chance:
d.draw_corridor(prev.cx, prev.cy, room.cx, room.cy)
prev = room
if len(rooms) > 1:
d.ensure_connected(rooms)
# Place stairs — down in first room, up in last (levels 1-3 skip up stairs)if rooms:
r0 = rooms[0]
d.tile_type[d.idx(r0.row + 1, r0.col + 1)] = TILE_STAIR_DN
if level > 3and len(rooms) > 1:
rL = rooms[-1]
d.tile_type[d.idx(rL.row + 1, rL.col + 1)] = TILE_STAIR_UP
return d
defprint_map(d: Dungeon):
GLYPH = {
TILE_WALL: '█',
TILE_FLOOR: '·',
TILE_STAIR_DN: '>',
TILE_STAIR_UP: '<',
}
for r in range(d.h):
row_str = []
for c in range(d.w):
t = d.tile_type[d.idx(r, c)]
f = d.tile_flags[d.idx(r, c)]
if t == TILE_STAIR_DN: row_str.append('>')
elif t == TILE_STAIR_UP: row_str.append('<')
elif f & FLAG_DOOR: row_str.append('+')
elif f & FLAG_ROOM: row_str.append('.')
elif f & FLAG_CORR: row_str.append('#')
else: row_str.append(' ')
print(''.join(row_str))
if __name__ == '__main__':
dungeon = generate(level=5, max_rooms=12, corr_chance=70, seed=42)
print_map(dungeon)
Requires Python 3.10+. Run with python cotw_dungeon.py — or generate(seed=None) for a random dungeon each time.