1/*
2qmasm-maze generates mazes for solution by QMASM and validates solutions
3returned from QMASM.
4
5Author: Scott Pakin, pakin@lanl.gov
6*/
7package main
8
9import (
10	"fmt"
11	"io"
12	"log"
13	"math/rand"
14	"os"
15	"path"
16	"strconv"
17	"strings"
18	"time"
19
20	"bufio"
21	"errors"
22	"github.com/spakin/disjoint"
23)
24
25// notify is used to write status messages to the user.
26var notify *log.Logger
27
28// A Room is identified by its walls and by the other rooms it can reach.
29type Room struct {
30	N       bool              // North side of room is a wall
31	S       bool              // South side of room is a wall
32	E       bool              // East side of room is a wall
33	W       bool              // West side of room is a wall
34	Reaches *disjoint.Element // Element in a set of reachable rooms
35}
36
37// A Maze is a 2-D array of Rooms.
38type Maze [][]Room
39
40// RandomMaze creates a maze of given dimensions.
41func RandomMaze(w, h int) Maze {
42	// Allocate and initialize the maze to all walls present.
43	maze := make([][]Room, h)
44	for y := range maze {
45		maze[y] = make([]Room, w)
46		for x := range maze[y] {
47			// Start with all walls present and no other rooms reachable.
48			maze[y][x].N = true
49			maze[y][x].S = true
50			maze[y][x].E = true
51			maze[y][x].W = true
52			maze[y][x].Reaches = disjoint.NewElement()
53		}
54	}
55
56	// Repeatedly remove walls until a single connected component remains.
57	for cc := w * h; cc > 1; {
58		// Because of symmetry, we need only connect to the right or
59		// down rather than in all four directions.
60		x0 := rand.Intn(w)
61		y0 := rand.Intn(h)
62		x1 := x0
63		y1 := y0
64		dir := rand.Intn(2)
65		if dir == 0 && x0 < w-1 {
66			x1++ // Go right.
67		} else if dir == 1 && y0 < h-1 {
68			y1++ // Go down.
69		} else {
70			continue // Can't go in the desired direction
71		}
72		if maze[y0][x0].Reaches.Find() == maze[y1][x1].Reaches.Find() {
73			continue // Already connected
74		}
75
76		// Tear down the wall.
77		if dir == 0 {
78			// Right/left
79			maze[y0][x0].E = false
80			maze[y1][x1].W = false
81		} else {
82			// Down/up
83			maze[y0][x0].S = false
84			maze[y1][x1].N = false
85		}
86		disjoint.Union(maze[y0][x0].Reaches, maze[y1][x1].Reaches)
87		cc--
88	}
89
90	// Punch one hole on the top and one hole on the bottom.
91	maze[0][rand.Intn(w)].N = false
92	maze[h-1][rand.Intn(w)].S = false
93
94	// Return the generated maze.
95	return maze
96}
97
98// ColName maps the numbers {0, 1, 2, ..., 675} to the strings {"A", "B", "C",
99// ..., "ZZ"}.
100func ColName(c int) string {
101	switch {
102	case c < 26:
103		return fmt.Sprintf("%c", c+'A')
104
105	case c < 26*26:
106		return fmt.Sprintf("%c%c", c/26+'A'-1, c%26+'A')
107
108	default:
109		notify.Fatalf("Invalid column number %d", c)
110	}
111	return "" // Will never get here.
112}
113
114// RoomName maps 0-based row and column numbers to letter-number strings (e.g.,
115// "J15").
116func RoomName(r, c int) string {
117	return fmt.Sprintf("%s%d", ColName(c), r+1)
118}
119
120// Write outputs a Maze with a given row prefix.  We assume the maze contains
121// fewer than 677 columns and 1000 rows.
122func (m Maze) Write(w io.Writer, pfx string) {
123	// Output a column header.
124	fmt.Fprint(w, pfx+"    ")
125	for c := range m[0] {
126		fmt.Fprintf(w, " %-2s", ColName(c))
127	}
128	fmt.Fprint(w, "\n")
129
130	// Output each row in turn.
131	for r, row := range m {
132		// Output the row's northern walls as one line of ASCII graphics.
133		fmt.Fprint(w, pfx+"    ")
134		for _, cell := range row {
135			if cell.N {
136				fmt.Fprintf(w, "+--")
137			} else {
138				fmt.Fprintf(w, "+  ")
139			}
140		}
141		fmt.Fprintln(w, "+")
142
143		// Output the row's western walls as another line of ASCII
144		// graphics.
145		fmt.Fprintf(w, "%s%3d ", pfx, r+1)
146		for _, cell := range row {
147			if cell.W {
148				fmt.Fprintf(w, "|  ")
149			} else {
150				fmt.Fprintf(w, "   ")
151			}
152		}
153
154		// End the line with the single, easternmost wall.
155		if row[len(row)-1].E {
156			fmt.Fprintln(w, "|")
157		} else {
158			fmt.Fprintln(w, "")
159		}
160	}
161
162	// Output the bottomost row's southern walls as a final line of ASCII
163	// graphics.
164	fmt.Fprint(w, pfx+"    ")
165	for _, cell := range m[len(m)-1] {
166		if cell.S {
167			fmt.Fprintf(w, "+--")
168		} else {
169			fmt.Fprintf(w, "+  ")
170		}
171	}
172	fmt.Fprintln(w, "+")
173}
174
175// WriteHeader writes some boilerplate header to a file.
176func WriteHeader(w io.Writer, m Maze) {
177	fmt.Fprintln(w, `#########################################
178# Find the shortest path through a maze #
179# By Scott Pakin <pakin@lanl.gov>       #
180#########################################
181
182# This is a generated file.
183# Command line:`,
184		strings.Join(os.Args, " "), `
185
186# Maze to solve:
187#`)
188	m.Write(w, "# ")
189	fmt.Fprintln(w, `
190# Truth table for a room:
191#
192#   0 0 0 0
193#   0 0 1 1
194#   0 1 0 1
195#   0 1 1 0
196#   1 0 0 1
197#   1 0 1 0
198#   1 1 0 0
199
200# Define a macro for a room that has the preceding truth table as
201# the degenerate ground state of the corresponding Hamiltonian.
202!begin_macro room
203N   0.50
204E   0.50
205S   0.50
206W   0.50
207$a1 1.00
208
209N E   0.25
210N S   0.25
211N W   0.25
212N $a1 0.50
213E S   0.25
214E W   0.25
215E $a1 0.50
216S W   0.25
217S $a1 0.50
218W $a1 0.50
219!end_macro room
220
221# Define some helpful aliases.
222!let egress := TRUE
223!let wall   := FALSE
224`)
225}
226
227// WriteRooms writes the rooms of a maze to a file.
228func WriteRooms(w io.Writer, m Maze) {
229	fmt.Fprintln(w, "# Output in turn each room of the maze.")
230	for r, row := range m {
231		for c, cell := range row {
232			fmt.Fprintln(w, "")
233			rstr := RoomName(r, c)
234			fmt.Fprintf(w, "!use_macro room %s\n", rstr)
235
236			// Output egresses (which always face north or south in
237			// the current implementation).
238			if r == 0 && !cell.N {
239				fmt.Fprintln(w, rstr+".N := egress")
240			}
241			if r == len(m)-1 && !cell.S {
242				fmt.Fprintln(w, rstr+".S := egress")
243			}
244
245			// Output all walls.
246			if cell.N {
247				fmt.Fprintln(w, rstr+".N := wall")
248			}
249			if cell.E {
250				fmt.Fprintln(w, rstr+".E := wall")
251			}
252			if cell.S {
253				fmt.Fprintln(w, rstr+".S := wall")
254			}
255			if cell.W {
256				fmt.Fprintln(w, rstr+".W := wall")
257			}
258
259			// Output northern and western paths.  (The others are
260			// symmetric.)
261			if r > 0 && !cell.N {
262				fmt.Fprintf(w, "%s.N = %s.S\n", rstr, RoomName(r-1, c))
263			}
264			if c > 0 && !cell.W {
265				fmt.Fprintf(w, "%s.W = %s.E\n", rstr, RoomName(r, c-1))
266			}
267		}
268	}
269}
270
271// GenerateMaze generates a maze of given dimensions.
272func GenerateMaze(out io.Writer, w, h int) {
273	// Validate the dimensions provided.
274	if w < 1 || h < 1 {
275		notify.Fatal("Mazes must contain at least one row and one column")
276	}
277	if w >= 676 || h > 999 {
278		notify.Fatal("Mazes must contain no more than 999 rows and 676 columns")
279	}
280
281	// Seed the random-number generator.
282	seed := time.Now().UnixNano() * int64(os.Getpid())
283	rand.Seed(seed)
284
285	// Generate a maze.
286	maze := RandomMaze(w, h)
287
288	// Output QMASM code.
289	WriteHeader(out, maze)
290	WriteRooms(out, maze)
291}
292
293// NewMaze allocates a new, empty Maze.
294func NewMaze() Maze {
295	m := make([][]Room, 0, 10)
296	for i := range m {
297		m[i] = make([]Room, 0, 10)
298	}
299	return m
300}
301
302// Extend extends a maze by parsing a coordinate and a Boolean value.
303func (m Maze) Extend(s string, b bool) Maze {
304	// Parse the string.  Invalid input strings do not extend the maze.
305	var i int
306	col := 0
307	for i = 0; s[i] >= 'A' && s[i] <= 'Z'; i++ {
308		col = col*26 + int(s[i]-'A')
309	}
310	row := 0
311	for ; s[i] >= '0' && s[i] <= '9'; i++ {
312		row = row*10 + int(s[i]-'0')
313	}
314	row--
315	if row < 0 {
316		return m // Invalid room specification
317	}
318	i++ // Skip the "."
319	dir := s[i]
320	switch dir {
321	case 'N', 'E', 'S', 'W':
322	default:
323		return m // Invalid room specification
324	}
325
326	// Add rows and columns as necessary.  We assume we'll typically be
327	// adding rooms in order.
328	for row >= len(m) {
329		m = append(m, make([]Room, 1))
330	}
331	for col >= len(m[row]) {
332		m[row] = append(m[row], Room{})
333	}
334
335	// Update the specified room.
336	switch dir {
337	case 'N':
338		m[row][col].N = b
339	case 'S':
340		m[row][col].S = b
341	case 'E':
342		m[row][col].E = b
343	case 'W':
344		m[row][col].W = b
345	default:
346		panic("Invalid direction") // We should never get here.
347	}
348	return m
349}
350
351// ReadSolutions returns a list of maze solutions and their tallies as read
352// from a Reader.
353func ReadMazes(r *bufio.Reader) ([]Maze, []int) {
354	mazes := make([]Maze, 0, 1)  // List of mazes to return
355	var m Maze                   // Current maze
356	tallies := make([]int, 0, 1) // List of tallies to return
357	var t int64                  // Current tally
358	haveSoln := false            // true=saw at least one Solution; false=still in header text
359	for {
360		// Read a line from the file and split it into fields.
361		ln, err := r.ReadString('\n')
362		if err == io.EOF {
363			break
364		}
365		if err != nil {
366			notify.Fatal(err)
367		}
368		f := strings.Fields(ln)
369		if len(f) == 0 {
370			continue
371		}
372
373		// Process the current line.
374		switch {
375		case f[0] == "Solution":
376			// Start a new maze when we see "Solution".
377			haveSoln = true
378			if m != nil {
379				mazes = append(mazes, m)
380				tallies = append(tallies, int(t))
381			}
382			m = NewMaze()
383			t, err = strconv.ParseInt(f[7][:len(f[7])-2], 10, 0)
384			if err != nil {
385				notify.Fatal(err)
386			}
387
388		case !haveSoln:
389			// Don't get confused by header text.
390
391		case len(f) == 2 && (f[1] == "True" || f[1] == "False"):
392			// Append a room to the current maze when we see a
393			// solution row.
394			m = m.Extend(f[0], f[1] == "True")
395		}
396	}
397	if m != nil {
398		mazes = append(mazes, m)          // Final maze
399		tallies = append(tallies, int(t)) // Final tally
400	}
401	return mazes, tallies
402}
403
404// NextRoom maps a "from" direction (N, S, E, or W) and room
405// coordinates to a new "from" direction and room coordinates.
406func (m Maze) NextRoom(r, c int, dir string) (int, int, string) {
407	room := m[r][c]
408	switch {
409	case room.N && dir != "N":
410		return r - 1, c, "S"
411	case room.E && dir != "E":
412		return r, c + 1, "W"
413	case room.S && dir != "S":
414		return r + 1, c, "N"
415	case room.W && dir != "W":
416		return r, c - 1, "E"
417	default:
418		panic("Unexpectedly stuck in a room") // Should never get here.
419	}
420}
421
422// PathString returns a path through a maze or an error.
423func (m Maze) PathString() (string, error) {
424	// Ensure that each room has either zero or two exits.
425	bool2int := map[bool]int{true: 1, false: 0}
426	for r, row := range m {
427		for c, cell := range row {
428			n := bool2int[cell.N] + bool2int[cell.E] + bool2int[cell.S] + bool2int[cell.W]
429			switch n {
430			case 0:
431			case 2:
432			default:
433				return "", fmt.Errorf("Room %s contains %d exit(s) but should have 0 or 2", RoomName(r, c), n)
434			}
435		}
436	}
437
438	// Find the ingress and egress columns.  Complain if more than one of
439	// each is found.
440	cin := -1
441	for c, cell := range m[0] {
442		if cell.N {
443			if cin == -1 {
444				cin = c
445			} else {
446				return "", fmt.Errorf("More than one ingress exists on the top row (%s and %d)", RoomName(0, cin), RoomName(0, c))
447			}
448		}
449	}
450	if cin == -1 {
451		return "", errors.New("No ingress found in the top row")
452	}
453	nrows := len(m)
454	cout := -1
455	for c, cell := range m[nrows-1] {
456		if cell.S {
457			if cout == -1 {
458				cout = c
459			} else {
460				return "", fmt.Errorf("More than one egress exists on the bottom row (%s and %d)", RoomName(nrows-1, cout), RoomName(nrows-1, c))
461			}
462		}
463	}
464	if cout == -1 {
465		return "", errors.New("No egress found in the bottom row")
466	}
467
468	// Complain if the maze is open on the left or right.
469	for r, row := range m {
470		if row[0].W {
471			return "", fmt.Errorf("Maze unexpectedly exits to the left at %s", RoomName(r, 0))
472		}
473		ncols := len(row)
474		if row[ncols-1].E {
475			return "", fmt.Errorf("Maze unexpectedly exits to the right at %s", RoomName(r, ncols-1))
476		}
477	}
478
479	// Starting from the ingress, make our way to the egress.
480	path := make([]string, 0, nrows*nrows)
481	for r, c, d := 0, cin, "N"; r != nrows-1 || c != cout; r, c, d = m.NextRoom(r, c, d) {
482		path = append(path, RoomName(r, c))
483	}
484	path = append(path, RoomName(nrows-1, cout))
485	return strings.Join(path, " -- "), nil
486}
487
488// ValidatePaths reports if a path through a maze looks valid.
489func ValidatePaths(r io.Reader) {
490	// Ensure we can read line-by-line.
491	rb, ok := r.(*bufio.Reader)
492	if !ok {
493		rb = bufio.NewReader(r)
494	}
495
496	// Read a list of mazes.
497	mazes, tallies := ReadMazes(rb)
498
499	// Process each maze in turn.
500	for i, m := range mazes {
501		fmt.Printf("Solution %d (tally: %d): ", i+1, tallies[i])
502		s, err := m.PathString()
503		if err != nil {
504			fmt.Printf("[%s]\n", err)
505		} else {
506			fmt.Println(s)
507		}
508	}
509}
510
511func main() {
512	// Parse the command line.
513	notify = log.New(os.Stderr, path.Base(os.Args[0])+": ", 0)
514	switch {
515	case len(os.Args) == 4 && os.Args[1] == "gen":
516		// Mode 1: Generate a maze.
517		w, err := strconv.Atoi(os.Args[2])
518		if err != nil {
519			notify.Fatal(err)
520		}
521		h, err := strconv.Atoi(os.Args[3])
522		if err != nil {
523			notify.Fatal(err)
524		}
525		GenerateMaze(os.Stdout, w, h)
526
527	case len(os.Args) == 2 && os.Args[1] == "validate":
528		// Mode 2: Validate QMASM maze output.
529		ValidatePaths(os.Stdin)
530
531	default:
532		notify.Fatalf("Usage: %s gen <width> <height> | %s validate", os.Args[0], os.Args[0])
533	}
534}
535