#!/usr/local/bin/perl
#
#############################################################################
# this program generates mazes on a text terminal and allows you to traverse 
# them. it requires the Term::ReadKey module (available from www.cpan.org) 
# to automatically size the mazes (-m flag).
#
# the current version of this program is available at:
#
# http://www.robobunny.com/projects/textmaze
#
# run textmaze -h for usage information
#
#############################################################################
# Author:
#   Kirk Baucom <kbaucom@schizoid.com>
#
# Contributors:
#   Kyle Guilbert <kguilber@coe.neu.edu>: maze solve code
#   Juan Orlandini <jorlandini@DATALINK.com>: move count, additional move keys
#
# License:
#
# Copyright (C) 2001 Kirk Baucom (kbaucom@schizoid.com)
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#############################################################################

$SIG{'INT'} = 'quit';
$SIG{'KILL'} = 'quit';

require Term::Cap;

# 9600 bps is fast enough for anybody!
$terminal = Tgetent Term::Cap {TERM => undef, OSPEED => 9600};
$| = 1;
$VERSION = 0.5;

######## CONFIGURATION #########

## controls ## 
# yes, i know these shouldn't be hard coded, but i can't get the Term::Cap
# values to work right...
# $up = $terminal->{_ku};      # up arrow

$up = "\e[A";      # up arrow
$down = "\e[B";    # down arrow
$left = "\e[D";    # left arrow
$right = "\e[C";   # right arrow

# "user defineable" keys, although they aren't any more user defineable
# than the others. i'm sure the arrow key bindings above won't work for
# some people, and Juan Orlandini wanted vi style keys anyway. feel free
# to set them to whatever you want.
$u_up = "k";      # up arrow
$u_down = "j";    # down arrow
$u_left = "h";    # left arrow
$u_right = "l";   # right arrow

$redraw = chr(12); # ctrl-l

## colors to use, if the maze is in color
# format: \e[{foreground};{background}
# add an 'm' to the end for bold forground
#
# color		background	foreground
#
# black		40		30
# red		41		31
# green		42		32
# yellow	43		33
# blue		44		34
# magenta	45		35
# cyan		46		36
# white		47		37

$c_wall = '37m';
$c_trail = "\e[42;${c_wall}";
$c_cursor = "\e[43;30m";
$c_finish = "\e[40;31m";
$c_unseen = "\e[40;${c_wall}";

## default maze size ##
$default_width = 20;
$default_height = 20;

######### END CONFIGURATION ###########

##### DON'T EDIT BELOW HERE UNLESS YOU KNOW WHAT YOU (and i) ARE DOING #####
# what each array element is
$north = 0;
$south = 1;
$east = 2;
$west = 3;
$set = 4;
$setlist = 5;

# set values after generating maze:
$empty = 0;
$cursor_pos = -1;
$maze_end = -2;
$trail = -3;

# find the width and height of the terminal
if(-t STDOUT && eval "require Term::ReadKey") {
  import Term::ReadKey;
  ($screen_width, $screen_height, $junk, $junk) = GetTerminalSize('STDOUT');
  $autosize = 1; # so we know if we can limit the max size or not
}
else {
  $screen_width = 80;
  $screen_height = 24;
}

if(($screen_width < 11) || ($screen_height < 5)) {
  help("Your terminal screen must be at least 11 x 5");
}

if(($default_width * 2) > $screen_width) { $default_width = int($screen_width / 2); }
if($default_height +1 > $screen_height) { $default_height = $screen_height - 1; }

# get the width, height, and random seed
($w, $h, $seed) = getinput();

$size = $w * $h;

system("clear");

unless($dissolve) { print "Initializing maze...\n"; }
initialize($size);  

if($dissolve) { tty_display(); }
else { print "Generating maze...\n"; }
generate_maze($seed);
unless($dissolve) { system("clear"); }

tty_display();

#### MAIN LOOP ####
system("stty raw -echo");
while(1) {
  $input = getc(STDIN);
  if($input eq "\e") {
    $input .= getc(STDIN) . getc(STDIN);
  }
  # process the input
  move($input);    
}

############ SUBROUTINES ###############

## create the grid (although it is stored as a flat list)
sub initialize {
  my $size = shift;
  for ($i=0; $i<$size; $i++) {
      $maze[$i][$set] = $i;
      @{$maze[$i][$setlist]} = ("$i");
      $maze[$i][$north] = $maze[$i][$south] = $maze[$i][$west]
        = $maze[$i][$east] = "NULL";
  }
}

## yank walls out until we have a real maze
sub generate_maze { 
  my $seed = shift;
  srand($seed);
  $cell_num;     # current cell that is being looked at
  $dir; 	 # direction to check
  $counter;      # how we tell when we are done generating the maze
  $entry;        # where the maze starts
  $exit;         # where the maze ends

  my $unified = 0;  # maze is finished when this is equal to the # of cells
                    # in the maze

  $starttime = time();
  while($unified < $size -1) {
    $cell_num = int(rand($size)); #pick a cell
    $dir= int(rand(4));             #pick a direction
    if($dir == $north) {
      if(($cell_num >= $w) && 
           ($maze[$cell_num][$set] != $maze[$cell_num-$w][$set])) {
        $maze[$cell_num][$north] = $cell_num - $w;
        $maze[$cell_num - $w][$south] = $cell_num;
        $unified = unify($maze[$cell_num][$set], $maze[$cell_num - $w][$set], $unified);
        if($dissolve) { clearwall($cell_num - $w, $south); }
      }
    }

    elsif($dir == $south) {
      if( ($cell_num<($w*($h-1)) ) && 
           ($maze[$cell_num][$set] != $maze[$cell_num+$w][$set]) ) {
        $maze[$cell_num][$south] = $cell_num + $w;
        $maze[$cell_num + $w][$north] = $cell_num;
        $unified = unify($maze[$cell_num][$set], $maze[$cell_num + $w][$set], $unified);
        if($dissolve) { clearwall($cell_num, $dir); }
      }
    }

    elsif($dir == $east) {
      if( ($cell_num % $w) != ($w - 1) && 
              ($maze[$cell_num][$set] != $maze[$cell_num+1][$set])) {
        $maze[$cell_num][$east] = $cell_num + 1;
        $maze[$cell_num + 1][$west] = $cell_num;
        $unified = unify($maze[$cell_num][$set], $maze[$cell_num +1][$set], $unified);
        if($dissolve) { clearwall($cell_num, $dir); }
      }
    }

    elsif($dir == $west) {
      if( (($cell_num % $w) != 0) &&
               ($maze[$cell_num][$set] != $maze[$cell_num-1][$set])) {
        $maze[$cell_num][$west] = $cell_num - 1;
        $maze[$cell_num - 1][$east] = $cell_num;
        $unified = unify($maze[$cell_num][$set], $maze[$cell_num-1][$set], $unified);
        if($dissolve) { clearwall($cell_num, $dir); }
      }
    }
  }
  $entry = ( int(rand($h)) * $w ) + ($w - 1);  # beginning of maze
  $exit = int(rand($h)) * $w;		   # end of maze
  $maze[$entry][$east] = $entry;           # entry and exit point to themselves
  $maze[$exit][$west] = $exit;             #   so that you can't leave the maze
  $maze[$exit][$set] = $maze_end;
  $cursor = $entry;                        # put the cursor at the entry point
  $maze[$cursor][$set] = $cursor_pos;
  $endtime = time();
}


# puts two cells in the same set, used when generating the maze
sub unify {
  my ($a, $b, $unified) = @_;  # set numbers for the two sets to combine
  $unified++;

  if($a < $b) { $x = $a; $y = $b; }
  else        { $y = $a; $x = $b; }

  for(@{$maze[$y][$setlist]}) {
    $maze[$_][$set] = $x;
  }
  push(@{$maze[$x][$setlist]}, @{$maze[$y][$setlist]});
  @{$maze[$y][$setlist]} = ();

  return($unified);
}


# redraw current and last cursor positions
sub update { 
  
  my $size = $w * $h;
  foreach $i (@_) {
    $c = 2 * ($i % $w);
    $r = int($i / $w);
    $terminal->Tgoto('cm', $c+1, $r+1, STDOUT);

    # use this to mark the path you have traveled
    if($color && $maze[$i][$set] == $trail) {
      print $c_trail;
    }

    # cursor position
    if ( $maze[$i][$set] == $cursor_pos) {
      if($color) { print $c_cursor, "*", $c_trail; }
      else { print "\e[7m\e[4m*\e[0m"; }
    }
    elsif($maze[$i][$south] eq "NULL") {
      print "_";
    }
    else {
      print " ";
    }

    if($maze[$i][$east] eq "NULL") {
      if($color) { print $c_unseen; }
      print "|";
    }
    elsif($maze[$i][$set] != $trail) {
      print "_";
    }
    if( $maze[$i][$set] == $cursor_pos || $maze[$i][$set] == $trail ) {
      print "\e[0m";
    }    
  }
  $terminal->Tgoto('cm', 0, 79, STDOUT);
}

# this routine is used in "dissolve" (-d) mode to remove walls
sub clearwall {
  my ($cell, $dir) = @_;

  next if(($cell < 0) || ($cell >= ($w * $h))); # shouldn't happen...
  $c = 2 * ($cell % $w);
  $r = int($cell / $w) +1;

  if($dir eq $west) {
    $terminal->Tgoto('cm', $c, $r, STDOUT);
    if($maze[$i][$west] eq "NULL") {
      print "|";
    }
    else {
      print "_";
    }
  }
  elsif($dir eq $south) {
    $terminal->Tgoto('cm', $c+1, $r, STDOUT);
    if($maze[$i][$south] eq "NULL") {
      print "_";
    }
    else {
      print " ";
    }
  }
  elsif($dir eq $east) {
    $terminal->Tgoto('cm', $c+2, $r, STDOUT);
    if($maze[$i][$east] eq "NULL") {
      print "|";
    }
    else {
      print "_";    
    }
  }
}

## display the maze
sub tty_display { 

  my $size = $w * $h;
  $terminal->Tgoto('cm', 0, 0, STDOUT);
  if($color) { print $c_unseen; }
  print " ";  

  # row across the top
  for($i=0; $i<(2*$w); $i++) { print "_"; }

  for($i=0; $i<$size; $i++) {
    $c = 2 * ($i % $w);
    $r = int($i / $w);
    if(($i%$w) == 0) {
      $terminal->Tgoto('cm', $c, $r+1, STDOUT);
      if($color) { print $c_unseen; }
      print "|";
      if($color) { print "\e[0m"; }
    }
    else { $terminal->Tgoto('cm', $c+1, $r+1, STDOUT); }
    
    if($color && $maze[$i][$set] == $trail) { print $c_trail; }

    if($maze[$i][$set] == $maze_end) {
      if($color) { print $c_finish, "%", "\e[0m"; }
      else { print "%"; }
    }
    elsif ( $maze[$i][$set] == $cursor_pos) {
      if($color) { print $c_cursor, "*", "\e[0m"; }
      else { print "\e[7m\e[4m*\e[0m" };
    }
    elsif($maze[$i][$south] eq "NULL") {
      if($color) {
        if($maze[$i][$set] == $trail) { print $c_trail; }
        else { print $c_unseen; }
      }
      print "_";
    }
    else {
      if($color) {
        if($maze[$i][$set] == $trail) { print $c_trail; }
        else { print $c_unseen; }
      }
      print " ";
    }

    if($maze[$i][$east] eq "NULL") {
      if($color) { print $c_unseen; }
      print "|";
    }
    else {
      if($color) {
        if($maze[$i][$set] == $trail) { print $c_trail; }
        else { print $c_unseen; }
      }
      print "_";    
    }
  }
}

## take input, mostly for moving your little dude around in the maze
sub move {
  my $key = shift;
  my $moved = 0;
  $prev_cursor = $cursor;

  if (( $key eq "$up" || $key eq "$u_up") && $maze[$cursor][$north] ne "NULL") {
    $maze[$cursor][$set] = $trail;
    $cursor = $maze[$cursor][$north];
    $moved = 1;
  } 
  elsif (( $key eq "$left" || $key eq "$u_left") && $maze[$cursor][$west] ne "NULL") {
    $maze[$cursor][$set] = $trail;
    $cursor = $maze[$cursor][$west];
    $moved = 1;
  }
  elsif (( $key eq "$right" || $key eq "$u_right") && $maze[$cursor][$east] ne "NULL") {
    $maze[$cursor][$set] = $trail;
    $cursor = $maze[$cursor][$east];
    $moved = 1;
  }
  elsif (( $key eq "$down" || $key eq "$u_down") && $maze[$cursor][$south] ne "NULL") {
    $maze[$cursor][$set] = $trail;
    $cursor = $maze[$cursor][$south];
    $moved = 1;
  }
  elsif( $key eq 'c' || $key eq 'C') { # toggle the color
    $color = 1 - $color;
    tty_display(@maze);
  }
  elsif ( $key eq 'r' || $key eq 'R' || $key eq $redraw) { 
    system("clear");
    tty_display(@maze);
  }
  elsif ( $key eq 'q' || $key eq 'Q' ) { 
    quit();
  }
  elsif ( $key eq 's' || $key eq 'S' ) { 
    solve();
  }

  if($moved == 1) {
    if( $maze[$cursor][$set] == $maze_end ) { 
      win($seed);
    }
    $movesdone++;
    $maze[$cursor][$set] = $cursor_pos;
  }
  update($prev_cursor, $cursor);
}

################## BEGIN MAZE SOLVING FUNCTIONS ##########################
## count the number of exits. used by solve().
sub numExits {
  my $exits = 0;
  for ($i=0; $i<4; $i++) {
    if ( $maze[$cursor][$i] ne "NULL" ) {
      $exits++;
    }
  }
  return $exits;
}

## whether or not a given path from an intersection has been tried already
sub beenThere {
  my $pos = shift;
  my $found = 0;
  foreach $ele (@tried) {
    if( $ele == $pos ) {
      $found = 1;
    }
  }
  return $found;
}

## figure out if we've got nowhere new to go from the current intersection
sub noMoreOptions {
  # if a new path still exists, return 0 (i.e. "more options")
  for($i=0; $i<4; $i++) {
    if( $maze[$cursor][$i] ne "NULL" &&
    $maze[$maze[$cursor][$i]][$set] != $trail &&
    !beenThere($maze[$cursor][$i]) ) {
      return 0;
    }
  }
  return 1;
}

## back-track to the last intersection
sub gotoLastIntersect {
  do {
    for($i=0; $i<4; $i++) {      
      if( $maze[$maze[$cursor][$i]][$set] == $trail ) {
        $prev_cursor = $cursor;
        $maze[$cursor][$set] = $empty;
        $cursor = $maze[$cursor][$i];
        update($prev_cursor, $cursor);
        last;
      }
    }
  } while( numExits() < 3 );
}

## solve the maze
sub solve {
  # reset the maze
  initialize($size);
  generate_maze($seed);
  tty_display(@maze);
  $solvestart = time();
  while(1) {
    # are we at a dead end?
    if( numExits() == 1 ) {
      gotoLastIntersect();
    }
    # pick a random direction
    $dir = int(rand(4));

    # don't try moving into a wall
    if( $maze[$cursor][$dir] eq "NULL") {
      next;
    }
    # don't go where we just came from
    if( $maze[$maze[$cursor][$dir]][$set] == $trail ) {
      next;
    }
    # are we at an intersection?
    if( numExits() > 2 ) {
      # have we been in that direction already?
      if( beenThere($maze[$cursor][$dir]) ) {
        # if we've been to all the exits, go to the last intersection
        if( noMoreOptions() ) {
          gotoLastIntersect();
        }
        next;
      }
      else {
        # add on to the list of exits we've taken
        push(@tried, $maze[$cursor][$dir]);
      }
    }

    # make the move
    $maze[$cursor][$set] = $trail;
    $prev_cursor = $cursor;
    $cursor = $maze[$cursor][$dir];
    # did we win?
    if( $maze[$cursor][$set] == $maze_end ) {
      $solveend = time();
      $maze[$cursor][$set] = $cursor_pos;
      return;
    }
    $maze[$cursor][$set] = $cursor_pos;
    update($prev_cursor, $cursor);
  }
}
#################### END MAZE SOLVING FUNCTIONS ########################## 


sub quit {
  system("stty -raw echo");
  print "\n\nQuitting...\n\n";
  print "\nYou just played: -r $h -c $w -s $seed\n";
  print "Maze generated in ", $endtime - $starttime, " seconds.\n";
  if($solvestart) {
    print "Maze solved in ", $solveend - $solvestart, " seconds.\n";
  }
  elsif($movesdone) {
    print "You performed $movesdone moves.\n" 
  }
  exit(0);
}

## read the command line arguments
sub getinput {
  while($arg = shift @ARGV) {
    if($arg eq "--help" || $arg eq "-h") {
      help();
    }
    elsif($arg eq "-d") { $dissolve = 1; }
    elsif($arg eq "-a") { $color = 1; }
    elsif($arg eq "-m") {
      $width = int(($screen_width-1)/2);
      $height = $screen_height-1;
    }
    elsif($arg eq "-c") {
      $width = shift @ARGV;
      if($width < 1) { help("Maze width must be at least 1"); }
      elsif($autosize && ($width > $screen_width)) { help("Maze width must be less than half of the screen width (Max: $screen_width)\n"); }
    }
    elsif($arg eq "-r") {
      $height = shift @ARGV; 
      if($height < 1) { help("Maze height must be at least 1"); }
      elsif($autosize && ($height > $screen_height)) { help("Maze height must be one less than the screen height (Max: $screen_height)\n"); }
    }
    elsif($arg eq "-s") { $random_seed = shift @ARGV; }
  }
  unless($width) { $width = $default_width; }
  unless($height) { $height = $default_height; }
  if(($width + $height) < 3) { help("A 1 x 1 maze is too small."); } 
  unless($random_seed) { $random_seed = (time); }

  return($width, $height, $random_seed);
}


## hot dog! you won! ##
sub win {
  my ($seed) = @_;
  
  system("stty -raw echo");

  print <<EOF;
             _                                     |###|
            ( )                                    \\###/
            |H|              YOU                   (o^o)
           _|=|_                   WON!             / \\
         =|/:M:\\|=                                  \\_/
         U{ :W: }U    TOM and CROW                ___=___
           \\___/                                [|=======|]
         TOM SERVO        congratulate you      | CROOOOW |
         /___8___\\                              |   / \\   |
        (_________)                                 L_J
EOF


  quit();
}

sub help {
  my $msg = shift;
  if($msg) { print "\n$msg\n\n"; }
  $max_height = $screen_height -1;
  $max_width = int(($screen_width-1)/2);
  print <<END;
Usage: $0 [-h] [-m] [-d] [-r <rows>] [-c <columns>] [-s <seed>]
     -m           set maze size to the maximum allowed by your screen
     -d           generate in \"dissolve\" mode, slower but fun to watch
     -h           print this help text
     -r <num>     set the number of rows in the maze
     -c <num>     set the number of columns in the maze
     -s <num>     provide a seed value for generating a maze
     -a           use ANSI color

Keys:
    Movement:            Arrow keys, or vi movement keys (h,j,k,l)
    Redraw:              r
    Solve Maze:          s
    Quit:                q
    Toggle ANSI color:   c

END

printf "Default height: %3d   Max height: %3d\n", $default_height, $max_height;
printf "Default width:  %3d   Max Width:  %3d\n", $default_width, $max_width;


  print "\nTextMaze v$VERSION by Kirk Baucom <kbaucom\@schizoid.com>\n";

  unless(eval "require Term::ReadKey") {
    print "\nWARNING: You do not have the Term::ReadKey module. You will not be able to\n";
    print "         use the -m option properly. Term::ReadKey is available at:\n";
    print "         http://www.cpan.org\n";
  } 
  exit;
}
