This commit is contained in:
PenguinRobert1
2023-12-16 00:37:58 +00:00
parent cfdbf28555
commit f26ceb5453
41 changed files with 54209 additions and 1 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
<a href="/8-bit/index.html">8-bit</a>
<a href="/space/space/SpaceInvaders-master/index.html">spaceinvaders</a>
<a href="roberts games/clash-royale-api-master/robertw games/public/index.html">clash Royal</a>
<a href="">pac man</a>
<a href="pac man/pacman-mazegen-gh-pages/pacman-mazegen-gh-pages/index.htm">pac man</a>
<body><input type = "button" onclick = "EventHandler();" name = "ok" value = "fanf2"/></body>
<p>miles is not outing for 2 days this will get updated every day</p>
</html>
Binary file not shown.
@@ -0,0 +1,21 @@
# Pac-Man Maze Generator
Generating random mazes aesthetically similar to those found in Pac-Man and Ms. Pac-Man.
The [tetris](tetris) solution yielded best results:
<img src="tetris/working.png" />
<a href="http://shaunlebron.github.com/pacman-mazegen">Click here for an explanation.</a>
## History
Research conducted summer 2012.
- The [randomfill](randomfill) folder contains a Python solution that uses a special
heuristic for progressively placing random valid walls.
- The [answerset](answerset) folder contains a
[Clingo](http://potassco.sourceforge.net/#clingo) solution that specifies
declarative constraints. This [blog post](http://eis-blog.ucsc.edu/2011/10/map-generation-speedrun/)
is a good introduction to map generation using Clingo.
- The [spanningtree](spanningtree) folder is a lead to apply a modified version of conventional maze generation algorithms.
- The [tetris](tetris) folder contains a solution for stacking pieces in a tetris like manner.
@@ -0,0 +1,31 @@
import sys,re
def print_map(line):
walls = {}
blank = {}
xdim = {}
ydim = {}
for m in re.finditer('wall\((\d+),(\d+)\)', line):
walls[(int(m.group(1)),int(m.group(2)))] = True
for m in re.finditer('blank\((\d+),(\d+)\)', line):
blank[(int(m.group(1)),int(m.group(2)))] = True
for m in re.finditer('xdim\((\d+)\)', line):
xdim[int(m.group(1))] = True
for m in re.finditer('ydim\((\d+)\)', line):
ydim[int(m.group(1))] = True
for y in range(max(ydim.keys())):
for x in range(max(xdim.keys())):
if (x+1,y+1) in walls:
sys.stdout.write('|')
elif (x+1,y+1) in blank:
sys.stdout.write('_')
else:
sys.stdout.write('.')
print ''
if __name__ == "__main__":
lines = sys.stdin.readlines()
for i,line in enumerate(lines):
if line.startswith("Answer:"):
print_map(lines[i+1])
print ""
@@ -0,0 +1,320 @@
||||||||||||||||||||||||||||
|....||..............||....|
|.||.||.||||||||||||.||.||.|
|.||.||.||||||||||||.||.||.|
|.||......||....||......||.|
|.|||||||.||.||.||.|||||||.|
|.|||||||.||.||.||.|||||||.|
|......||....||....||......|
|||.||.||.||.||.||.||.||.|||
|||.||....||.||.||....||.|||
|...||.||.||.||.||.||.||...|
|.||||.||..........||.||||.|
|.||||.||.||||||||.||.||||.|
|...||....|______|....||...|
|||.|||||.|______|.|||||.|||
|||.|||||.|______|.|||||.|||
|......||.||||||||.||......|
|.||||.||..........||.||||.|
|.|__|.||.||.||.||.||.|__|.|
|.||||.||.||.||.||.||.||||.|
|.........||.||.||.........|
||||.|||||||.||.|||||||.||||
||||.|||||||.||.|||||||.||||
........||........||........
||||.||.||.||||||.||.||.||||
___|.||.||.||||||.||.||.|___
___|.||..............||.|___
___|.|||||||.||.|||||||.|___
||||.|||||||.||.|||||||.||||
.............||.............
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
.....||..............||.....
||||.||.||||||||||||.||.||||
___|.||.||||||||||||.||.|___
___|....||........||....|___
___|.||.||.||||||.||.||.|___
||||.||....|____|....||.||||
.....||.||.||||||.||.||.....
|.||.||.||........||.||.||.|
|.||.||.||||||||||||.||.||.|
|.||.||.||||||||||||.||.||.|
|..........................|
|.|||||||.||||||||.|||||||.|
|.|||||||.|______|.|||||||.|
|.||......|______|......||.|
|.||.||||.|______|.||||.||.|
|....||||.||||||||.||||....|
|||||||..............|||||||
|||||||.||||||||||||.|||||||
|.......||||||||||||.......|
|.|||.||||........||||.|||.|
|.|||.||||.||||||.||||.|||.|
|.......||.||||||.||.......|
|||||||.||........||.|||||||
|||||||.||.||||||.||.|||||||
|....||.||.||||||.||.||....|
|.||.||.||........||.||.||.|
|.||....||.||||||.||....||.|
|.||.||.||.||||||.||.||.||.|
|....||..............||....|
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|.........||....||.........|
|.||.||||.||.||.||.||||.||.|
|.||.||||....||....||||.||.|
|.||...||.||||||||.||...||.|
|.||||.||.||||||||.||.||||.|
|.||||.||.||....||.||.||||.|
|.........||.||.||.........|
||||.||||.||.||.||.||||.||||
||||.|__|.||.||.||.|__|.||||
..||.||||.||.||.||.||||.||..
|.||....................||.|
|.||.||||.||||||||.||||.||.|
|.||.||||.|______|.||||.||.|
|.........|______|.........|
|||||||||.|______|.|||||||||
|||||||||.||||||||.|||||||||
..||....................||..
|.||.||.||||.||.||||.||.||.|
|.||.||.||||.||.||||.||.||.|
|....||...||.||.||...||....|
|||||||||.||.||.||.|||||||||
|||||||||.||.||.||.|||||||||
|.........||....||.........|
|.|||||.||||.||.||||.|||||.|
|.|___|.||||.||.||||.|___|.|
|.|___|......||......|___|.|
|.|___|.||||||||||||.|___|.|
|.|||||.||||||||||||.|||||.|
|..........................|
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
||.......||||||||||.......||
||.||.||.||......||.||.||.||
||.||.||.||.||||.||.||.||.||
||.||.||.||.||||.||.||.||.||
||....||............||....||
|||||.||.||||||||||.||.|||||
|||||.||.||||||||||.||.|||||
||....||............||....||
||.||.|||||.||||.|||||.||.||
||.||.|||||.||||.|||||.||.||
||.||..................||.||
||.||.|||.||||||||.|||.||.||
||.||.|||.||||||||.|||.||.||
||.||.....||||||||.....||.||
||.||.|||.||||||||.|||.||.||
||.||.|||.||||||||.|||.||.||
||.||..................||.||
||.||.||||||.||.||||||.||.||
||....||||||.||.||||||....||
|||||.||.....||.....||.|||||
|||||.||.||||||||||.||.|||||
||....||.||||||||||.||....||
||.||.||.||......||.||.||.||
||.||....||.||||.||....||.||
||.||.||.||.||||.||.||.||.||
||.||.||............||.||.||
||.||.|||||.||||.|||||.||.||
||.||.|||||.||||.|||||.||.||
||........................||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|.......||........||.......|
|.||.||.||.||||||.||.||.||.|
|.||.||.||.||||||.||.||.||.|
|.||.||.||........||.||.||.|
|.||.||.||.||||||.||.||.||.|
|.||.||.||.||||||.||.||.||.|
|.......||...||...||.......|
|.||||||||||.||.||||||||||.|
|.||||||||||.||.||||||||||.|
|..........................|
|||||.|||.||||||||.|||.|||||
|||||.|||.||||||||.|||.|||||
|.........||||||||.........|
|.|||||||.||||||||.|||||||.|
|.|||||||.||||||||.|||||||.|
|.||....................||.|
|.||.||.||||.||.||||.||.||.|
|....||.||||.||.||||.||....|
|.||.||.||...||...||.||.||.|
|.||.||.||.||||||.||.||.||.|
|.||.||.||.||||||.||.||.||.|
|.......||........||.......|
||||.|||||.||||||.|||||.||||
||||.|||||.||||||.|||||.||||
||...........||...........||
||.|||||||||.||.|||||||||.||
||.|||||||||.||.|||||||||.||
||...........||...........||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|.........||....||.........|
|.||||.||.||.||.||.||.||||.|
|.||||.||....||....||.||||.|
|......||.||||||||.||......|
|.||||.||.||||||||.||.||||.|
|.||||.......||.......||||.|
|...|||||.||.||.||.|||||...|
|||.|||||.||....||.|||||.|||
|||.||....||||||||....||.|||
|...||.||.||||||||.||.||...|
|.||||.||..........||.||||.|
|.||||.||.||||||||.||.||||.|
|.........||||||||.........|
|.||||.||.||||||||.||.||||.|
|.||||.||.||||||||.||.||||.|
|.||...||.||||||||.||...||.|
|.||.||||..........||||.||.|
|.||.||||.||.||.||.||||.||.|
|.........||.||.||.........|
||||.||||.||.||.||.||||.||||
||||.||||.||.||.||.||||.||||
|.........||.||.||.........|
|.|||||.||||....||||.|||||.|
|.|||||.||||.||.||||.|||||.|
|............||............|
|.||.||.||||.||.||||.||.||.|
|.||.||.||||.||.||||.||.||.|
|.||.||..............||.||.|
|....||||||||||||||||||....|
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|............||............|
|.||.||.||||.||.||||.||.||.|
|.||.||.||||.||.||||.||.||.|
|.||.||..............||.||.|
|....||||.||||||||.||||....|
|.||.||||.||||||||.||||.||.|
|.||...||..........||...||.|
|.||||.||.||.||.||.||.||||.|
|.||||.||.||.||.||.||.||||.|
|......||.||.||.||.||......|
|||.||.||..........||.||.|||
|||.||.||.||||||||.||.||.|||
|...||.||.||||||||.||.||...|
|.||||....||||||||....||||.|
|.||||.||.||||||||.||.||||.|
|...||.||.||||||||.||.||...|
|||.||.||..........||.||.|||
|||.||.||.||||||||.||.||.|||
|......||.||||||||.||......|
|.|||||||..........|||||||.|
|.|||||||.||||||||.|||||||.|
|.........||||||||.........|
|||||||.||||....||||.|||||||
|||||||.||||.||.||||.|||||||
||...........||...........||
||.||||.||.||||||.||.||||.||
||.||||.||.||||||.||.||||.||
||......||........||......||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|..........................|
|.|||.|||.||||||||.|||.|||.|
|.|||.|||.||||||||.|||.|||.|
|..........................|
|.|||||.||||||||||||.|||||.|
|.|||||.||||||||||||.|||||.|
|....||..............||....|
|.||.||||.||.||.||.||||.||.|
|.||.||||.||.||.||.||||.||.|
|.||...||.||.||.||.||...||.|
|.||||.||..........||.||||.|
|.||||.||.||||||||.||.||||.|
|.........||||||||.........|
|.||.||||.||||||||.||||.||.|
|.||.||||.||||||||.||||.||.|
|.||...||.||||||||.||...||.|
|.||||.||..........||.||||.|
|.||||.||.||.||.||.||.||||.|
|......||.||.||.||.||......|
|.||||.||.||.||.||.||.||||.|
|.||||....||.||.||....||||.|
|...|||||.||.||.||.|||||...|
|||.|||||.||....||.|||||.|||
|||.||....||||||||....||.|||
|...||.||.||||||||.||.||...|
|.||||.||....||....||.||||.|
|.||||.||.||.||.||.||.||||.|
|.........||.||.||.........|
||||||||||||....||||||||||||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|....||......||......||....|
|.||.||.||||.||.||||.||.||.|
|.||.||.||||.||.||||.||.||.|
|.||.||.||........||.||.||.|
|.......||.||||||.||.......|
|.||.|||||.||||||.|||||.||.|
|.||.|||||...||...|||||.||.|
|.||....||||.||.||||....||.|
|.||.||.||||.||.||||.||.||.|
|.||.||..............||.||.|
|.||.||||.||||||||.||||.||.|
|.||.||||.||||||||.||||.||.|
|.........||||||||.........|
|.||.||||.||||||||.||||.||.|
|.||.||||.||||||||.||||.||.|
|.||....................||.|
|.||||.||||.||||.||||.||||.|
|.||||.||||.||||.||||.||||.|
|........||......||........|
|||.||||.||.||||.||.||||.|||
|||.||||.||.||||.||.||||.|||
|.....||............||.....|
|.|||.|||||.||||.|||||.|||.|
|.|||.|||||.||||.|||||.|||.|
|..........................|
|.|||||||||.||||.|||||||||.|
|.|||||||||.||||.|||||||||.|
|..........................|
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
||||||||||||||||||||||||||||
|..........................|
|.||||.||||||||||||||.||||.|
|.||||.||||||||||||||.||||.|
|...||.......||.......||...|
|||.|||||.||.||.||.|||||.|||
|||.|||||.||.||.||.|||||.|||
|.........||....||.........|
|.|||.|||.||||||||.|||.|||.|
|.|||.|||.||||||||.|||.|||.|
|..........................|
|||||.|||.||||||||.|||.|||||
|||||.|||.||||||||.|||.|||||
||........||||||||........||
||.||.|||.||||||||.|||.||.||
||.||.|||.||||||||.|||.||.||
||.||..................||.||
||.||||||.||||||||.||||||.||
||.||||||.||||||||.||||||.||
||........................||
||.||||||.||||||||.||||||.||
||.||||||.||||||||.||||||.||
||...||..............||...||
||||.||.||||.||.||||.||.||||
||||.||.||||.||.||||.||.||||
|.......||...||...||.......|
|.||||||||.||||||.||||||||.|
|.||||||||.||||||.||||||||.|
|..........................|
||||||||||||||||||||||||||||
@@ -0,0 +1,3 @@
#/bin/bash
clingo --rand-prob --seed=$RANDOM rules.lp | python draw.py
@@ -0,0 +1,168 @@
#const width=28.
#const height=31.
#const homeleft=11.
#const homeright=18.
#const hometop=13.
#const homebottom=17.
#const pacy=24.
#const pacx1=13.
#const pacx2=16.
xdim(1..width).
ydim(1..height).
step(1,0;;0,1;;-1,0;;0,-1).
diag(-1,-1;;1,1;;1,-1;;-1,1).
surround(DX,DY) :- step(DX,DY).
surround(DX,DY) :- diag(DX,DY).
% choose arbitrary walls
% make sure there are less than 520 wall tiles
% (maps should have around 310 walkable tiles)
{ wall(X,Y) :xdim(X) :ydim(Y) } 520.
% choose no more than 40 blanks
{ blank(X,Y) :xdim(X) :ydim(Y) } 40.
% prevent skinny blanks
:- blank(X,Y), wall(X+1,Y), wall(X-1,Y).
:- blank(X,Y), wall(X,Y+1), wall(X,Y-1).
% prevent blanks from touching paths
:- blank(X,Y), path(X+DX,Y+DY), surround(DX,DY).
% prevent walls from touching more than one blank by step
%:- wall(X,Y), 2 { blank(X+DX,Y+DY) :step(DX,DY) } 2.
:- wall(X,Y), blank(X+1,Y), blank(X,Y+1), blank(X+1,Y+1).
:- blank(X,Y), wall(X+1,Y), blank(X,Y+1), blank(X+1,Y+1).
:- blank(X,Y), blank(X+1,Y), wall(X,Y+1), blank(X+1,Y+1).
:- blank(X,Y), blank(X+1,Y), blank(X,Y+1), wall(X+1,Y+1).
% prevent padding blanks with more than one wall
:- blank(X,Y), wall(X+1,Y), wall(X+2,Y).
:- wall(X,Y), wall(X+1,Y), blank(X+2,Y).
:- blank(X,Y), wall(X,Y+1), wall(X,Y+2).
:- wall(X,Y), wall(X,Y+1), blank(X,Y+2).
% prevent a blank block from being adjacent to the edge of the map
:- blank(X,Y), wall(X+1,Y), not xdim(X+2).
:- blank(X,Y), wall(X-1,Y), not xdim(X-2).
:- blank(X,Y), wall(X,Y+1), not ydim(Y+2).
:- blank(X,Y), wall(X,Y-1), not ydim(Y-2).
% define paths as tiles that are not walls or blanks
path(X,Y) :- not wall(X,Y), not blank(X,Y), xdim(X), ydim(Y).
% prevent paths on top and bottom
:- path(X,1), xdim(X).
:- path(X,height), xdim(X).
% create 0 to 2 tunnels
{ path(1,Y) :ydim(Y) } 2.
:- path(1,Y), path(1,Y-1).
:- path(1,Y), path(1,Y+1).
% define ghost home
wall(homeleft..homeright,hometop).
wall(homeleft..homeright,homebottom).
wall(homeleft,hometop..homebottom).
wall(homeright,hometop..homebottom).
blank(homeleft+1..homeright-1,hometop+1..homebottom-1).
path(homeleft-1,hometop-1..homebottom+1).
path(homeright+1,hometop-1..homebottom+1).
path(homeleft-1..homeright+1,hometop-1).
path(homeleft-1..homeright+1,homebottom+1).
% define pacman start
path(pacx1..pacx2,pacy).
% ensure all paths are reachable
reachable(pacx1,pacy).
reachable(X,Y) :- path(X,Y), reachable(X+DX,Y+DY), step(DX,DY).
:- path(X,Y), not reachable(X,Y).
% prevent fat paths:
:- path(X,Y), path(X,Y+1), path(X+1,Y), path(X+1,Y+1).
% prevent this
% |.
% .|
% and
% .|
% |.
:- wall(X,Y), wall(X+1,Y+1), path(X+1,Y), path(X,Y+1).
:- path(X,Y), path(X+1,Y+1), wall(X+1,Y), wall(X,Y+1).
% prevent thin walls
:- wall(X,Y), path(X+1,Y), path(X-1,Y).
:- wall(X,Y), path(X,Y+1), path(X,Y-1).
% prevent this
% |.
% .|
:- wall(X,Y), wall(X,Y+1), path(X+1,Y), path(X-1,Y+1).
:- wall(X,Y), wall(X,Y+1), path(X-1,Y), path(X+1,Y+1).
% .
% ||
% .
:- wall(X,Y), wall(X+1,Y), path(X,Y-1), path(X+1,Y+1).
:- wall(X,Y), wall(X+1,Y), path(X+1,Y-1), path(X,Y+1).
% prevent 2x2 wall island
:- path(X,Y), path(X+1,Y), path(X+2,Y), path(X+3,Y),
path(X,Y+1), path(X+3,Y+1),
path(X,Y+2), path(X+3,Y+2),
path(X,Y+3), path(X+1,Y+3), path(X+2,Y+3), path(X+3,Y+3).
% prevent 3x2 wall island
:- path(X,Y), path(X+1,Y), path(X+2,Y), path(X+3,Y),
path(X,Y+1), path(X+3,Y+1),
path(X,Y+2), path(X+3,Y+2),
path(X,Y+3), path(X+3,Y+3),
path(X,Y+4), path(X+1,Y+4), path(X+2,Y+4), path(X+3,Y+4).
% prevent 3x2 wall island
:- path(X,Y), path(X,Y+1), path(X,Y+2), path(X,Y+3),
path(X+1,Y), path(X+1,Y+3),
path(X+2,Y), path(X+2,Y+3),
path(X+3,Y), path(X+3,Y+3),
path(X+4,Y), path(X+4,Y+1), path(X+4,Y+2), path(X+4,Y+3).
% TODO: limit number of contiguous pieces (~20)
% TODO: limit size of a contiguous piece
% prevent 3x3 wall
:- wall(X,Y), wall(X+1,Y), wall(X+2,Y),
wall(X,Y+1), wall(X+1,Y+1), wall(X+2,Y+1),
wall(X,Y+2), wall(X+1,Y+2), wall(X+2,Y+2).
% prevent this:
% .| |.
% ||| |||
% |. .|
:- path(X,Y), wall(X+1,Y), wall(X,Y+1), wall(X+1,Y+1), wall(X+2,Y+1), wall(X+1,Y+2), path(X+2,Y+2).
:- wall(X+1,Y), path(X+2,Y), wall(X,Y+1), wall(X+1,Y+1), wall(X+2,Y+1), path(X,Y+2), wall(X+1,Y+2).
% prevent dead end paths
:- path(X,Y), path(X,Y+1), wall(X-1,Y), wall(X+1,Y), wall(X,Y-1).
:- path(X,Y), path(X,Y-1), wall(X-1,Y), wall(X+1,Y), wall(X,Y+1).
:- path(X,Y), path(X-1,Y), wall(X,Y+1), wall(X,Y-1), wall(X+1,Y).
:- path(X,Y), path(X+1,Y), wall(X,Y+1), wall(X,Y-1), wall(X-1,Y).
:- path(X,Y), wall(X+1,Y), wall(X,Y+1), wall(X,Y-1), wall(X-1,Y).
% prevent tight corners
:- path(X,Y), path(X+1,Y), path(X+1,Y+1), path(X+2,Y+1).
:- path(X,Y), path(X-1,Y), path(X-1,Y+1), path(X-2,Y+1).
:- path(X,Y), path(X,Y+1), path(X+1,Y+1), path(X+1,Y+2).
:- path(X,Y), path(X,Y+1), path(X-1,Y+1), path(X-1,Y+2).
% prevent horizontal asymmetry
:- wall(X,Y), not wall(width-X+1,Y).
:- blank(X,Y), not blank(width-X+1,Y).
% prevent thick outer wall
:- wall(2,Y), path(3,Y), ydim(Y).
:- wall(width-1,Y), path(width-2,Y), ydim(Y).
:- wall(X,2), path(X,3), xdim(X).
:- wall(X,height-1), path(X,height-2), xdim(X).
@@ -0,0 +1,138 @@
#const width=28.
#const height=31.
#const homeleft=11.
#const homeright=18.
#const hometop=13.
#const homebottom=17.
#const pacy=24.
#const pacx1=13.
#const pacx2=16.
xdim(1..width).
ydim(1..height).
step(-1;1,0;;0,-1;1).
diag(-1;1,-1;1).
surround(DX,DY) :- step(DX,DY).
surround(DX,DY) :- diag(DX,DY).
% choose arbitrary walls
% make sure there are less than 520 wall tiles
% (maps should have around 310 walkable tiles)
{ wall(X,Y) :xdim(X) :ydim(Y) } 520.
% choose no more than 40 blanks
{ blank(X,Y) :xdim(X) :ydim(Y) } 40.
% prevent long walls
:- wall(X+DX,Y), DX:=1..8, Y>1, Y<height.
:- wall(X,Y+DY), DY:=1..8, X>1, X<width.
% prevent blanks from touching paths
:- blank(X,Y), path(X+DX,Y+DY), surround(DX,DY).
% prevent walls from touching more than one blank
:- wall(X,Y), 2 { blank(X+DX,Y+DY) :step(DX,DY) } 2.
% prevent padding blanks with more than one wall
:- blank(X,Y), wall(X+DX,Y+DY), wall(X+2*DX,Y+2*DY), step(DX,DY).
% prevent a blank block from being adjacent to the edge of the map
:- blank(X,Y), wall(X+DX,Y+DY), not xdim(X+2*DX), not ydim(Y+2*DY), step(DX,DY).
%:- blank(X,Y), wall(X+1,Y), not xdim(X+2).
%:- blank(X,Y), wall(X-1,Y), not xdim(X-2).
%:- blank(X,Y), wall(X,Y+1), not ydim(Y+2).
%:- blank(X,Y), wall(X,Y-1), not ydim(Y-2).
% define paths as tiles that are not walls or blanks
path(X,Y) :- not wall(X,Y), not blank(X,Y), xdim(X), ydim(Y).
% prevent paths on top and bottom
:- path(X,1), xdim(X).
:- path(X,height), xdim(X).
% create 0 to 2 tunnels
{ path(1,Y) :ydim(Y) } 2.
:- path(1,Y), path(1,Y-1).
:- path(1,Y), path(1,Y+1).
% define ghost home
wall(homeleft..homeright,hometop;homebottom).
wall(homeleft;homeright,hometop..homebottom).
blank(homeleft+1..homeright-1,hometop+1..homebottom-1).
path(homeleft-1;homeright+1,hometop-1..homebottom+1).
path(homeleft-1..homeright+1,hometop-1;homebottom+1).
% define pacman start
path(pacx1..pacx2,pacy).
% ensure all paths are reachable
reachable(pacx1,pacy).
reachable(X,Y) :- path(X,Y), reachable(X+DX,Y+DY), step(DX,DY).
:- path(X,Y), not reachable(X,Y).
% prevent fat paths:
:- path(X,Y), path(X,Y+1), path(X+1,Y), path(X+1,Y+1).
% prevent this
% |.
% .|
% and
% .|
% |.
:- wall(X,Y), wall(X+1,Y+1), path(X+1,Y), path(X,Y+1).
:- path(X,Y), path(X+1,Y+1), wall(X+1,Y), wall(X,Y+1).
% prevent thin walls
:- wall(X,Y), path(X+1,Y), path(X-1,Y).
:- wall(X,Y), path(X,Y+1), path(X,Y-1).
% prevent this
% |.
% .|
:- wall(X,Y), wall(X,Y+1), path(X+1,Y), path(X-1,Y+1).
:- wall(X,Y), wall(X,Y+1), path(X-1,Y), path(X+1,Y+1).
% .
% ||
% .
:- wall(X,Y), wall(X+1,Y), path(X,Y-1), path(X+1,Y+1).
:- wall(X,Y), wall(X+1,Y), path(X+1,Y-1), path(X,Y+1).
% prevent 2x2 wall island
:- path(X,Y), path(X+1,Y), path(X+2,Y), path(X+3,Y),
path(X,Y+1), path(X+3,Y+1),
path(X,Y+2), path(X+3,Y+2),
path(X,Y+3), path(X+1,Y+3), path(X+2,Y+3), path(X+3,Y+3).
% TODO: limit number of contiguous pieces (~20)
% TODO: limit size of a contiguous piece
% prevent 3x3 wall
:- wall(X,Y), 9 { wall(X+DX,Y+DY) :DX=0..2 :DY=0..2 } 9.
% prevent this:
% .| |.
% ||| |||
% |. .|
:- path(X,Y), wall(X+1,Y), wall(X,Y+1), wall(X+1,Y+1), wall(X+2,Y+1), wall(X+1,Y+2), path(X+2,Y+2).
:- wall(X+1,Y), path(X+2,Y), wall(X,Y+1), wall(X+1,Y+1), wall(X+2,Y+1), path(X,Y+2), wall(X+1,Y+2).
% prevent dead-ends
:- path(X,Y), 3 { wall(X+DX,Y+DY) :step(DX,DY) } 4.
% prevent tight corners
:- path(X,Y), path(X+1,Y), path(X+1,Y+1), path(X+2,Y+1).
:- path(X,Y), path(X-1,Y), path(X-1,Y+1), path(X-2,Y+1).
:- path(X,Y), path(X,Y+1), path(X+1,Y+1), path(X+1,Y+2).
:- path(X,Y), path(X,Y+1), path(X-1,Y+1), path(X-1,Y+2).
% prevent horizontal asymmetry
:- wall(X,Y), not wall(width-X+1,Y).
:- blank(X,Y), not blank(width-X+1,Y).
% prevent thick outer wall
:- wall(2,Y), path(3,Y), ydim(Y).
:- wall(width-1,Y), path(width-2,Y), ydim(Y).
:- wall(X,2), path(X,3), xdim(X).
:- wall(X,height-1), path(X,height-2), xdim(X).
@@ -0,0 +1,386 @@
// direction enums (in clockwise order)
var DIR_UP = 0;
var DIR_RIGHT = 1;
var DIR_DOWN = 2;
var DIR_LEFT = 3;
// get direction enum from a direction vector
var getEnumFromDir = function(dir) {
if (dir.x==-1) return DIR_LEFT;
if (dir.x==1) return DIR_RIGHT;
if (dir.y==-1) return DIR_UP;
if (dir.y==1) return DIR_DOWN;
};
// set direction vector from a direction enum
var setDirFromEnum = function(dir,dirEnum) {
if (dirEnum == DIR_UP) { dir.x = 0; dir.y =-1; }
else if (dirEnum == DIR_RIGHT) { dir.x =1; dir.y = 0; }
else if (dirEnum == DIR_DOWN) { dir.x = 0; dir.y = 1; }
else if (dirEnum == DIR_LEFT) { dir.x = -1; dir.y = 0; }
};
// size of a square tile in pixels
var tileSize = 8;
// constructor
var Map = function(numCols, numRows, tiles) {
// sizes
this.numCols = numCols;
this.numRows = numRows;
this.numTiles = numCols*numRows;
this.widthPixels = numCols*tileSize;
this.heightPixels = numRows*tileSize;
// ascii map
this.tiles = tiles;
this.resetCurrent();
this.parseWalls();
};
// reset current tiles
Map.prototype.resetCurrent = function() {
this.currentTiles = this.tiles.split(""); // create a mutable list copy of an immutable string
};
// This is a procedural way to generate original-looking maps from a simple ascii tile
// map without a spritesheet.
Map.prototype.parseWalls = function() {
var that = this;
// creates a list of drawable canvas paths to render the map walls
this.paths = [];
// a map of wall tiles that already belong to a built path
var visited = {};
// we extend the x range to suggest the continuation of the tunnels
var toIndex = function(x,y) {
if (x>=-2 && x<that.numCols+2 && y>=0 && y<that.numRows)
return (x+2)+y*(that.numCols+4);
};
// a map of which wall tiles that are not completely surrounded by other wall tiles
var edges = {};
var i=0,x,y;
for (y=0;y<this.numRows;y++) {
for (x=-2;x<this.numCols+2;x++,i++) {
if (this.getTile(x,y) == '|' &&
(this.getTile(x-1,y) != '|' ||
this.getTile(x+1,y) != '|' ||
this.getTile(x,y-1) != '|' ||
this.getTile(x,y+1) != '|' ||
this.getTile(x-1,y-1) != '|' ||
this.getTile(x-1,y+1) != '|' ||
this.getTile(x+1,y-1) != '|' ||
this.getTile(x+1,y+1) != '|')) {
edges[i] = true;
}
}
}
// walks along edge wall tiles starting at the given index to build a canvas path
var makePath = function(tx,ty) {
// get initial direction
var dir = {};
var dirEnum;
if (toIndex(tx+1,ty) in edges)
dirEnum = DIR_RIGHT;
else if (toIndex(tx, ty+1) in edges)
dirEnum = DIR_DOWN;
else
throw "tile shouldn't be 1x1 at "+tx+","+ty;
setDirFromEnum(dir,dirEnum);
// increment to next tile
tx += dir.x;
ty += dir.y;
// backup initial location and direction
var init_tx = tx;
var init_ty = ty;
var init_dirEnum = dirEnum;
var path = [];
var pad; // (persists for each call to getStartPoint)
var point;
var lastPoint;
var turn,turnAround;
/*
We employ the 'right-hand rule' by keeping our right hand in contact
with the wall to outline an individual wall piece.
Since we parse the tiles in row major order, we will always start
walking along the wall at the leftmost tile of its topmost row. We
then proceed walking to the right.
When facing the direction of the walk at each tile, the outline will
hug the left side of the tile unless there is a walkable tile to the
left. In that case, there will be a padding distance applied.
*/
var getStartPoint = function(tx,ty,dirEnum) {
var dir = {};
setDirFromEnum(dir, dirEnum);
if (!(toIndex(tx+dir.y,ty-dir.x) in edges))
pad = that.isFloorTile(tx+dir.y,ty-dir.x) ? 5 : 0;
var px = -tileSize/2+pad;
var py = tileSize/2;
var a = dirEnum*Math.PI/2;
var c = Math.cos(a);
var s = Math.sin(a);
return {
// the first expression is the rotated point centered at origin
// the second expression is to translate it to the tile
x:(px*c - py*s) + (tx+0.5)*tileSize,
y:(px*s + py*c) + (ty+0.5)*tileSize,
};
};
while (true) {
visited[toIndex(tx,ty)] = true;
// determine start point
point = getStartPoint(tx,ty,dirEnum);
if (turn) {
// if we're turning into this tile, create a control point for the curve
//
// >---+ <- control point
// |
// V
lastPoint = path[path.length-1];
if (dir.x == 0) {
point.cx = point.x;
point.cy = lastPoint.y;
}
else {
point.cx = lastPoint.x;
point.cy = point.y;
}
}
// update direction
turn = false;
turnAround = false;
if (toIndex(tx+dir.y, ty-dir.x) in edges) { // turn left
dirEnum = (dirEnum+3)%4;
turn = true;
}
else if (toIndex(tx+dir.x, ty+dir.y) in edges) { // continue straight
}
else if (toIndex(tx-dir.y, ty+dir.x) in edges) { // turn right
dirEnum = (dirEnum+1)%4;
turn = true;
}
else { // turn around
dirEnum = (dirEnum+2)%4;
turnAround = true;
}
setDirFromEnum(dir,dirEnum);
// commit path point
path.push(point);
// special case for turning around (have to connect more dots manually)
if (turnAround) {
path.push(getStartPoint(tx-dir.x, ty-dir.y, (dirEnum+2)%4));
path.push(getStartPoint(tx, ty, dirEnum));
}
// advance to the next wall
tx += dir.x;
ty += dir.y;
// exit at full cycle
if (tx==init_tx && ty==init_ty && dirEnum == init_dirEnum) {
that.paths.push(path);
break;
}
}
};
// iterate through all edges, making a new path after hitting an unvisited wall edge
i=0;
for (y=0;y<this.numRows;y++)
for (x=-2;x<this.numCols+2;x++,i++)
if (i in edges && !(i in visited)) {
visited[i] = true;
makePath(x,y);
}
};
Map.prototype.posToIndex = function(x,y) {
if (x>=0 && x<this.numCols && y>=0 && y<this.numRows)
return x+y*this.numCols;
};
// retrieves tile character at given coordinate
// extended to include offscreen tunnel space
Map.prototype.getTile = function(x,y) {
if (x>=0 && x<this.numCols && y>=0 && y<this.numRows)
return this.currentTiles[this.posToIndex(x,y)];
// extend walls and paths outward for entrances and exits
if ((x==-1 && this.getTile(x+1,y)=='|' && (this.isFloorTile(x+1,y+1)||this.isFloorTile(x+1,y-1))) ||
(x==this.numCols && this.getTile(x-1,y)=='|' && (this.isFloorTile(x-1,y+1)||this.isFloorTile(x-1,y-1))))
return '|';
if ((x==-1 && this.isFloorTile(x+1,y)) ||
(x==this.numCols && this.isFloorTile(x-1,y)))
return ' ';
};
// determines if the given character is a walkable floor tile
Map.prototype.isFloorTileChar = function(tile) {
return tile==' ' || tile=='.' || tile=='o';
};
// determines if the given tile coordinate has a walkable floor tile
Map.prototype.isFloorTile = function(x,y) {
return this.isFloorTileChar(this.getTile(x,y));
};
// function to draw the map as a tile map
Map.prototype.draw = function(ctx,left,top,print) {
// save state
ctx.save();
ctx.translate(0.5,0.5); // pixel perfect lines?
// translate to the position of the map
ctx.translate(left,top);
// clip the drawing surface
ctx.beginPath();
ctx.rect(0,0,this.widthPixels, this.heightPixels);
ctx.clip();
// set colors
ctx.fillStyle = print?"#333":this.wallFillColor;
ctx.strokeStyle = print?"#333":this.wallStrokeColor;
var x,y;
var i,j;
var tile;
for (i=0; i<this.paths.length; i++) {
var path = this.paths[i];
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (j=1; j<path.length; j++) {
if (path[j].cx != undefined)
ctx.quadraticCurveTo(path[j].cx, path[j].cy, path[j].x, path[j].y);
else
ctx.lineTo(path[j].x, path[j].y);
}
ctx.quadraticCurveTo(path[j-1].x, path[0].y, path[0].x, path[0].y);
ctx.fill();
ctx.stroke();
}
// draw pellets for each path tile
ctx.fillStyle=print?"#bbb":this.pelletColor;
var pelletSize = print?tileSize:2;
for (y=0; y<this.numRows; y++) {
for (x=0; x<this.numCols; x++) {
if (this.isFloorTile(x,y)) {
ctx.fillRect(
x*tileSize+tileSize/2-pelletSize/2,
y*tileSize+tileSize/2-pelletSize/2,
pelletSize,pelletSize);
}
}
}
// draw grid
ctx.strokeStyle=print?"rgba(0,0,0,0.3)":"rgba(255,255,255,0.3)";
ctx.beginPath();
for (y=0; y<=this.numRows; y++) {
ctx.moveTo(0,y*tileSize);
ctx.lineTo(this.widthPixels,y*tileSize);
}
for (x=0; x<=this.numCols; x++) {
ctx.moveTo(x*tileSize,0);
ctx.lineTo(x*tileSize,this.heightPixels);
}
ctx.stroke();
// draw title
if (this.name) {
ctx.fillStyle = print?"#000":"#fff";
ctx.fillText(this.name, 0,tileSize/2);
}
ctx.restore();
};
// function to draw the map using simple representation of the paths as straight lines
Map.prototype.drawPath = function(ctx,left,top) {
var print = true;
// save state
ctx.save();
ctx.translate(0.5,0.5); // pixel perfect lines?
// translate to the position of the map
ctx.translate(left,top);
// clip the drawing surface
ctx.beginPath();
ctx.rect(0,0,this.widthPixels, this.heightPixels);
ctx.clip();
var x,y;
var i,j;
var tile;
// draw pellets for each path tile
ctx.lineWidth = 2.0;
ctx.strokeStyle="rgba(0,0,0,0.8)";
ctx.beginPath();
for (y=0; y<this.numRows-1; y++) {
for (x=0; x<this.numCols-1; x++) {
if (this.isFloorTile(x,y)) {
if (this.isFloorTile(x+1,y)) {
ctx.moveTo(x*tileSize,y*tileSize);
ctx.lineTo((x+1)*tileSize,y*tileSize);
}
if (this.isFloorTile(x,y+1)) {
ctx.moveTo(x*tileSize,y*tileSize);
ctx.lineTo(x*tileSize,(y+1)*tileSize);
}
}
}
}
ctx.stroke();
// draw grid
ctx.lineWidth = 1.0;
ctx.strokeStyle=print?"rgba(0,0,0,0.3)":"rgba(255,255,255,0.3)";
ctx.beginPath();
for (y=0; y<this.numRows; y++) {
ctx.moveTo(0,y*tileSize);
ctx.lineTo(this.widthPixels-tileSize,y*tileSize);
}
for (x=0; x<this.numCols; x++) {
ctx.moveTo(x*tileSize,0);
ctx.lineTo(x*tileSize,this.heightPixels-tileSize);
}
ctx.stroke();
// draw title
if (this.name) {
ctx.fillStyle = print?"#000":"#fff";
ctx.fillText(this.name, 0,tileSize/2);
}
ctx.restore();
};
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<!--
This was an attempt to illustrate why conventional maze generation routines
couldn't be used to generate Pac-Man maps, but in the process I've actually
found a lead that may suggest otherwise.
-->
<head>
<script src="Map.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
window.onload = function() {
var x,y;
var cols=4,rows=4;
var size = 10; // size of a tile in this spanning tree example
var scale = 4;
var canvas = document.getElementById('canvas');
canvas.width = (cols*4*size+3*size+size)*scale;
canvas.height = (rows*size+size)*scale;
var ctx = canvas.getContext('2d');
ctx.scale(scale,scale);
ctx.translate(0.5,0.5);
var drawGrid = function() {
ctx.beginPath();
for (y=0; y<=rows; y++) {
ctx.moveTo(0,y*size);
ctx.lineTo(cols*size,y*size);
}
for (x=0; x<=cols; x++) {
ctx.moveTo(x*size,0);
ctx.lineTo(x*size,rows*size);
}
ctx.stroke();
};
var drawMaze = function() {
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0,size);
ctx.moveTo(0,2*size);
ctx.lineTo(0,4*size);
ctx.lineTo(4*size,4*size);
ctx.lineTo(4*size,3*size);
ctx.moveTo(4*size,2*size);
ctx.lineTo(4*size,0);
ctx.lineTo(0,0);
ctx.moveTo(0,2*size);
ctx.lineTo(size,2*size);
ctx.lineTo(size,size);
ctx.lineTo(size,3*size);
ctx.lineTo(size*2,3*size);
ctx.moveTo(size*2,2*size);
ctx.lineTo(size*2,size);
ctx.lineTo(size*3,size);
ctx.lineTo(size*3,0);
ctx.moveTo(size*4,size*2);
ctx.lineTo(size*3,size*2);
ctx.lineTo(size*3,size*3);
ctx.stroke();
};
ctx.strokeStyle="rgba(0,0,0,0.2)";
drawGrid();
ctx.translate((cols+1)*size,0);
drawGrid();
ctx.strokeStyle="#000";
drawMaze();
ctx.translate((cols+1)*size,0);
drawMaze();
ctx.translate((cols+1)*size,0);
var map = new Map(12,12,
"||||||||||||" +
"|.......||.|" +
"|.||.|||||.|" +
"|.||.|||||.|" +
"..||.||....|" +
"||||.||.||||" +
"||||.||.||||" +
"|.||....||.." +
"|.|||||.||.|" +
"|.|||||.||.|" +
"|..........|" +
"||||||||||||");
var s = 1/(map.numRows*tileSize/(size*rows));
ctx.scale(s,s);
map.draw(ctx,0,0,print=true);
};
</script>
</body>
</html>
@@ -0,0 +1,321 @@
<!DOCTYPE html>
<html>
<head>
<script src="Map.js"></script>
</head>
<body>
<form>
<input id="chkPrint" type="checkbox" onchange="update();">
<label for="chkPrint">printer friendly</label>
</form>
<canvas id="canvas"></canvas>
<script>
var update = function() {
draw(scale=3.0,print=document.getElementById('chkPrint').checked);
};
window.onload = function() {
update();
};
var draw = function(scale,print) {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width=(28*tileSize*3+tileSize*2)*scale;
canvas.height=(36*tileSize*2+tileSize)*scale;
ctx.scale(scale,scale);
ctx.fillStyle = print?"#fff":"#000";
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.font = "bold " + 2*tileSize + "px sans-serif";
ctx.textBaseline = "top";
ctx.textAlign = "left";
var mapPacman = new Map(28, 36, (
"____________________________" +
"____________________________" +
"____________________________" +
"||||||||||||||||||||||||||||" +
"|............||............|" +
"|.||||.|||||.||.|||||.||||.|" +
"|o|__|.|___|.||.|___|.|__|o|" +
"|.||||.|||||.||.|||||.||||.|" +
"|..........................|" +
"|.||||.||.||||||||.||.||||.|" +
"|.||||.||.||||||||.||.||||.|" +
"|......||....||....||......|" +
"||||||.||||| || |||||.||||||" +
"_____|.||||| || |||||.|_____" +
"_____|.|| ||.|_____" +
"_____|.|| |||--||| ||.|_____" +
"||||||.|| |______| ||.||||||" +
" . |______| . " +
"||||||.|| |______| ||.||||||" +
"_____|.|| |||||||| ||.|_____" +
"_____|.|| ||.|_____" +
"_____|.|| |||||||| ||.|_____" +
"||||||.|| |||||||| ||.||||||" +
"|............||............|" +
"|.||||.|||||.||.|||||.||||.|" +
"|.||||.|||||.||.|||||.||||.|" +
"|o..||....... .......||..o|" +
"|||.||.||.||||||||.||.||.|||" +
"|||.||.||.||||||||.||.||.|||" +
"|......||....||....||......|" +
"|.||||||||||.||.||||||||||.|" +
"|.||||||||||.||.||||||||||.|" +
"|..........................|" +
"||||||||||||||||||||||||||||" +
"____________________________" +
"____________________________"));
mapPacman.name = "Pac-Man";
mapPacman.wallStrokeColor = "#47b897"; // from Pac-Man Plus (easier to see)
//mapPacman.wallStrokeColor = "#2121ff"; // from original
mapPacman.wallFillColor = "#000";
mapPacman.pelletColor = "#ffb8ae";
// Ms. Pac-Man map 1
var mapMsPacman1 = new Map(28, 36, (
"____________________________" +
"____________________________" +
"____________________________" +
"||||||||||||||||||||||||||||" +
"|......||..........||......|" +
"|o||||.||.||||||||.||.||||o|" +
"|.||||.||.||||||||.||.||||.|" +
"|..........................|" +
"|||.||.|||||.||.|||||.||.|||" +
"__|.||.|___|.||.|___|.||.|__" +
"|||.||.|||||.||.|||||.||.|||" +
" .||.......||.......||. " +
"|||.||||| |||||||| |||||.|||" +
"__|.||||| |||||||| |||||.|__" +
"__|. .|__" +
"__|.||||| |||--||| |||||.|__" +
"__|.||||| |______| |||||.|__" +
"__|.|| |______| ||.|__" +
"__|.|| || |______| || ||.|__" +
"|||.|| || |||||||| || ||.|||" +
" . || || . " +
"|||.|||||||| || ||||||||.|||" +
"__|.|||||||| || ||||||||.|__" +
"__|....... || .......|__" +
"__|.|||||.||||||||.|||||.|__" +
"|||.|||||.||||||||.|||||.|||" +
"|............ ............|" +
"|.||||.|||||.||.|||||.||||.|" +
"|.|__|.|||||.||.|||||.|__|.|" +
"|.|__|.||....||....||.|__|.|" +
"|o|__|.||.||||||||.||.|__|o|" +
"|.||||.||.||||||||.||.||||.|" +
"|..........................|" +
"||||||||||||||||||||||||||||" +
"____________________________" +
"____________________________"));
mapMsPacman1.name = "Ms. Pac-Man (1)";
mapMsPacman1.wallFillColor = "#FFB8AE";
mapMsPacman1.wallStrokeColor = "#FF0000";
mapMsPacman1.pelletColor = "#dedeff";
// Ms. Pac-Man map 2
var mapMsPacman2 = new Map(28, 36, (
"____________________________" +
"____________________________" +
"____________________________" +
"||||||||||||||||||||||||||||" +
" ||..........|| " +
"|||||| ||.||||||||.|| ||||||" +
"|||||| ||.||||||||.|| ||||||" +
"|o...........||...........o|" +
"|.|||||||.||.||.||.|||||||.|" +
"|.|||||||.||.||.||.|||||||.|" +
"|.||......||.||.||......||.|" +
"|.||.|||| ||....|| ||||.||.|" +
"|.||.|||| |||||||| ||||.||.|" +
"|......|| |||||||| ||......|" +
"||||||.|| ||.||||||" +
"||||||.|| |||--||| ||.||||||" +
"|......|| |______| ||......|" +
"|.||||.|| |______| ||.||||.|" +
"|.||||. |______| .||||.|" +
"|...||.|| |||||||| ||.||...|" +
"|||.||.|| ||.||.|||" +
"__|.||.|||| |||| ||||.||.|__" +
"__|.||.|||| |||| ||||.||.|__" +
"__|.........||||.........|__" +
"__|.|||||||.||||.|||||||.|__" +
"|||.|||||||.||||.|||||||.|||" +
" ....||... ...||.... " +
"|||.||.||.||||||||.||.||.|||" +
"|||.||.||.||||||||.||.||.|||" +
"|o..||.......||.......||..o|" +
"|.||||.|||||.||.|||||.||||.|" +
"|.||||.|||||.||.|||||.||||.|" +
"|..........................|" +
"||||||||||||||||||||||||||||" +
"____________________________" +
"____________________________"));
mapMsPacman2.name = "Ms. Pac-Man (2)";
mapMsPacman2.wallFillColor = "#47b8ff";
mapMsPacman2.wallStrokeColor = "#dedeff";
mapMsPacman2.pelletColor = "#ffff00";
// Ms. Pac-Man map 3
var mapMsPacman3 = new Map(28, 36, (
"____________________________" +
"____________________________" +
"____________________________" +
"||||||||||||||||||||||||||||" +
"|.........||....||.........|" +
"|o|||||||.||.||.||.|||||||o|" +
"|.|||||||.||.||.||.|||||||.|" +
"|.||.........||.........||.|" +
"|.||.||.||||.||.||||.||.||.|" +
"|....||.||||.||.||||.||....|" +
"||||.||.||||.||.||||.||.||||" +
"||||.||..............||.||||" +
" ....|||| |||||||| ||||.... " +
"|.|| |||| |||||||| |||| ||.|" +
"|.|| ||.|" +
"|.|||| || |||--||| || ||||.|" +
"|.|||| || |______| || ||||.|" +
"|. || |______| || .|" +
"|.|| |||| |______| |||| ||.|" +
"|.|| |||| |||||||| |||| ||.|" +
"|.|| ||.|" +
"|.|||| ||||| || ||||| ||||.|" +
"|.|||| ||||| || ||||| ||||.|" +
"|......||....||....||......|" +
"|||.||.||.||||||||.||.||.|||" +
"|||.||.||.||||||||.||.||.|||" +
"|o..||....... .......||..o|" +
"|.||||.|||||.||.|||||.||||.|" +
"|.||||.|||||.||.|||||.||||.|" +
"|......||....||....||......|" +
"|.||||.||.||||||||.||.||||.|" +
"|.||||.||.||||||||.||.||||.|" +
"|......||..........||......|" +
"||||||||||||||||||||||||||||" +
"____________________________" +
"____________________________"));
mapMsPacman3.name = "Ms. Pac-Man (3)";
mapMsPacman3.wallFillColor = "#de9751";
mapMsPacman3.wallStrokeColor = "#dedeff";
mapMsPacman3.pelletColor = "#ff0000";
// Ms. Pac-Man map 4
var mapMsPacman4 = new Map(28, 36, (
"____________________________" +
"____________________________" +
"____________________________" +
"||||||||||||||||||||||||||||" +
"|..........................|" +
"|.||.||||.||||||||.||||.||.|" +
"|o||.||||.||||||||.||||.||o|" +
"|.||.||||.||....||.||||.||.|" +
"|.||......||.||.||......||.|" +
"|.||||.||.||.||.||.||.||||.|" +
"|.||||.||.||.||.||.||.||||.|" +
"|......||....||....||......|" +
"|||.|||||||| || ||||||||.|||" +
"__|.|||||||| || ||||||||.|__" +
"__|....|| ||....|__" +
"||| ||.|| |||--||| ||.|| |||" +
" ||.|| |______| ||.|| " +
"||||||. |______| .||||||" +
"||||||.|| |______| ||.||||||" +
" ||.|| |||||||| ||.|| " +
"||| ||.|| ||.|| |||" +
"__|....||||| || |||||....|__" +
"__|.||.||||| || |||||.||.|__" +
"__|.||.... || ....||.|__" +
"__|.|||||.|| || ||.|||||.|__" +
"|||.|||||.|| || ||.|||||.|||" +
"|.........|| ||.........|" +
"|.||||.||.||||||||.||.||||.|" +
"|.||||.||.||||||||.||.||||.|" +
"|.||...||..........||...||.|" +
"|o||.|||||||.||.|||||||.||o|" +
"|.||.|||||||.||.|||||||.||.|" +
"|............||............|" +
"||||||||||||||||||||||||||||" +
"____________________________" +
"____________________________"));
mapMsPacman4.name = "Ms. Pac-Man (4)";
mapMsPacman4.wallFillColor = "#2121ff";
mapMsPacman4.wallStrokeColor = "#ffb851";
mapMsPacman4.pelletColor = "#dedeff";
// Iwatani's Prototype
var mapSketch = new Map(28, 36, (
"____________________________" +
"____________________________" +
"______||||||||||||||||______" +
"______|..............|______" +
"______|.||.||||||.||.|______" +
"______|.||.||||||.||.|______" +
"______|.||...||...||.|______" +
"______|.||||.||.||||.|______" +
"|||||||.||||.||.||||.|||||||" +
"........||........||........" +
"|||.||||||.||||||.||||||.|||" +
"|||.||||||.||||||.||||||.|||" +
"|.......||...||...||.......|" +
"|.|||||.||||.||.||||.|||||.|" +
"|.|||||.||||.||.||||.|||||.|" +
"|....||..............||....|" +
"||||.||||.|||--|||.||||.||||" +
"||||.||||.|______|.||||.||||" +
"..||......|______|......||.." +
"|.||.||||.|______|.||||.||.|" +
"|....||||.||||||||.||||....|" +
"|.||.||..............||.||.|" +
"|.||.||.||||||||||||.||.||.|" +
"|.||.||.||||||||||||.||.||.|" +
"..||.||......||......||.||.." +
"||||.|||||||.||.|||||||.||||" +
"||||.|||||||.||.|||||||.||||" +
"|..........................|" +
"|.||.||||||||||||||||||.||.|" +
"|.||.|________________|.||.|" +
"|.||.|________________|.||.|" +
"|.||.|________________|.||.|" +
"|....|________________|....|" +
"||||||________________||||||" +
"____________________________" +
"____________________________"));
mapSketch.name = "Original Prototype";
mapSketch.wallFillColor = "#555";
mapSketch.wallStrokeColor = "#fff";
mapSketch.pelletColor = "#dedeff";
var w = mapPacman.widthPixels+tileSize;
var h = mapPacman.heightPixels+tileSize;
// change these functions to 'draw' or 'drawPath' to change map representations
// when using 'drawPath', the 'print' checkbox must be selected for now to prevent blackout.
mapSketch.drawPath(ctx,0,0,print);
mapPacman.drawPath(ctx,w,0,print);
mapMsPacman1.drawPath(ctx,2*w,0,print);
mapMsPacman2.drawPath(ctx,0,h,print);
mapMsPacman3.drawPath(ctx,w,h,print);
mapMsPacman4.drawPath(ctx,2*w,h,print);
};
</script>
</body>
</html>
@@ -0,0 +1,82 @@
We represent the maps using ASCII characters representing three possible tiles: wall(|), path(.), and blank(_).
- A **wall** (|) tile is the boundary of a wall.
- A **void** (_) tile is the interior or exterior of a wall.
- A **path** (.) tile is a walkable area.
ORIGINAL PROTOTYPE PAC-MAN MS. PAC-MAN (1)
____________________________ ____________________________ ____________________________
____________________________ ____________________________ ____________________________
______||||||||||||||||______ ____________________________ ____________________________
______|..............|______ |||||||||||||||||||||||||||| ||||||||||||||||||||||||||||
______|.||.||||||.||.|______ |............||............| |......||..........||......|
______|.||.||||||.||.|______ |.||||.|||||.||.|||||.||||.| |.||||.||.||||||||.||.||||.|
______|.||...||...||.|______ |.|__|.|___|.||.|___|.|__|.| |.||||.||.||||||||.||.||||.|
______|.||||.||.||||.|______ |.||||.|||||.||.|||||.||||.| |..........................|
|||||||.||||.||.||||.||||||| |..........................| |||.||.|||||.||.|||||.||.|||
|.......||........||.......| |.||||.||.||||||||.||.||||.| __|.||.|___|.||.|___|.||.|__
|||.||||||.||||||.||||||.||| |.||||.||.||||||||.||.||||.| |||.||.|||||.||.|||||.||.|||
|||.||||||.||||||.||||||.||| |......||....||....||......| ....||.......||.......||....
|.......||...||...||.......| ||||||.|||||.||.|||||.|||||| |||.|||||.||||||||.|||||.|||
|.|||||.||||.||.||||.|||||.| _____|.|||||.||.|||||.|_____ __|.|||||.||||||||.|||||.|__
|.|||||.||||.||.||||.|||||.| _____|.||..........||.|_____ __|......................|__
|....||..............||....| _____|.||.|||--|||.||.|_____ __|.|||||.|||--|||.|||||.|__
||||.||||.|||--|||.||||.|||| ||||||.||.|______|.||.|||||| __|.|||||.|______|.|||||.|__
||||.||||.|______|.||||.|||| ..........|______|.......... __|.||....|______|....||.|__
..||......|______|......||.. ||||||.||.|______|.||.|||||| __|.||.||.|______|.||.||.|__
|.||.||||.|______|.||||.||.| _____|.||.||||||||.||.|_____ |||.||.||.||||||||.||.||.|||
|....||||.||||||||.||||....| _____|.||..........||.|_____ .......||..........||.......
|.||.||..............||.||.| _____|.||.||||||||.||.|_____ |||.||||||||.||.||||||||.|||
|.||.||.||||||||||||.||.||.| ||||||.||.||||||||.||.|||||| __|.||||||||.||.||||||||.|__
|.||.||.||||||||||||.||.||.| |............||............| __|..........||..........|__
..||.||......||......||.||.. |.||||.|||||.||.|||||.||||.| __|.|||||.||||||||.|||||.|__
||||.|||||||.||.|||||||.|||| |.||||.|||||.||.|||||.||||.| |||.|||||.||||||||.|||||.|||
||||.|||||||.||.|||||||.|||| |...||................||...| |..........................|
|..........................| |||.||.||.||||||||.||.||.||| |.||||.|||||.||.|||||.||||.|
|.||.||||||||||||||||||.||.| |||.||.||.||||||||.||.||.||| |.|__|.|||||.||.|||||.|__|.|
|.||.|________________|.||.| |......||....||....||......| |.|__|.||....||....||.|__|.|
|.||.|________________|.||.| |.||||||||||.||.||||||||||.| |.|__|.||.||||||||.||.|__|.|
|.||.|________________|.||.| |.||||||||||.||.||||||||||.| |.||||.||.||||||||.||.||||.|
|....|________________|....| |..........................| |..........................|
||||||________________|||||| |||||||||||||||||||||||||||| ||||||||||||||||||||||||||||
____________________________ ____________________________ ____________________________
____________________________ ____________________________ ____________________________
MS. PAC-MAN (2) MS. PAC-MAN (3) MS. PAC-MAN (4)
____________________________ ____________________________ ____________________________
____________________________ ____________________________ ____________________________
____________________________ ____________________________ ____________________________
|||||||||||||||||||||||||||| |||||||||||||||||||||||||||| ||||||||||||||||||||||||||||
.......||..........||....... |.........||....||.........| |..........................|
||||||.||.||||||||.||.|||||| |.|||||||.||.||.||.|||||||.| |.||.||||.||||||||.||||.||.|
||||||.||.||||||||.||.|||||| |.|||||||.||.||.||.|||||||.| |.||.||||.||||||||.||||.||.|
|............||............| |.||.........||.........||.| |.||.||||.||....||.||||.||.|
|.|||||||.||.||.||.|||||||.| |.||.||.||||.||.||||.||.||.| |.||......||.||.||......||.|
|.|||||||.||.||.||.|||||||.| |....||.||||.||.||||.||....| |.||||.||.||.||.||.||.||||.|
|.||......||.||.||......||.| ||||.||.||||.||.||||.||.|||| |.||||.||.||.||.||.||.||||.|
|.||.||||.||....||.||||.||.| ||||.||..............||.|||| |......||....||....||......|
|.||.||||.||||||||.||||.||.| .....||||.||||||||.||||..... |||.||||||||.||.||||||||.|||
|......||.||||||||.||......| |.||.||||.||||||||.||||.||.| __|.||||||||.||.||||||||.|__
||||||.||..........||.|||||| |.||....................||.| __|....||..........||....|__
||||||.||.|||--|||.||.|||||| |.||||.||.|||--|||.||.||||.| |||.||.||.|||--|||.||.||.|||
|......||.|______|.||......| |.||||.||.|______|.||.||||.| ....||.||.|______|.||.||....
|.||||.||.|______|.||.||||.| |......||.|______|.||......| ||||||....|______|....||||||
|.||||....|______|....||||.| |.||.||||.|______|.||||.||.| ||||||.||.|______|.||.||||||
|...||.||.||||||||.||.||...| |.||.||||.||||||||.||||.||.| ....||.||.||||||||.||.||....
|||.||.||..........||.||.||| |.||....................||.| |||.||.||..........||.||.|||
__|.||.||||.||||.||||.||.|__ |.||||.|||||.||.|||||.||||.| __|....|||||.||.|||||....|__
__|.||.||||.|__|.||||.||.|__ |.||||.|||||.||.|||||.||||.| __|.||.|||||.||.|||||.||.|__
__|.........|__|.........|__ |......||....||....||......| __|.||.......||.......||.|__
__|.|||||||.|__|.|||||||.|__ |||.||.||.||||||||.||.||.||| __|.|||||.||.||.||.|||||.|__
|||.|||||||.||||.|||||||.||| |||.||.||.||||||||.||.||.||| |||.|||||.||.||.||.|||||.|||
.......||..........||....... |...||................||...| |.........||....||.........|
|||.||.||.||||||||.||.||.||| |.||||.|||||.||.|||||.||||.| |.||||.||.||||||||.||.||||.|
|||.||.||.||||||||.||.||.||| |.||||.|||||.||.|||||.||||.| |.||||.||.||||||||.||.||||.|
|...||.......||.......||...| |......||....||....||......| |.||...||..........||...||.|
|.||||.|||||.||.|||||.||||.| |.||||.||.||||||||.||.||||.| |.||.|||||||.||.|||||||.||.|
|.||||.|||||.||.|||||.||||.| |.||||.||.||||||||.||.||||.| |.||.|||||||.||.|||||||.||.|
|..........................| |......||..........||......| |............||............|
|||||||||||||||||||||||||||| |||||||||||||||||||||||||||| ||||||||||||||||||||||||||||
____________________________ ____________________________ ____________________________
____________________________ ____________________________ ____________________________
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html>
<head>
<title>Pac-Man Maze Generation</title>
<script src="tetris/colors.js"></script>
<script src="tetris/mapgen.js"></script>
<script src="tetris/Map.js"></script>
<script>
var draw = function() {
var size = 40;
var canvas = document.getElementById('canvas');
canvas.width = 1024;
canvas.height = 1024;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#FFF";
ctx.fillRect(0,0,canvas.width,canvas.height);
var map = mapgen();
var x,y;
x = size;
y = size;
drawCells(ctx,x,y,size,"Simple Model",{'drawNumbers':true});
x += (cols+1)*size;
drawCells(ctx,x,y,size,"Height Adjustments",{'drawRaiseHeightCandidate':true, 'drawRaiseHeight':true});
x += (cols+1)*size;
drawCells(ctx,x,y,size,"Width Adjustments",{'drawShrinkWidthCandidate':true, 'drawShrinkWidth':true});
x += (cols+1)*size;
drawCells(ctx,x,y,size,"Border Cells and Tunnels",
{'drawJoinCandidate':true,
'drawSingleDeadEnd':true,
'drawDoubleDeadEnd':true,
'drawVoidTunnel':true,
'drawEdgeTunnel':true,
});
x = size;
y += (rows+1)*size;
ctx.save();
ctx.translate(x,y);
ctx.scale(2,2);
map.name = "Final Paths";
map.drawPath(ctx,0,0);
ctx.restore();
x = 13*size;
ctx.save();
ctx.translate(x,y);
ctx.scale(2,2);
map.name = "Final Tiles";
map.draw(ctx,0,0,true);
ctx.restore();
};
window.onload = function() {
draw();
};
</script>
<style>
body {
background:#AAA;
}
p, li {
line-height:1.5em;
}
#container {
padding: 20px;
background:#FFF;
width: 800px;
margin-left:auto;
margin-right:auto;
}
canvas {
width: 100%;
}
</style>
</head>
<body>
<div id="container">
<p>
<h1>Pac-Man Maze Generation</h1>
<p>
by <a href="http://twitter.com/shaunlebron">Shaun LeBron</a>.
(<a href="https://github.com/shaunlebron/pacman-mazegen">code on GitHub</a>)
</p>
<button onclick="draw()">Click to generate new example</button>
</p>
<canvas id='canvas'></canvas>
<p>
<h2><span style="color:red">Work in Progress</span></h2>
<p>
<i>
Generating random Pac-Man mazes is a deceptively difficult problem that I spent some months working on. It is not easy to describe clearly. I hope you are patient. This page is an effort to begin communicating how the algorithm works. It will slowly be refined (your feedback appreciated) until it is all stated as clearly as possible.
</i>
</p>
<h2>Contraints</h2>
<p>
The mazes are built carefully to closely match design patterns deduced from the original maps found in Pac-Man and Ms. Pac-Man:
</p>
<ul>
<li>Map is 28x31 tiles.</li>
<li>Paths are only 1 tile thick</li>
<li>No sharp turns (i.e. intersections are separated by atleast 2 tiles).</li>
<li>There are 1 or 2 tunnels</li>
<li>No dead-ends.</li>
<li>Only I, L, T, or + wall shapes are allowed, including the occasional rectangular wall.</li>
<li>Any non-rectangular wall pieces must only be 2 tiles thick.</li>
</ul>
<h2>It's like Tetris</h2>
<p>
We start by stacking tetris pieces on a 5x9 grid. Gravity pulls the pieces in the left direction rather than down. The edges of the resulting tetris pieces correspond to walkable paths in the maze. This grid is then mirrored across the left vertical axis to create a symmetric map, then scaled by 3 to form an original-size 28x31 map.
</p>
<h2>Definitions</h2>
<p>
For clarity, I call the squares in the initial 5x9 grid, <b>cells</b>, and the squares in the final 28x31 grid, <b>tiles</b>. So, this algorithm first creates the <b>cells</b> and transforms them into <b>tiles</b>
</p>
<h2>Simple Model</h2>
<p>
Shown in the above diagram titled "Simple Model" is the 5x9 grid of tetris pieces. The pieces are created one cell at a time using some algorithm to limit the type of pieces at certain locations (they are numbered to show the order of creation).
</p>
<p>
The ghost pen and the edge between rows 7 and 8 at column 1 are present in every map, since the starting location of Pac-Man and the ghost pen location are constant.
</p>
<h2>Height and Width Adjustments</h2>
<p>
Cells are directly transformed into a 3x3 group of tiles. Unfortunately, this creates a resulting map that is too short by 1 tile and too wide by 1 tile. So, we increase the height of one cell for every column, and decrease the width of one cell for every row, allowing the generated map to fit in the exact dimensions of the original game.
</p>
<p>
Shown in the above diagrams titled "Height Adjustments" and "Width Adjustments", the highlighted cells are the candidate cells whose size can be changed without creating ugly walls (i.e. walls that have non-uniform thickness).
</p>
<p>
Arrows occupy cells which have been chosen for size adjustment. Care is taken to prevent discontinuities in the edges as a result of the shifting of cells from the size change.
</p>
<h2>Border Cells and Tunnels</h2>
<p>
I won't explain too much about this right now. But the above diagram titled "Border Cells and Tunnels" has arrows to indicate the tunnel candidates. The highlighted cells show the type of tunnel candidates by color. Some cell edges are erased to create some variation in how walls connect with the boundary of the map (shown in green). The tunnel creation algorithm is sophisticated in how it chooses different types of tunnels.
</p>
<h2>Final Paths</h2>
<p>
When the cells are finally transformed into tiles, what you are left with is shown in the diagram above titled "Final Paths". Here you can directly map a cell to a 3x3 group of tiles. You can even pick out the cells whose height are width have been adjusted by 1 tile in this map.
</p>
<h2>Final Tiles</h2>
<p>
See how the above diagram titled "Final Tiles" differs from "Final Paths". The paths are shifted from the tile <em>edges</em> toward the tile <em>centers</em>. Each tile with a path going through its center is turned into a path tile. Finally, any tile that touches a path tile becomes a wall tile. The map structure is now complete.
</p>
<h2>Results</h2>
<p>
<a href="tetris/many.htm">Click here to see many generated Pac-Man mazes together.</a>
</p>
<h2>Appendix</h2>
<h3>Original Maps</h3>
<img src="img/origmaps_2x.png" width="100%" />
<h3>Original Maps (plain)</h3>
<img src="img/origmaps_2x_print.png" width="100%" />
<h3>Original Maps (paths)</h3>
<img src="img/origmaps_path.png" width="100%" />
</div>
</body>
</html>
@@ -0,0 +1,107 @@
#!/usr/bin/python
import unittest
from walls import *
class MapTest(unittest.TestCase):
def assertMap(self,expected):
actual = str(self.tileMap).splitlines()
expected = format_map_str(expected,"\n").splitlines()
self.assertEqual(actual,expected, "\n".join(repr(a)+' '+repr(b) for a,b in zip(actual,expected)))
def test1(self):
self.tileMap = Map(9,9,"""
.........
..||.....
..||.....
.........
.........
.........
.........
.........
.........
""")
self.tileMap.add_wall_obstacle(1,4)
self.assertMap("""
.........
..||.....
..||.....
.........
..||.....
..||.....
..||.....
.........
.........
""")
def test2(self):
self.tileMap = Map(9,9,"""
.........
..||.....
..||.....
.........
.........
.........
.........
.........
.........
""")
self.tileMap.add_wall_obstacle(5,1)
self.assertMap("""
.........
..||.....
..||.|||.
.....|||.
.........
.........
.........
.........
.........
""")
def test3(self):
self.tileMap = Map(11,7,"""
...........
...||......
...||......
...........
..||.......
..||.......
...........
""")
self.tileMap.add_wall_obstacle(7,2)
self.assertMap("""
...........
...||......
...||......
......||||.
..||.|||||.
..||.||....
...........
""")
def test4(self):
self.tileMap = Map(15,8,"""
...............
.....||.||.....
.....||.||.....
.....||....|||.
...........|||.
.||........|||.
.||............
.........||.||.
""")
self.tileMap.add_wall_obstacle(3,4)
self.assertMap("""
...............
.....||.||.....
.....||.||.....
.....||....|||.
........||.|||.
.||.||||||.|||.
.||.||||.......
.........||.||.
""")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,431 @@
#!/usr/bin/python
"""
Running this script spits out a random Pac-Man maze
example with extend phase off:
||||||||||||||||||||||||||||
|..........................|
|.||||.||.||||||||.||.||||.|
|.||||.||.||||||||.||.||||.|
|.||||......||||......||||.|
|.||||.||||..||..||||.||||.|
|......|||||.||.|||||......|
|.||||.|||||....|||||.||||.|
|.||||.|||||.||.|||||.||||.|
|.||||.|||||.||.|||||.||||.|
|.........||.||.||.........|
|.|||.|||..........|||.|||.|
|.|||.|||.||||||||.|||.|||.|
|.|||.....||||||||.....|||.|
|.|||.|||.||||||||.|||.|||.|
|.|||.|||.||||||||.|||.|||.|
|.........||||||||.........|
|.||||.||..........||.||||.|
|.||||.||||.||||.||||.||||.|
|........||.||||.||........|
|.|||.||.||......||.||.|||.|
|.|||.||.....||.....||.|||.|
|......||.||.||.||.||......|
|.||||.||.||.||.||.||.||||.|
|.||||................||||.|
|......||.||||||||.||......|
|.||||.||.||||||||.||.||||.|
|.||||.||.||||||||.||.||||.|
|.||||.||.||||||||.||.||||.|
|..........................|
||||||||||||||||||||||||||||
example with extend phase on:
||||||||||||||||||||||||||||
|..........................|
|.|||.||||.||||||.||||.|||.|
|.|||.||||.||||||.||||.|||.|
|.|||......||||||......|||.|
|.||||||||........||||||||.|
|.||||||||.||..||.||||||||.|
|..........||..||..........|
|.|||||||.||....||.|||||||.|
|.|||||||.||||||||.|||||||.|
|....||||.||||||||.||||....|
|.||.||||..........||||.||.|
|.||.||||.||||||||.||||.||.|
|.||......||||||||......||.|
|.||..|||.||||||||.|||..||.|
|.||..|||.||||||||.|||..||.|
|.||.||||.||||||||.||||.||.|
|.||.||..............||.||.|
|.......||.||||||.||.......|
|.||||||||.||||||.||||||||.|
|.||||||||.||||||.||||||||.|
|.......||.||||||.||.......|
|.|||||.||.||||||.||.|||||.|
|.|||||.||........||.|||||.|
|...........||||...........|
|.|||||.|||.||||.|||.|||||.|
|.|||||.|||.||||.|||.|||||.|
|.|||||.|||.||||.|||.|||||.|
|.|||||.|||.||||.|||.|||||.|
|..........................|
||||||||||||||||||||||||||||
OVERVIEW:
This currently works by starting with an empty half map with a ghost
house.
PLACE PHASE
We add walls by placing 2x2 blocks in areas that allow for a one
tile wide margin.
.... ....
.... .||.
.... > .||.
.... ....
GROW PHASE
After placing a new wall piece, a gap-filling heuristic is used to grow the piece.
Basically, the wall is grown to fill in adjacent areas that cannot be filled by new pieces.
........... ........... ...........
...||...... ...||...... ...||......
...||...... ...||...... ...||......
........... > ........||. > ......||||.
..||....... ..||....||. ..||.|||||.
..||....... ..||....... ..||.||....
........... ........... ...........
(start) (new piece) (after growth)
EXTEND PHASE
An additional extend phase is employed which applies contiguous blocks
in a straight random direction, and starting again in the same direction
but rotated 90 degrees. The turning is triggered by a certain distance
traveled or by a dead end. Each block that is placed also undergoes
its own grow phase to fill in gaps along the way.
CURRENT PROBLEMS:
Walls are very fragmented. Make the pieces grow more by extending in a random direction after initial mandatory growing.
We could alternatively do a post-process to join smaller pieces together:
....... .......
.||.||. > .|||||.
.||.||. .|||||.
....... .......
(One solution is the extend phase mentioned above)
Some gaps aren't filled, need to study them some more and add appropriate test cases.
There is currently a path around the entire border.
Could possibly extend some contiguous pieces to the border to fix this.
Seems rare, but sometimes dead ends and single tile thick walls are formed.
It could be easier to just throw out a map if this conditions are detected.
Conditions:
a 2x2 empty block => dead end
a wall tile that is not part of a 2x2 wall block => single tile wall
"""
import sys
import random
# TODO:
# define an Obstacle class to represent a single group of contiguous wall tiles
# Obstacle class
# Box Obstacle
# Line Obstacle
# map from tile to Obstacle
def all(iter):
for e in iter:
if not e: return False
return True
def any(iter):
for e in iter:
if e: return True
return False
# takes multi-line map string, trims indentation, replaces newlines with given separator
def format_map_str(tiles,sep):
return sep.join(line.strip() for line in tiles.splitlines())
class Map:
def __init__(self,w,h,tile_str=None):
if tile_str is None:
# just create a clear map
self.tiles = []
self.w = w
self.h = h
for i in xrange(w*h):
self.tiles.append('.')
else:
self.setMap(w,h,tile_str)
# sets logging verbosity (on|off)
self.verbose = False
# create a map from a tile string
def setMap(self,w,h,tile_str):
self.w = w
self.h = h
self.tiles = list(format_map_str(tile_str,""))
# creates a string of the current map
def __str__(self):
s = "\n"
i = 0
for y in xrange(self.h):
for x in xrange(self.w):
s += self.tiles[i]
i += 1
s += "\n"
return s
# converts x,y to index
def xy_to_i(self,x,y):
return x+y*self.w
# converts index to x,y
def i_to_xy(self,i):
return i%self.w, i/self.w
# validates x,y
def xy_valid(self,x,y):
return x >= 0 and x < self.w and y>=0 and y<self.h
# gets tile at x,y or returns None if invalid
def get_tile(self,x,y):
if not self.xy_valid(x,y):
return None
return self.tiles[x+y*self.w]
# adds a single wall tile at x,y
def add_wall_tile(self,x,y):
if self.xy_valid(x,y):
self.tiles[x+y*self.w] = '|'
def is_wall_block_filled(self,x,y):
return all(self.get_tile(x+dx,y+dy) == '|' for dy in range(1,3) for dx in range(1,3))
# adds a 2x2 block inside the 4x4 block at the given x,y coordinate
def add_wall_block(self,x,y):
self.add_wall_tile(x+1,y+1)
self.add_wall_tile(x+2,y+1)
self.add_wall_tile(x+1,y+2)
self.add_wall_tile(x+2,y+2)
# determines if a 2x2 block can fit inside the 4x4 block at the given x,y coordinate
# (the whole 4x4 block must be empty)
def can_new_block_fit(self,x,y):
if not (self.xy_valid(x,y) and self.xy_valid(x+3,y+3)):
return False
for y0 in xrange(y,y+4):
for x0 in xrange(x,x+4):
if self.get_tile(x0,y0) != '.':
return False
return True
# create a list of valid starting positions
def update_pos_list(self):
self.pos_list = []
for y in xrange(self.h):
for x in xrange(self.w):
if self.can_new_block_fit(x,y):
self.pos_list.append((x,y))
# A connection is a sort of dependency of one tile block on another.
# If a valid starting position is against another wall, then add this tile
# to other valid start positions' that intersect this one so that they fill
# it when they are chosen. This filling is a heuristic to eliminate gaps.
def update_connections(self):
self.connections = {}
for y in xrange(self.h):
for x in xrange(self.w):
if (x,y) in self.pos_list:
if any(self.get_tile(x-1,y+y0)=='|' for y0 in range(4)): self.add_connection(x,y,1,0)
if any(self.get_tile(x+4,y+y0)=='|' for y0 in range(4)): self.add_connection(x,y,-1,0)
if any(self.get_tile(x+x0,y-1)=='|' for x0 in range(4)): self.add_connection(x,y,0,1)
if any(self.get_tile(x+x0,y+4)=='|' for x0 in range(4)): self.add_connection(x,y,0,-1)
# the block at x,y is against a wall, so make intersecting blocks in the direction of
# dx,dy fill the block at x,y if they are filled first.
def add_connection(self,x,y,dx,dy):
def connect(x0,y0):
src = (x,y)
dest = (x0,y0)
if not dest in self.pos_list:
return
if dest in self.connections:
self.connections[dest].append(src)
else:
self.connections[dest] = [src]
if (x,y) in self.pos_list:
connect(x+dx,y+dy)
connect(x+2*dx,y+2*dy)
if not (x-dy,y-dx) in self.pos_list: connect(x+dx-dy,y+dy-dx)
if not (x+dy,y+dx) in self.pos_list: connect(x+dx+dy,y+dy+dx)
if not (x+dx-dy,y+dy-dx) in self.pos_list: connect(x+2*dx-dy, y+2*dy-dx)
if not (x+dx+dy,y+dy+dx) in self.pos_list: connect(x+2*dx+dy, y+2*dy+dx)
# update the starting positions and dependencies
def update(self):
self.update_pos_list()
self.update_connections()
# expand a wall block at the given x,y
# return number of tiles added
def expand_wall(self,x,y):
visited = []
def expand(x,y):
count = 0
src = (x,y)
if src in visited:
return 0
visited.append(src)
if src in self.connections:
for x0,y0 in self.connections[src]:
if not self.is_wall_block_filled(x0,y0):
count += 1
self.add_wall_block(x0,y0)
count += expand(x0,y0)
return count
return expand(x,y)
def get_most_open_dir(self,x,y):
dirs = ((0,-1),(0,1),(1,0),(-1,0))
max_dir = random.choice(dirs)
max_len = 0
for dx,dy in dirs:
len = 0
while (x+dx*len,y+dy*len) in self.pos_list:
len += 1
if len > max_len:
max_dir = (dx,dy)
max_len = len
return max_dir
# start a wall at block x,y
def add_wall_obstacle(self,x=None,y=None,extend=False):
self.update()
if not self.pos_list:
return False
# choose random valid starting position if none provided
if (x is None or y is None):
x,y = random.choice(self.pos_list)
# add first block
self.add_wall_block(x,y)
# initialize verbose print lines
first_lines = str(self).splitlines()
grow_lines = [""]*(self.h+2)
extend_lines = [""]*(self.h+2)
# mandatory grow phase
count = self.expand_wall(x,y)
if count > 0:
grow_lines = str(self).splitlines()
# extend phase
if extend:
# desired maximum block size
max_blocks = 4
# 35% chance of forcing the block to turn
# turn means the turn has been taken
# turn_blocks is the number of blocks traveled before turning
turn = False
turn_blocks = max_blocks
if random.random() <= 0.35:
turn_blocks = 4
max_blocks += turn_blocks
# choose a random direction
dx,dy = random.choice(((0,-1),(0,1),(1,0),(-1,0)))
orig_dir = (dx,dy)
i = 0
while count < max_blocks:
x0 = x+dx*i
y0 = y+dy*i
# turn if we're past turning point or at a dead end
if (not turn and count >= turn_blocks) or not (x0,y0) in self.pos_list:
turn = True
dx,dy = -dy,dx # rotate
i = 1
# stop if we've come full circle
if orig_dir == (dx,dy): break
else: continue
# add wall block and grow to fill gaps
if not self.is_wall_block_filled(x0,y0):
self.add_wall_block(x0,y0)
count += 1 + self.expand_wall(x0,y0)
i += 1
extend_lines = str(self).splitlines()
# print the map states after each phase for debugging
if self.verbose:
print "added block at ",x,y
for a,b,c in zip(first_lines, grow_lines, extend_lines):
print a,b,c
return True
if __name__ == "__main__":
# initial empty map with standard ghost house
tileMap = Map(16,31,"""
||||||||||||||||
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|.........||||||
|.........||||||
|.........||||||
|.........||||||
|.........||||||
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
|...............
||||||||||||||||
""")
# verbosity option (-v)
if len(sys.argv) > 1 and sys.argv[1] == "-v":
tileMap.verbose = True
# generate map by adding walls until there's no more room
while tileMap.add_wall_obstacle(extend=True):
pass
# reflect the first 14 columns to print the map
for line in str(tileMap).splitlines():
s = line[:14]
print s+s[::-1]
@@ -0,0 +1 @@
The goal here is to explore a simple algorithm that can be implemented more easily in z80
@@ -0,0 +1,783 @@
import sys
import random
mapwidth = 5
mapheight = 9
######################################################################
# Define all possible pieces.
class Piece:
def __init__(self):
pass
def __str__(self):
return self.string
def setFromStr(self, s):
self.string = s
offsets = []
self.leftHeight = 0
y = 0
foundTop = False
for line in s.split('\n'):
x = 0
for char in line.rstrip():
if char == '#':
if x == 0:
self.leftHeight += 1
if not foundTop:
foundTop = True
for p in offsets:
p[1] -= y
y = 0
x += 1
continue
offsets.append([x,y])
x += 1
y += 1
self.offsets = [(a[0],a[1]) for a in offsets]
self.offsets.append((0,0))
self.miny = min(y for x,y in self.offsets)
self.maxy = max(y for x,y in self.offsets)
self.maxx = max(x for x,y in self.offsets)
self.size = len(self.offsets)
def makePieces(a):
pieces = []
groups = a.split('\n\n')
i = 0
for group in groups:
if not group.strip():
continue
piece = Piece()
piece.index = i
piece.setFromStr(group)
pieces.append(piece)
i += 1
return pieces
pieces = makePieces("""
# 0
## 1
# 2
#
## 3
#
# 4
##
## 5
#
# 6
##
### 7
#
# 8
###
# 9
##
#
# 10
##
#
### 11
#
# 12
###
### 13
#
# 14
###
## 15
#
#
## 16
#
#
# 17
###
#
### 18
#
#
# 19
#
###
# 20
###
#
# 21
###
#
#### 22
#
#### 23
#
# 24
####
# 25
####
# 26
##
#
#
# 27
#
##
#
# 28
##
#
#
# 29
#
##
#
""")
######################################################################
# Determine valid pieces for each position regarding map boundaries.
def makeValidPieceTable(pieces):
valid_pieces = {}
for x in xrange(mapwidth-1):
for y in xrange(mapheight):
valid_pieces[(x,y)] = []
def inPen(x,y):
return x >= 0 and x <= 1 and y >= 3 and y <= 4
for x,y in valid_pieces:
# disallow anything in ghost pen
if inPen(x,y):
continue
for p in pieces:
# disallow anything in ghost pen
if any(inPen(x+dx,y+dy) for dx,dy in p.offsets):
continue
# disallow piece of size 1 anywhere but the top and bottom rows
if p.size == 1 and y > 0 and y < 8:
continue
# special restrictions on first column due to symmetry
if x == 0:
# disallow anything too wide
if p.maxx > 1:
continue
# disallow anything too large
if p.size > 4:
continue
# disallow complicated reflections
if p.size == 4 and p.leftHeight == 1:
continue
# disallow pieces obstructing pacman's starting point
if y == 5 and p.leftHeight > 2:
continue
if y == 6 and p.leftHeight > 1:
continue
# disallow pieces that don't fit inside map
maxx = x + p.maxx
miny = y + p.miny
maxy = y + p.maxy
if maxx >= mapwidth or miny < 0 or maxy >= mapheight:
continue
valid_pieces[(x,y)].append(p)
return valid_pieces
def shuffleValidPieces(v):
for a in v.values():
random.shuffle(a)
valid_pieces = makeValidPieceTable(pieces)
######################################################################
# Create a tile map class for searching piece configurations.
UP = 0
RIGHT = 1
DOWN = 2
LEFT = 3
class Cell:
def __init__(self,tile_map,x,y):
self.x = x
self.y = y
self.nextCells = [None] * 4
def isConnected(d):
dx = [0,1,0,-1][d]
dy = [-1,0,1,0][d]
# make sure connection is horizontally reflected at x==0
if x == 0 and dx == -1:
dx = 1
val = tile_map.getTile(x,y)
# empty tiles (always on the right side in a completed map) always open to the right
#if val == 0 and x == mapwidth-1 and dx = 1:
# return True
x0 = x+dx
y0 = y+dy
if x0 < 0 or x0 >= mapwidth or y0 < 0 or y0 >= mapheight:
return False
adjacentVal = tile_map.getTile(x0,y0)
return val == adjacentVal
self.connect = [isConnected(i) for i in xrange(4)]
class TileMap:
def __init__(self):
self.reset()
def getPieceList(self):
return [valid_pieces[(x,y)][i].index for i,x,y in self.piece_stack]
def getShell(self):
s = ""
for row in self.tiles:
for c in row:
if c == 0:
s += "0"
else:
s += "1"
return s
def getState(self):
return (self.getShell(), self.hasTopSquare, self.hasBottomSquare, self.numSize2, self.numSize5)
def reset(self):
self.tiles = [[0 for i in xrange(mapwidth)] for j in xrange(mapheight)]
self.setTile(0,3,1)
self.setTile(1,3,1)
self.setTile(0,4,1)
self.setTile(1,4,1)
self.num_pieces = 1
self.piece_stack = []
self.pos_dict = dict(((x,y),None) for x in range(mapwidth-1) for y in range(mapheight))
# state
self.hasTopSquare = False
self.hasBottomSquare = False
self.numSize2 = 0
self.maxSize2 = 2
self.numSize5 = 0
self.maxSize5 = 1
def setTopConfig(self,i):
x,y = 0,0
for p in top_configs[i]:
piece = pieces[p]
self.writePiece(piece,x,y)
y += piece.leftHeight
def setBottomConfig(self,i):
x,y = 0,5
for p in bottom_configs[i]:
piece = pieces[p]
self.writePiece(piece,x,y)
y += piece.leftHeight
def setTile(self,x,y,c):
self.tiles[y][x] = c
def getTile(self,x,y):
return self.tiles[y][x]
def buildCells(self):
# build table of cells
self.cells = [[Cell(self,x,y) for x in xrange(mapwidth)] for y in xrange(mapheight)]
# allow the referencing of adjacent cells
for y in xrange(mapheight):
for x in xrange(mapwidth):
c = self.cells[y][x]
if y+1 < mapheight:
cd = self.cells[y+1][x]
c.nextCells[DOWN] = cd
cd.nextCells[UP] = c
if x+1 < mapwidth:
cr = self.cells[y][x+1]
c.nextCells[RIGHT] = cr
cr.nextCells[LEFT] = c
def __str__(self):
s = ""
for row in self.tiles:
for col in row:
if col == 1:
col = ' '
elif col == 0:
col = '.'
s += ('%3s' % str(col))
s += "\n"
return s
def canPieceFit(self,piece,x,y):
if piece.size == 1:
if y == 0:
if self.hasTopSquare:
return False
elif y == mapheight-1:
if self.hasBottomSquare:
return False
else:
return False
elif piece.size == 2:
if self.numSize2 == self.maxSize2:
return False
elif piece.size == 5:
if self.numSize5 == self.maxSize5:
return False
# prevent two horizontal pieces from being on top of one another
if (piece.index == 1 and
(x,y-1) in self.pos_dict and
self.pos_dict[(x,y-1)] == 1):
return False
for dx,dy in piece.offsets:
if self.getTile(x+dx,y+dy) > 0:
return False
return True
def writePiece(self,piece,x,y):
self.num_pieces += 1
n = self.num_pieces
for dx,dy in piece.offsets:
self.setTile(x+dx,y+dy,n)
# update constraints
if piece.size == 1:
if y == 0:
self.hasTopSquare = True
else:
self.hasBottomSquare = True
elif piece.size == 2:
self.numSize2 += 1
elif piece.size == 5:
self.numSize5 += 1
def erasePiece(self,piece,x,y):
self.num_pieces -= 1
n = 0
for dx,dy in piece.offsets:
self.setTile(x+dx,y+dy,n)
# update constraints
if piece.size == 1:
if y == 0:
self.hasTopSquare = False
else:
self.hasBottomSquare = False
elif piece.size == 2:
self.numSize2 -= 1
elif piece.size == 5:
self.numSize5 -= 1
def pushPiece(self,i,x,y):
if isinstance(i,Piece):
piece = i
i = 0
for j in valid_pieces[(x,y)]:
i += 1
if j == piece:
break
else:
piece = valid_pieces[(x,y)][i]
self.pos_dict[(x,y)] = piece.index
self.piece_stack.append((i,x,y))
self.writePiece(piece,x,y)
def popPiece(self):
i,x,y = self.piece_stack.pop()
self.pos_dict[(x,y)] = None
piece = valid_pieces[(x,y)][i]
self.erasePiece(piece,x,y)
return i,x,y
def getNextOpenTile(self,x,y):
for x0 in xrange(x,mapwidth-1):
for y0 in xrange(y,mapheight):
if self.getTile(x0,y0) == 0:
return x0,y0
y = 0
return None
def depthFirstSearch(self,x,y,solutionCallback=None,shouldStop=None,debug=False):
i = 0
while True:
# We start this iteration knowing that we are at an open tile.
# Try to find a piece that fits in our open tile.
found_piece = False
potential_pieces = valid_pieces[(x,y)][i:]
for piece in potential_pieces:
if self.canPieceFit(piece,x,y):
if debug:
print "can fit piece %d at (%d,%d)" % (piece.index,x,y)
print self
found_piece = True
break
i += 1
# If we have found a piece to place at current tile.
if found_piece:
self.pushPiece(i,x,y)
pos = self.getNextOpenTile(x,y)
if not pos:
self.buildCells()
self.setResizeCandidates()
if self.chooseTallRows() and self.chooseNarrowCols():
if solutionCallback:
solutionCallback()
if debug:
print "SOLUTION:"
print self
if shouldStop and shouldStop(None,None):
break # stop search
else:
# TODO: set x,y to position before the first offending row or col that prevented resize
pass
else:
i = 0
x,y = pos
if shouldStop and shouldStop(x,y):
if solutionCallback:
solutionCallback()
# We have an open tile to continue with, so skip backtracking
else:
continue
# Backtrack to find next open tile
try:
# Pop pieces until we get to a position whose possibilities haven't been exhausted.
while True:
i,x,y = self.popPiece()
if debug:
print "removed piece %d at (%d,%d)" % (valid_pieces[(x,y)][i].index, x,y)
print self
try:
i += 1
valid_pieces[(x,y)][i]
# position not exhausted, use it.
break
except IndexError:
# position exhausted, continue popping
continue
except IndexError:
# exit search
break
def setResizeCandidates(self):
for y in xrange(mapheight):
for x in xrange(mapwidth):
c = self.cells[y][x]
q = c.connect
# _
# |_| or | |
if not q[LEFT] and not q[RIGHT] and (not q[UP] or not q[DOWN]):
c.isRaiseHeightCandidate = True
# __
# |__|
if x+1 < mapwidth:
cr = c.nextCells[RIGHT]
qr = cr.connect
if (not q[LEFT] and not q[UP] and not q[DOWN] and q[RIGHT] and
not qr[RIGHT] and not qr[UP] and not qr[DOWN] and qr[LEFT]):
c.isRaiseHeightCandidate = cr.isRaiseHeightCandidate = True
# _ _
# _| or |_
if not q[UP] and not q[DOWN] and q[LEFT] != q[RIGHT]:
c.isShrinkWidthCandidate = True
# empty cell on right border
if x == mapwidth-1 and self.getTile(x,y) == 0:
c.isShrinkWidthCandidate = True
# _
# |
# _|
if y+1 < mapheight:
cd = c.nextCells[DOWN]
qd = cd.connect
if (q[LEFT] and not q[UP] and not q[RIGHT] and q[DOWN] and
qd[LEFT] and not qd[DOWN] and not qd[RIGHT] and qd[UP]):
c.isShrinkWidthCandidate = cd.isShrinkWidthCandidate = True
def chooseTallRows(self):
return True
def chooseNarrowCols(self):
return True
######################################################################
# Create preset piece configurations for segments of the map.
top_right_configs = [[p.index] for p in pieces if p.size > 1 and (p.maxx,p.miny) in p.offsets]
bottom_right_configs = [[p.index] for p in pieces if p.size > 1 and (p.maxx,p.maxy) in p.offsets]
def createTopLeftConfigs():
configs = []
tile_map = TileMap()
shouldStop = lambda x,y: y > 2
def callback():
configs.append(tile_map.getPieceList())
tile_map.depthFirstSearch(0,0,solutionCallback=callback,shouldStop=shouldStop)
return configs
def createBottomLeftConfigs():
configs = []
tile_map = TileMap()
shouldStop = lambda x,y: x > 0
def callback():
configs.append(tile_map.getPieceList())
tile_map.depthFirstSearch(0,5,solutionCallback=callback,shouldStop=shouldStop)
return configs
top_left_configs = createTopLeftConfigs()
bottom_left_configs = createBottomLeftConfigs()
def printConfigInfo():
print "TOP LEFT", len(top_left_configs)
print "BOTTOM LEFT", len(bottom_left_configs)
print "TOP RIGHT", len(top_right_configs)
print "BOTTOM RIGHT", len(bottom_right_configs)
def printConfig(label, configs):
print label
for config in configs:
print config
printConfig("TOP LEFT", top_left_configs)
printConfig("BOTTOM LEFT", bottom_left_configs)
printConfig("TOP RIGHT", top_right_configs)
printConfig("BOTTOM RIGHT", bottom_right_configs)
def getTopRightXY(piece):
x = mapwidth-1-piece.maxx
y = -piece.miny
return x,y
def getBottomRightXY(piece):
x = mapwidth-1-piece.maxx
y = mapheight-1-piece.maxy
return x,y
def findTopConfigs():
configs = []
tile_map = TileMap()
for i,tl in enumerate(top_left_configs):
tile_map.reset()
x,y = 0,0
for p in tl:
piece = pieces[p]
tile_map.pushPiece(piece,x,y)
x,y = tile_map.getNextOpenTile(x,y)
for j,tr in enumerate(top_right_configs):
piece = pieces[tr[0]]
x,y = getTopRightXY(piece)
if tile_map.canPieceFit(piece,x,y):
configs.append((i,j))
return configs
def findBottomConfigs():
configs = []
tile_map = TileMap()
for i,bl in enumerate(bottom_left_configs):
tile_map.reset()
x,y = 0,5
for p in bl:
piece = pieces[p]
tile_map.pushPiece(piece,x,y)
x,y = tile_map.getNextOpenTile(x,y)
for j,br in enumerate(bottom_right_configs):
piece = pieces[br[0]]
x,y = getBottomRightXY(piece)
if tile_map.canPieceFit(piece,x,y):
configs.append((i,j))
return configs
top_configs = findTopConfigs()
bottom_configs = findBottomConfigs()
def getAllConfigs():
configs = []
for i,tc in enumerate(top_configs):
tl_index,tr_index = tc
tl_config = top_left_configs[tl_index]
tr_config = top_right_configs[tr_index]
for j,bc in enumerate(bottom_configs):
bl_index,br_index = bc
bl_config = bottom_left_configs[bl_index]
br_config = bottom_right_configs[br_index]
# Write the top left and bottom left configs
x,y = 0,0
tile_map = TileMap()
valid = True
for p in tl_config + bl_config:
piece = pieces[p]
if tile_map.canPieceFit(piece,x,y):
tile_map.writePiece(piece,x,y)
else:
valid = False
break
x,y = tile_map.getNextOpenTile(x,y)
if valid:
# Write the top right config
piece = pieces[tr_config[0]]
x,y = getTopRightXY(piece)
if tile_map.canPieceFit(piece,x,y):
tile_map.writePiece(piece,x,y)
else:
valid = False
if valid:
# Write the bottom right config
piece = pieces[br_config[0]]
x,y = getBottomRightXY(piece)
if tile_map.canPieceFit(piece,x,y):
tile_map.writePiece(piece,x,y)
else:
valid = False
if valid:
configs.append((i,j))
return configs
all_configs = getAllConfigs()
def makePresetTileMap(i):
top_config, bottom_config = all_configs[i]
# Get configs
tl_index,tr_index = top_configs[top_config]
bl_index,br_index = bottom_configs[bottom_config]
tl_config = top_left_configs[tl_index]
tr_config = top_right_configs[tr_index]
bl_config = bottom_left_configs[bl_index]
br_config = bottom_right_configs[br_index]
#print "top indexes:",tl_index, tr_index
#print "bottom indexes:",bl_index, br_index
#print "topleft:",tl_config
#print "bottomleft:",bl_config
#print "topright:",tr_config
#print "bottomright:",br_config
# Make tile map
tile_map = TileMap()
tile_map.preset_segments = [tl_index, tr_index, bl_index, br_index]
tile_map.preset_pieces = [
pieces[tr_config[0]].index,
pieces[br_config[0]].index,
]
# Write the top left and bottom left configs
x,y = 0,0
for p in tl_config+bl_config:
piece = pieces[p]
tile_map.preset_pieces.append(piece.index)
tile_map.writePiece(piece,x,y)
x,y = tile_map.getNextOpenTile(x,y)
# Write the top right config
piece = pieces[tr_config[0]]
x,y = getTopRightXY(piece)
tile_map.writePiece(piece,x,y)
# Write the bottom right config
piece = pieces[br_config[0]]
x,y = getBottomRightXY(piece)
tile_map.writePiece(piece,x,y)
return tile_map
def genMapForAllRoots():
numRoots = len(all_configs)
shouldStop = lambda x,y: x is None and y is None
for i in xrange(numRoots):
shuffleValidPieces(valid_pieces)
print >> sys.stderr, "%d / %d:" % (i+1,numRoots)
tile_map = makePresetTileMap(i)
success = [False]
def callback():
success[0] = True
x,y = tile_map.getNextOpenTile(0,0)
tile_map.depthFirstSearch(x,y,solutionCallback=callback,shouldStop=shouldStop)
if success[0]:
print tile_map.preset_segments, tile_map.preset_pieces
######################################################################
# Main.
def main():
genMapForAllRoots()
if __name__ == "__main__":
main()
@@ -0,0 +1,43 @@
We want a tool to help enumerate all possible maps, but mostly sub-maps because:
- there are too many to generate
- sub-maps will help us identify choke points for focusing on segments
- identifying patterns that can help us compress our representation of these mazes in z90
Generating all cases up to x=2 y=4:
time: 8 minutes
number of solutions: 10 million
time growth: factor of
on the order of 10 trillion solutions
Don't bother generating all, but identify unique pairings of KEYS and STATES
- this will allow us to FOLD
- a KEY = a silhouette of the current structure of a map
- a STATE = current counters for piece limits
We need a way to save and load intermediate results:
Perhaps a good enough shorcut to generate these maps would be to come up with
an algorithm for jumping between a given set of maps that we find to be valid.
A map is represented as a sequence of piece numbers to be stacked top to bottom, left to right.
------------------
Steps toward z80
* Find simple way to generate valid tilings of 5x9 map
* Prune out tilings that cannot be height- and width-adjusted
* Scale up to game tiles
* Add tunnels
* Add fruit paths
* Add pellets
* Add energizers
------------------
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
Spanning Tree Method
--------------------
Conventional mazes can be created by generating a spanning tree of a
rectangular grid of nodes. [Check out this great presentation by Jamis Buck to
learn about conventional maze generators](http://www.jamisbuck.org/presentations/rubyconf2011/index.html).
We can translate such spanning trees to be used in a Pac-Man tilemap by placing each node in the center of a 3x3 tile cell:
<img src="../img/spanning1.png"/>
@@ -0,0 +1 @@
/node_modules
@@ -0,0 +1,2 @@
<script src="index.js" type="module"></script>
@@ -0,0 +1,169 @@
const UP = 0;
const RIGHT = 1;
const DOWN = 2;
const LEFT = 3;
const numRows = 9;
const numCols = 5;
const ghostRow = 3;
function makeCell({ x, y }) {
return {
x,
y,
filled: false,
connect: [false, false, false, false],
next: {},
no: undefined,
group: undefined
};
}
function setGhostHomeCells(table) {
let c;
c = table[ghostRow][0];
c.filled = true;
c.connect[LEFT] = c.connect[RIGHT] = c.connect[DOWN] = true;
c = table[ghostRow][1];
c.filled = true;
c.connect[LEFT] = c.connect[DOWN] = true;
c = table[ghostRow + 1][0];
c.filled = true;
c.connect[LEFT] = c.connect[UP] = c.connect[RIGHT] = true;
c = table[ghostRow + 1][1];
c.filled = true;
c.connect[UP] = c.connect[LEFT] = true;
}
function makeTable() {
// initialize cells
const table = [];
const cells = [];
for (let y = 0; y < numRows; y++) {
const row = [];
for (let x = 0; x < numCols; x++) {
const cell = makeCell({ x, y });
cells.push(cell);
row.push(cell);
}
table.push(row);
}
// allow each cell to refer to surround cells by direction
for (const cell of cells) {
const { x, y } = cell;
if (x > 0) cell.next[LEFT] = table[y][x - 1];
if (x < numCols - 1) cell.next[RIGHT] = table[y][x + 1];
if (y > 0) cell.next[UP] = table[y - 1][x];
if (y < numRows - 1) cell.next[DOWN] = table[y + 1][x];
}
setGhostHomeCells(table);
return table;
}
function getLeftMostEmptyCells(table) {
const leftCells = [];
for (let x = 0; x < numCols; x++) {
for (let y = 0; y < numRows; y++) {
const c = table[y][x];
if (!c.filled) leftCells.push(c);
}
if (leftCells.length > 0) break;
}
return leftCells;
}
function makeState() {
return {
table: makeTable(),
cell: null,
firstCell: null,
firstCell: null,
numFilled: 0,
numGroups: 0,
size: 0,
singleCount: {
0: 0,
[numRows - 1]: 0
}
};
}
function fillCell(state, cell) {
cell.filled = true;
cell.no = state.numFilled++;
cell.group = state.numGroups;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomElement(list) {
const n = list.length;
if (n > 0) return list[getRandomInt(0, n - 1)];
}
const probTopAndBotSingleCellJoin = 1;
function trySingleCellGroup(state) {
const { cell, singleCount } = state;
// randomly allow one single-cell piece on the top or bottom of the map.
if (
cell.x < numCols - 1 &&
cell.y in singleCount &&
Math.random() <= probTopAndBotSingleCellJoin &&
singleCount[cell.y] == 0
) {
cell.connect[cell.y == 0 ? UP : DOWN] = true;
singleCount[cell.y]++;
return true;
}
}
function startNewGroup(state) {
const { table } = state;
const openCells = getLeftMostEmptyCells(table);
const cell = randomElement(openCells);
if (cell) {
fillCell(state, cell);
state.firstCell = state.cell = cell;
return cell;
}
}
function genRandomCells() {
const state = makeState();
while (true) {
const cell = startNewGroup(state);
if (!cell) break;
if (trySingleCellGroup(state)) continue;
state.size = 1;
if (cell.x === numCols - 1) {
cell.connect[RIGHT] = true;
cell.isRaiseHeightCandidate = true;
continue;
}
console.log(state);
break;
state.numGroups++;
}
}
genRandomCells();
@@ -0,0 +1,8 @@
{
"scripts": {
"start": "http-server -o"
},
"devDependencies": {
"http-server": "^0.11.1"
}
}
@@ -0,0 +1,138 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
async@^1.5.2:
version "1.5.2"
resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
[email protected]:
version "1.0.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
corser@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
dependencies:
ms "2.0.0"
debug@^2.2.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
ecstatic@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.0.tgz#91cd417d152abf85b37b1ab3ebf3bd25cdc64e80"
dependencies:
he "^1.1.1"
mime "^1.6.0"
minimist "^1.1.0"
url-join "^2.0.5"
eventemitter3@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
follow-redirects@^1.0.0:
version "1.5.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.8.tgz#1dbfe13e45ad969f813e86c00e5296f525c885a1"
dependencies:
debug "=3.1.0"
he@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
http-proxy@^1.8.1:
version "1.17.0"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
dependencies:
eventemitter3 "^3.0.0"
follow-redirects "^1.0.0"
requires-port "^1.0.0"
http-server@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b"
dependencies:
colors "1.0.3"
corser "~2.0.0"
ecstatic "^3.0.0"
http-proxy "^1.8.1"
opener "~1.4.0"
optimist "0.6.x"
portfinder "^1.0.13"
union "~0.4.3"
mime@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
[email protected]:
version "0.0.8"
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
minimist@^1.1.0:
version "1.2.0"
resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
[email protected]:
version "0.5.1"
resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
opener@~1.4.0:
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
[email protected]:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
portfinder@^1.0.13:
version "1.0.17"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a"
dependencies:
async "^1.5.2"
debug "^2.2.0"
mkdirp "0.5.x"
qs@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404"
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
union@~0.4.3:
version "0.4.6"
resolved "https://registry.yarnpkg.com/union/-/union-0.4.6.tgz#198fbdaeba254e788b0efcb630bc11f24a2959e0"
dependencies:
qs "~2.3.3"
url-join@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
@@ -0,0 +1,396 @@
// direction enums (in clockwise order)
var DIR_UP = 0;
var DIR_RIGHT = 1;
var DIR_DOWN = 2;
var DIR_LEFT = 3;
// get direction enum from a direction vector
var getEnumFromDir = function(dir) {
if (dir.x==-1) return DIR_LEFT;
if (dir.x==1) return DIR_RIGHT;
if (dir.y==-1) return DIR_UP;
if (dir.y==1) return DIR_DOWN;
};
// set direction vector from a direction enum
var setDirFromEnum = function(dir,dirEnum) {
if (dirEnum == DIR_UP) { dir.x = 0; dir.y =-1; }
else if (dirEnum == DIR_RIGHT) { dir.x =1; dir.y = 0; }
else if (dirEnum == DIR_DOWN) { dir.x = 0; dir.y = 1; }
else if (dirEnum == DIR_LEFT) { dir.x = -1; dir.y = 0; }
};
// size of a square tile in pixels
var tileSize = 8;
// constructor
var Map = function(numCols, numRows, tiles) {
// sizes
this.numCols = numCols;
this.numRows = numRows;
this.numTiles = numCols*numRows;
this.widthPixels = numCols*tileSize;
this.heightPixels = numRows*tileSize;
// ascii map
this.tiles = tiles;
this.resetCurrent();
this.parseWalls();
};
// reset current tiles
Map.prototype.resetCurrent = function() {
this.currentTiles = this.tiles.split(""); // create a mutable list copy of an immutable string
};
// This is a procedural way to generate original-looking maps from a simple ascii tile
// map without a spritesheet.
Map.prototype.parseWalls = function() {
var that = this;
// creates a list of drawable canvas paths to render the map walls
this.paths = [];
// a map of wall tiles that already belong to a built path
var visited = {};
// we extend the x range to suggest the continuation of the tunnels
var toIndex = function(x,y) {
if (x>=-2 && x<that.numCols+2 && y>=0 && y<that.numRows)
return (x+2)+y*(that.numCols+4);
};
// a map of which wall tiles that are not completely surrounded by other wall tiles
var edges = {};
var i=0,x,y;
for (y=0;y<this.numRows;y++) {
for (x=-2;x<this.numCols+2;x++,i++) {
if (this.getTile(x,y) == '|' &&
(this.getTile(x-1,y) != '|' ||
this.getTile(x+1,y) != '|' ||
this.getTile(x,y-1) != '|' ||
this.getTile(x,y+1) != '|' ||
this.getTile(x-1,y-1) != '|' ||
this.getTile(x-1,y+1) != '|' ||
this.getTile(x+1,y-1) != '|' ||
this.getTile(x+1,y+1) != '|')) {
edges[i] = true;
}
}
}
// walks along edge wall tiles starting at the given index to build a canvas path
var makePath = function(tx,ty) {
// get initial direction
var dir = {};
var dirEnum;
if (toIndex(tx+1,ty) in edges)
dirEnum = DIR_RIGHT;
else if (toIndex(tx, ty+1) in edges)
dirEnum = DIR_DOWN;
else
throw "tile shouldn't be 1x1 at "+tx+","+ty;
setDirFromEnum(dir,dirEnum);
// increment to next tile
tx += dir.x;
ty += dir.y;
// backup initial location and direction
var init_tx = tx;
var init_ty = ty;
var init_dirEnum = dirEnum;
var path = [];
var pad; // (persists for each call to getStartPoint)
var point;
var lastPoint;
var turn,turnAround;
/*
We employ the 'right-hand rule' by keeping our right hand in contact
with the wall to outline an individual wall piece.
Since we parse the tiles in row major order, we will always start
walking along the wall at the leftmost tile of its topmost row. We
then proceed walking to the right.
When facing the direction of the walk at each tile, the outline will
hug the left side of the tile unless there is a walkable tile to the
left. In that case, there will be a padding distance applied.
*/
var getStartPoint = function(tx,ty,dirEnum) {
var dir = {};
setDirFromEnum(dir, dirEnum);
if (!(toIndex(tx+dir.y,ty-dir.x) in edges))
pad = that.isFloorTile(tx+dir.y,ty-dir.x) ? 5 : 0;
var px = -tileSize/2+pad;
var py = tileSize/2;
var a = dirEnum*Math.PI/2;
var c = Math.cos(a);
var s = Math.sin(a);
return {
// the first expression is the rotated point centered at origin
// the second expression is to translate it to the tile
x:(px*c - py*s) + (tx+0.5)*tileSize,
y:(px*s + py*c) + (ty+0.5)*tileSize,
};
};
while (true) {
visited[toIndex(tx,ty)] = true;
// determine start point
point = getStartPoint(tx,ty,dirEnum);
if (turn) {
// if we're turning into this tile, create a control point for the curve
//
// >---+ <- control point
// |
// V
lastPoint = path[path.length-1];
if (dir.x == 0) {
point.cx = point.x;
point.cy = lastPoint.y;
}
else {
point.cx = lastPoint.x;
point.cy = point.y;
}
}
// update direction
turn = false;
turnAround = false;
if (toIndex(tx+dir.y, ty-dir.x) in edges) { // turn left
dirEnum = (dirEnum+3)%4;
turn = true;
}
else if (toIndex(tx+dir.x, ty+dir.y) in edges) { // continue straight
}
else if (toIndex(tx-dir.y, ty+dir.x) in edges) { // turn right
dirEnum = (dirEnum+1)%4;
turn = true;
}
else { // turn around
dirEnum = (dirEnum+2)%4;
turnAround = true;
}
setDirFromEnum(dir,dirEnum);
// commit path point
path.push(point);
// special case for turning around (have to connect more dots manually)
if (turnAround) {
path.push(getStartPoint(tx-dir.x, ty-dir.y, (dirEnum+2)%4));
path.push(getStartPoint(tx, ty, dirEnum));
}
// advance to the next wall
tx += dir.x;
ty += dir.y;
// exit at full cycle
if (tx==init_tx && ty==init_ty && dirEnum == init_dirEnum) {
that.paths.push(path);
break;
}
}
};
// iterate through all edges, making a new path after hitting an unvisited wall edge
i=0;
for (y=0;y<this.numRows;y++)
for (x=-2;x<this.numCols+2;x++,i++)
if (i in edges && !(i in visited)) {
visited[i] = true;
makePath(x,y);
}
};
Map.prototype.posToIndex = function(x,y) {
if (x>=0 && x<this.numCols && y>=0 && y<this.numRows)
return x+y*this.numCols;
};
// retrieves tile character at given coordinate
// extended to include offscreen tunnel space
Map.prototype.getTile = function(x,y) {
if (x>=0 && x<this.numCols && y>=0 && y<this.numRows)
return this.currentTiles[this.posToIndex(x,y)];
// extend walls and paths outward for entrances and exits
if ((x==-1 && this.getTile(x+1,y)=='|' && (this.isFloorTile(x+1,y+1)||this.isFloorTile(x+1,y-1))) ||
(x==this.numCols && this.getTile(x-1,y)=='|' && (this.isFloorTile(x-1,y+1)||this.isFloorTile(x-1,y-1))))
return '|';
if ((x==-1 && this.isFloorTile(x+1,y)) ||
(x==this.numCols && this.isFloorTile(x-1,y)))
return ' ';
};
// determines if the given character is a walkable floor tile
Map.prototype.isFloorTileChar = function(tile) {
return tile==' ' || tile=='.' || tile=='o';
};
// determines if the given tile coordinate has a walkable floor tile
Map.prototype.isFloorTile = function(x,y) {
return this.isFloorTileChar(this.getTile(x,y));
};
// function to draw the map as a tile map
Map.prototype.draw = function(ctx,left,top,print) {
// save state
ctx.save();
ctx.translate(0.5,0.5); // pixel perfect lines?
// translate to the position of the map
ctx.translate(left,top);
// clip the drawing surface
ctx.beginPath();
ctx.rect(0,0,this.widthPixels, this.heightPixels);
ctx.clip();
if (!print) {
ctx.fillStyle = "#000";
ctx.fillRect(0,0,this.widthPixels, this.heightPixels);
}
// set colors
ctx.fillStyle = print?"#333":this.wallFillColor;
ctx.strokeStyle = print?"#333":this.wallStrokeColor;
var x,y;
var i,j;
var tile;
for (i=0; i<this.paths.length; i++) {
var path = this.paths[i];
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (j=1; j<path.length; j++) {
if (path[j].cx != undefined)
ctx.quadraticCurveTo(path[j].cx, path[j].cy, path[j].x, path[j].y);
else
ctx.lineTo(path[j].x, path[j].y);
}
ctx.quadraticCurveTo(path[j-1].x, path[0].y, path[0].x, path[0].y);
ctx.fill();
ctx.stroke();
}
// draw pellets for each path tile
var pelletSize = print?tileSize:2;
var energizerSize = 3;
for (y=0; y<this.numRows; y++) {
for (x=0; x<this.numCols; x++) {
var t = this.getTile(x,y);
if (t=='o' || t=='.' || t==' ') {
ctx.fillStyle=print?"#bbb":this.pelletColor;
ctx.fillRect(
x*tileSize+tileSize/2-pelletSize/2,
y*tileSize+tileSize/2-pelletSize/2,
pelletSize,pelletSize);
}
}
}
// draw grid
ctx.strokeStyle=print?"rgba(0,0,0,0.3)":"rgba(255,255,255,0.3)";
ctx.beginPath();
for (y=0; y<=this.numRows; y++) {
ctx.moveTo(0,y*tileSize);
ctx.lineTo(this.widthPixels,y*tileSize);
}
for (x=0; x<=this.numCols; x++) {
ctx.moveTo(x*tileSize,0);
ctx.lineTo(x*tileSize,this.heightPixels);
}
ctx.stroke();
// draw title
if (this.name) {
ctx.textBaseline = "top";
ctx.font = "20px sans-serif";
ctx.fillStyle = print?"#000":"#fff";
ctx.fillText(this.name, 0,tileSize/2);
}
ctx.restore();
};
// function to draw the map using simple representation of the paths as straight lines
Map.prototype.drawPath = function(ctx,left,top) {
var print = true;
// save state
ctx.save();
ctx.translate(0.5,0.5); // pixel perfect lines?
// translate to the position of the map
ctx.translate(left,top);
// clip the drawing surface
ctx.beginPath();
ctx.rect(0,0,this.widthPixels, this.heightPixels);
ctx.clip();
var x,y;
var i,j;
var tile;
// draw pellets for each path tile
ctx.lineWidth = 2.0;
ctx.strokeStyle="rgba(0,0,0,0.8)";
ctx.beginPath();
for (y=0; y<this.numRows-1; y++) {
for (x=0; x<this.numCols-1; x++) {
if (this.isFloorTile(x,y)) {
if (this.isFloorTile(x+1,y)) {
ctx.moveTo(x*tileSize,y*tileSize);
ctx.lineTo((x+1)*tileSize,y*tileSize);
}
if (this.isFloorTile(x,y+1)) {
ctx.moveTo(x*tileSize,y*tileSize);
ctx.lineTo(x*tileSize,(y+1)*tileSize);
}
}
}
}
ctx.stroke();
// draw grid
ctx.lineWidth = 1.0;
ctx.strokeStyle=print?"rgba(0,0,0,0.3)":"rgba(255,255,255,0.3)";
ctx.beginPath();
for (y=0; y<this.numRows; y++) {
ctx.moveTo(0,y*tileSize);
ctx.lineTo(this.widthPixels-tileSize,y*tileSize);
}
for (x=0; x<this.numCols; x++) {
ctx.moveTo(x*tileSize,0);
ctx.lineTo(x*tileSize,this.heightPixels-tileSize);
}
ctx.stroke();
// draw title
if (this.name) {
ctx.fillStyle = print?"#000":"#fff";
ctx.font = "20px sans-serif";
ctx.textBaseline = "top";
ctx.fillText(this.name, 0,tileSize/2);
}
ctx.restore();
};
@@ -0,0 +1,32 @@
# Generate Pac-Man Mazes using Tetris-stacking
## Progress
This is a sample of the current generating model:
<img src="working.png" />
[Click here to view the actual demo.](http://shaunlebron.github.com/pacman-mazegen)
## Summary
In the pursuit of a simple maze generator for Pac-Man, we first visualize the
structure of the original Pac-Man maps as a tiling of blocks. Then, we attempt
to simplify this structure by lowering its resolution while still maintaining features.
(The maps are symmetric, so only the middle to the right half are shown.)
The first row shows the simplified representations. The second row shows the
edits to each cell that must be performed after upscaling by a factor of 3.
A blue down arrow means the cell's height must be increased by 1. A red left
arrow means the cell's width must be decreased by 1.
<img src="simplify.png" />
We propose that one may generate a random simplified map (phase 1), then transform
it to a correctly sized map by upscaling and applying some clever
shifting/resizing of a few key wall segments (phase 2).
## Contents
* index.htm currently displays a demo of random simple maps using mapgen.js.
* drawpresets.htm draws the simplified versions of the original maps.
@@ -0,0 +1,149 @@
// source: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number l The lightness
* @return Array The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
r *= 255;
g *= 255;
b *= 255;
return [r,g,b];
}
/**
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and v in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSV representation
*/
function rgbToHsv(r, g, b){
r = r/255, g = g/255, b = b/255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
if(max == min){
h = 0; // achromatic
}else{
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, v];
}
/**
* Converts an HSV color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number v The value
* @return Array The RGB representation
*/
function hsvToRgb(h, s, v){
var r, g, b;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch(i % 6){
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
r *= 255;
g *= 255;
b *= 255;
return [r,g,b];
}
function rgbString(rgb) {
var r = Math.floor(rgb[0]);
var g = Math.floor(rgb[1]);
var b = Math.floor(rgb[2]);
return 'rgb('+r+','+g+','+b+')';
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
<!doctype html>
<html>
<head>
<script>
var UP = 0;
var RIGHT = 1;
var DOWN = 2;
var LEFT = 3;
var rows = 9;
var cols = 5;
var createCells = function() {
var cells = {};
for (i=0; i<rows*cols; i++) {
cells[i] = {
x: i%cols,
y: Math.floor(i/cols),
filled: false,
connect: [true, true, true, true],
colors: {},
next: [],
no: undefined,
group: undefined,
};
}
for (i=0; i<rows*cols; i++) {
var c = cells[i];
if (c.x > 0)
c.next[LEFT] = cells[i-1];
if (c.x < cols - 1)
c.next[RIGHT] = cells[i+1];
if (c.y > 0)
c.next[UP] = cells[i-cols];
if (c.y < rows - 1)
c.next[DOWN] = cells[i+cols];
}
return {
makeWalls: function(walls, color) {
color = color || "#000";
var wallLen = walls.length;
var i,j,x,y,dir;
var wall;
for (i=0; i<wallLen; i++) {
wall = walls[i];
var row = wall[0];
var col = wall[1];
var dir = wall[2];
if (dir.length) {
var dirlen = dir.length;
for (j=0; j<dirlen; j++) {
this.makeWall(row,col,dir[j], color);
}
}
else {
this.makeWall(row,col,dir, color);
}
}
},
makeWall: function(row, col, dir, color) {
color = color || "#000";
var cell = cells[col+row*cols];
cell.connect[dir] = false;
cell.colors[dir] = color;
cell.next[dir].connect[(dir+2)%4] = false;
cell.next[dir].colors[(dir+2)%4] = color;
if (cell.x == 0 && dir == RIGHT) {
cell.connect[LEFT] = false;
cell.colors[LEFT] = color;
}
},
display: function(ctx,x,y) {
var canvas = document.createElement('canvas');
var pad = 5;
var size = 40;
canvas.width = size*cols+pad*2;
canvas.height = size*rows+pad*2;
document.getElementById('container').appendChild(canvas);
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#FFF";
ctx.fillRect(0,0,canvas.width,canvas.height);
drawCells(ctx,cells,pad,pad,size);
},
};
};
var drawCells = function(ctx,cells,left,top,size,title,options) {
title = title || "";
options = options || {};
ctx.save();
ctx.translate(left,top);
// draw title
ctx.font = "bold " + size/3 + "px sans-serif";
ctx.textBaseline = "bottom";
ctx.textAlign = "left";
ctx.fillText(title, 0, -5);
ctx.beginPath();
for (y=0; y<=rows; y++) {
ctx.moveTo(0,y*size);
ctx.lineTo(cols*size,y*size);
}
for (x=0; x<=cols; x++) {
ctx.moveTo(x*size,0);
ctx.lineTo(x*size,rows*size);
}
ctx.lineWidth = "1";
ctx.strokeStyle = "#AAA";
ctx.stroke();
// set cell number font
ctx.font = size/3 + "px sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
var arrowsize = size/6;
ctx.lineWidth = "3";
for (i=0; i<cols*rows; i++) {
var c = cells[i];
var x = i % cols;
var y = Math.floor(i / cols);
if (options.drawRaiseHeightCandidate && c.isRaiseHeightCandidate) {
ctx.fillStyle = "rgba(0,0,255,0.2)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawShrinkWidthCandidate && c.isShrinkWidthCandidate) {
ctx.fillStyle = "rgba(255,0,0,0.2)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawJoinCandidate && c.isJoinCandidate) {
ctx.fillStyle = "rgba(0,255,0,0.2)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawSingleDeadEnd && c.isSingleDeadEndCandidate) {
ctx.fillStyle = "rgba(255,255,0,0.4)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawDoubleDeadEnd && c.isDoubleDeadEndCandidate) {
ctx.fillStyle = "rgba(0,255,255,0.2)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawVoidTunnel && c.isVoidTunnelCandidate) {
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(x*size,y*size,size,size);
}
if (options.drawChosenTunnel && c.topTunnel) {
ctx.beginPath();
ctx.save();
ctx.translate(x*size+size/2,y*size+5);
ctx.moveTo(-arrowsize,arrowsize);
ctx.lineTo(0,0);
ctx.lineTo(arrowsize,arrowsize);
ctx.strokeStyle = "rgba(0,255,0,0.7)";
ctx.stroke();
ctx.restore();
}
else if (options.drawEdgeTunnel && c.isEdgeTunnelCandidate) {
ctx.beginPath();
ctx.save();
ctx.translate(x*size+size/2,y*size+5);
ctx.moveTo(-arrowsize,arrowsize);
ctx.lineTo(0,0);
ctx.lineTo(arrowsize,arrowsize);
ctx.strokeStyle = "rgba(0,0,0,0.7)";
ctx.stroke();
ctx.restore();
}
if (options.drawRaiseHeight && c.raiseHeight) {
ctx.beginPath();
ctx.save();
ctx.translate(x*size+size/2,y*size+size-arrowsize);
ctx.moveTo(-arrowsize,-arrowsize);
ctx.lineTo(0,0);
ctx.lineTo(arrowsize,-arrowsize);
ctx.strokeStyle = "rgba(0,0,255,0.7)";
ctx.stroke();
ctx.restore();
}
if (options.drawShrinkWidth && c.shrinkWidth) {
ctx.beginPath();
ctx.save();
ctx.translate(x*size+size-arrowsize-arrowsize,y*size+size/2);
ctx.moveTo(arrowsize,-arrowsize);
ctx.lineTo(0,0);
ctx.lineTo(arrowsize,arrowsize);
ctx.restore();
ctx.strokeStyle = "rgba(255,0,0,0.7)";
ctx.stroke();
}
// draw cell number (order)
if (options.drawNumbers && c.no != undefined) {
ctx.fillStyle = "#000";
ctx.fillText(c.no, x*size+size/2, y*size+size/2);
}
}
var i;
for (i=0; i<cols*rows; i++) {
var c = cells[i];
var x = i % cols;
var y = Math.floor(i / cols);
// draw walls
ctx.lineWidth = "3";
ctx.lineCap = 'round';
if (!c.connect[UP]) {
ctx.beginPath();
ctx.moveTo(x*size, y*size);
ctx.lineTo(x*size+size, y*size);
ctx.strokeStyle = c.colors[UP];
ctx.stroke();
}
if (!c.connect[DOWN]) {
ctx.beginPath();
ctx.moveTo(x*size, y*size+size);
ctx.lineTo(x*size+size, y*size+size);
ctx.strokeStyle = c.colors[DOWN];
ctx.stroke();
}
if (!c.connect[LEFT]) {
ctx.beginPath();
ctx.moveTo(x*size, y*size);
ctx.lineTo(x*size, y*size+size);
ctx.strokeStyle = c.colors[LEFT];
ctx.stroke();
}
if (!c.connect[RIGHT]) {
ctx.beginPath();
ctx.moveTo(x*size+size, y*size);
ctx.lineTo(x*size+size, y*size+size);
ctx.strokeStyle = c.colors[RIGHT];
ctx.stroke();
}
}
ctx.restore();
};
</script>
<style>
body {
background: #DDD;
}
#container {
padding-left: 40px;
padding-top: 20px;
padding-bottom: 20px;
padding-right: 40px;
margin-left:auto;
margin-right:auto;
background: #FFF;
width: 500px;
}
</style>
</head>
<body>
<div id='container'>
<h1>Pac-Man Maze Generation</h1>
<p>We start with the following 5x9 grid of tiles:</p>
<script>
var cells = createCells();
cells.makeWalls([
[3,0,UP],
[3,1,[UP,RIGHT]],
[4,1,[DOWN,RIGHT]],
[4,0,DOWN],
],"#F00");
cells.makeWalls([
[6,0,DOWN]
],"#00F");
cells.display();
</script>
<p>In <span style="color:red">red</span> is the path around the ghost pen.
In <span style="color:blue">blue</span> is Pac-Man's starting position.</p>
<p>
We then proceed by playing tetris. Pieces fall to the left-hand side.
No gaps can be made.
</p>
<p>
The edges create paths inside the maze. We erase some of them on the
border for variation.
</p>
<p>
</p>
</div>
</body>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<script src="mapgen.js"></script>
<script src="colors.js"></script>
<script src="Map.js"></script>
</head>
<body>
<p>
<button onclick="draw()">Click to generate new maps</button>
</p>
<canvas id='canvas'></canvas>
<script>
var draw = function() {
var canvas = document.getElementById('canvas');
var rows = 4;
var cols = 4;
var w,h;
w = 28*8;
h = 36*8;
canvas.width = w*cols;
canvas.height = h*rows;
var ctx = canvas.getContext('2d');
var map;
var x,y;
for (y=0; y<rows; y++) {
for (x=0; x<cols; x++) {
map = mapgen();
map.draw(ctx,x*w,y*h);
}
}
//drawResult(ctx,(cols+2)*size,size,size);
//drawResult2(ctx,(2*cols+3)*size,size,size);
//drawTiles(ctx,(cols+2)*size,size,size);
};
window.onload = function() {
draw();
};
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB