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