#!/usr/bin/perl -w

$VERSION = "0.0.3";
($SELF = $0) =~ s,.*/,,;

$|++;

# game state:
our $plaintext;
our @key;
our @wipkey;
our $ciphertext;
our %counts;
our @undo_stack;
our $moves;
our $start_time;
our $puzzle_stat;
our $solved;
our $sort_alpha = 1;
our $lettercnt;

# terminal escape sequences (from tput):
our $home_cursor = "";
our $clreol = "";
our $clrscr = "";
our $clrtobottom = "";
our $color_off = "";

# intended to be overrideable via config file:
our $bright_colors = 0;
our $min_lines = 3;
our $max_lines = 8;
our $fortune_cmd = "fortune -a";
our $dup_check = 1;

sub prompt {
	my $p = shift || ">";
	print "$p $clreol";
	my $input = scalar <>;
	print $clrtobottom;
	return $input;
}

sub check_externals {
	# Don't bother to check stty here. If it's missing, check_term_size
	# will complain about it already.
	if(system("$fortune_cmd >/dev/null") != 0) {
		die <<EOF;
$SELF: fatal: can't run '$fortune_cmd'!
Make sure it's installed, and make sure it's available in your \$PATH.
EOF
	}
	if(system("tput init >/dev/null") != 0) {
		warn <<EOF;
$SELF: warning: can't run 'tput init'!
Make sure it's installed, and make sure it's available in your \$PATH.
Also, check your TERM environment variable.
Press Enter to continue in dumb terminal mode
EOF
		prompt;
	}
}

# when this gets called, Digest::MD5 has already been loaded
sub get_md5sum_filename {
	my $text = $_[0];
	my $md5 = Digest::MD5::md5_hex($text);
	my $one = substr($md5, 0, 1);
	my $two = substr($md5, 1, 1);
	our $md5sum_filename = $ENV{HOME} . "/.cryptokwot.md5sums/$one/$two/$md5";
	return $md5sum_filename;
}

# return true if we got a previously-seen fortune
sub is_dup {
	return 0 unless $dup_check;
	if(!eval("require Digest::MD5")) {
		$dup_check = 0;
		# TODO: let the user know
		return 0;
	}

	return -e get_md5sum_filename(join "", @_);
}

sub mark_dup {
	return unless $dup_check;
	my $dir = $md5sum_filename;
	$dir =~ s,/[^/]*$,,;
	system("mkdir -p '$dir' 2>/dev/null");
	open my $f, ">", $md5sum_filename;
	close $f;
}

# fairly expensive to repeatedly run fortune, but it doesn't usually
# have to run that many times.
sub get_fortune {
	my @lines = ();
	@lines = map { uc } qx/$fortune_cmd/
		while (@lines < $min_lines || @lines > $max_lines || is_dup(@lines));
	return join "", @lines;
}

# *much* slower way to generate keys, and the results aren't any better.
##sub generate_key_old {
##	my $a;
##	my $b;
##
##	@key = ('A'..'Z');
##
##	# swap 2 letters at random, 5000 to 7000 times.
##	for(1..(5000 + int(rand(2001)))) {
##		do {
##			$a = int(rand(26));
##			$b = int(rand(26));
##		} while ($a == $b);
##		@key[$a, $b] = (@key[$b, $a]);
##
##		# every 100 swaps, "cut" like a deck of cards
##		if($_ % 100 == 0) {
##			my $r = int(rand(19) + 3); # 4 to 22
##			@key = (@key[$r..25], @key[0..$r-1]);
##		}
##	}
##
##	# if there's any joe jobs, swap with a random letter.
##	# after the swap, neither letter will be a joe job (no need to check).
##	for(0..25) {
##		if($key[$_] eq chr($_ + 65)) {
##			my $r = int(rand(25));
##			$r++ if $r >= $_;
##			@key[$_, $r] = (@key[$r, $_]);
##		}
##	}
##}

# This is much faster: it iterates over the alphabet once (plus another
# loop over the key to detect/fix joe jobs). Randomness looks good.
sub generate_key {
	@key = ();
	my @a = ('A'..'Z');

	for(my $i = 0; $i < 26; $i++) {
		push @key, splice @a, int(rand(scalar @a)), 1;
	}

	# if there's any joe jobs, swap with a random letter.
	# after the swap, neither letter will be a joe job (no need to check).
	for(0..25) {
		if($key[$_] eq chr($_ + 65)) {
			my $r = int(rand(25));
			$r++ if $r >= $_;
			@key[$_, $r] = (@key[$r, $_]);
		}
	}
}

sub encipher {
	$_ = $plaintext;
	eval "tr/A-Z/" . join("", @key) . "/";
	$ciphertext = $_;
}

sub init_wipkey {
	@wipkey = ();
	push @wipkey, "_" for (0..25);
}

sub try_tput {
	my $result = "";
	for(@_) {
		$result = `tput $_ 2>/dev/null`;
		last if length($result);
	}
	return $result;
}

# Note: don't try to use 'tput lines' or 'tput cols', especially not
# with the try_tput wrapper: it captures tput's stdout, so stdout
# is no longer attached to the terminal. So it falls back to the
# default lines/cols values from the terminfo database (meaning it
# always reports 80x24). Some versions of tput might notice this, and
# try using stderr... but that's not the terminal either, since it's
# 2>/dev/null! Discussion here:
# https://unix.stackexchange.com/questions/299067/getting-console-width-using-a-bash-script
sub check_term_size {
	my $lines;
	my $cols;
	my $pause = 0;
	my $stty = `stty size`;
	if(defined($stty)) {
		($lines, $cols) = ($stty =~ /^(\d+)\s+(\d+)/);
	}

	my $minlines = $max_lines * 2 + 7;
	$minlines = 24 if $minlines < 24;

	if(!defined($lines) || !defined($cols)) {
		print "$SELF: can't determine terminal size. Hope it's at least 80x$minlines!\n";
		$pause = 1;
	} else {
		if($lines < $minlines) {
			print "$SELF: terminal is too short. Should be at least $minlines lines, ";
			print "but is only $lines.\n";
			$pause = 1;
		}
		if($cols < 80) {
			print "$SELF: terminal is too narrow. Should be at least 80 columns, ";
			print "but is only $cols\n";
			$pause = 1;
		}
	}
	if($pause) {
		print "Resize the terminal now if possible. Press Enter to continue";
		prompt;
	}
}

sub init_terminal {
	print try_tput("init");
	$clrscr = try_tput("clear", "cl");
	$home_cursor = try_tput("cup 0 0", "home", "ho");

	if(!length($clrscr) || !length($home_cursor)) {
		# if we can't clear the screen or home the cursor, attempt
		# to 'clear' the screen by scrolling the contents away.
		$clrscr = $home_cursor = "\n" x 100;
	} elsif(!length($home_cursor)) {
		# if we somehow can clear the screen but not home the cursor,
		# just use clear-screen instead of home. The display will
		# flicker when it's redrawn, but what are you gonna do?
		$home_cursor = $clrscr;
	}

	$clreol = try_tput("el", "ce");
	$clrtobottom = try_tput("ed", "cd");
	$color_off = try_tput("op");

	if(length($color_off) && $color_off =~ /^\x1b\[.*m$/) {
		# if there's color, and the code for it looks vaguely ANSI-ish, assume
		# the terminal uses ANSI escapes. I'm not calling tput 24 times here.
		for(0..7) {
			$fgdark[$_] = sprintf("\x1b[0;3%dm", $_);
			$fgbright[$_] = sprintf("\x1b[1;3%dm", $_);
			$bgcolor[$_] = sprintf("\x1b[4%dm", $_);
		}
	} else {
		# terminal doesn't support color.
		# see if we have bold, if so, use it for green. Works for vt100.
		# standout (smso/rmso) and 'rev' are both reverse video, which makes it
		# hard to tell what's going on IMO.
		$color_off = try_tput('sgr0');
		if(length($color_off)) {
			my $bold = try_tput('bold');
			if(length($bold)) {
				$fgdark[$_] = $fgbright[$_] = $bgcolor[$_] = "" for 0..7;
				$fgdark[2] = $fgbright[2] = $bold;
			}
		}
	}

	check_term_size;
}

# don't call this directly
sub get_color {
	return "" unless @fgdark;

	my $fg = shift;
	my $c = shift;
	my $bright = shift || 0;

	if($fg) {
		if($bright) {
			return $fgbright[$c] || "";
		} else {
			return $fgdark[$c] || "";
		}
	} else {
		return $bgcolor[$c] || "";
	}
}

# call this instead
sub fg_color {
	get_color(1, $_[0], $bright_colors);
}

# and/or this
sub bg_color {
	get_color(0, $_[0]);
}

sub prompt_yn {
	my $p = shift;
	my $default = shift || 0;
	my $yn;
	if($default) {
		$yn = fg_color(2) . "Y" . $color_off . "/" . fg_color(1) . "n" . $color_off;
	} else {
		$yn = fg_color(1) . "y" . $color_off . "/" . fg_color(2) . "N" . $color_off;
	}
	for(prompt "$p [$yn]?") {
		s/[^yn]//gi;
		return 1 if /y/i;
		return 0 if /n/i;
	}
	return $default;
}

# only used if a filename was given on the command line
sub read_file {
	my $got = "";
	open my $f, "<$_[0]" or die "$SELF: $_[0]: $!\n";
	while(<$f>) {
		$got .= uc $_;
	}
	close $f;
	return $got;
}

sub init_puzzle {
	%counts = ();
	$lettercnt = 0;

	print $clrscr;

	if(@ARGV) {
		if($already_enciphered) {
			$ciphertext = read_file($ARGV[0]);
			$plaintext = ""; # unknown!
		} else {
			$plaintext = read_file($ARGV[0]);
		}
		@ARGV = ();
	} else {
		$plaintext = get_fortune;
	}

	if(!$already_enciphered) {
		generate_key;
		encipher;
	}
	init_wipkey;
	@undo_stack = ();
	$moves = 0;
	$start_time = time;

	for(split "", $ciphertext) {
		next unless /^[A-Z]/;
		$lettercnt++;
		$counts{$_}++;
	}

	$puzzle_stat = $lettercnt . " letters, " . scalar split(/\s+/, $plaintext) . " words";
	#$puzzle_stat = $lettercnt . " letters, " .
		#scalar split(/\s+/, $plaintext) . " words, " .
		#scalar split("\n", $ciphertext) . " lines";

}

sub print_puzzle {
	my $wiptext = "";

	print $home_cursor;

	my @lines = split "\n", $ciphertext;
	for my $l (@lines) {
		my $wip = "";
		for(split "", $l) {
			my $asc = ord($_);
			if($asc >= ord 'A' && $asc <= ord 'Z') {
				$wip .= $wipkey[ord($_) - 65];
			} else {
				$wip .= $_;
			}
		}
		$wiptext .= "$wip\n";
		print fg_color(2) . $wip . $color_off . "\n";
		print fg_color(5) . $l . $color_off . "\n";
	}

	$solved = ($wiptext eq $plaintext);
}

sub sortcounts {
	$counts{$_[1]} <=> $counts{$_[0]};
}

sub format_count_entry {
	my $ltr = shift;

	if(not defined($counts{$ltr})) {
		return "     ";
	}

	my $c = fg_color(3);
	my $e = $color_off;
	my $f = fg_color(4);
	if($wipkey[ord($ltr) - 65] ne '_') {
		$c = fg_color(5);
	}
	my $numc = "";
	my $pct = ($counts{$ltr} / $lettercnt) * 100;
	if($pct >= 8) {
		$numc = fg_color(2);
	} elsif($pct >= 4) {
		$numc = fg_color(5);
	} else {
		$numc = fg_color(3);
	}
	sprintf("$c$ltr$f=$numc%-2d ", $counts{$ltr});
}

sub print_counts {
	my $output = "";
	my $wrapped = 0;
	my $i = 0;
	my @keys;
	if($sort_alpha) {
		@keys = "A" .. "Z";
	} else {
		@keys = sort { sortcounts($a, $b) } keys %counts;
	}
	for(@keys) {
		$i++;
		if(!$wrapped && ($i > 13)) {
			$output .= "\n        ";
			$wrapped = 1;
		}
		$output .= format_count_entry($_);
	}
	print "\n" . fg_color(6) . "Counts: " . $color_off . "$output$clreol\n";
}

sub print_status {
	my $dups = 0;
	my %seen;
	for(@wipkey) {
		next if $_ eq '_';
		$seen{$_}++;
	}

	print fg_color(6) . "Cipher: " . $color_off;
	my @blue;
	for(0..25) {
		my $ltr = chr(65 + $_);
		my $c = $counts{$ltr} ? 5 : 4;
		$blue[$_]++ unless $counts{$ltr};
		print fg_color($c) . $ltr . $color_off;
	}

	my $sec = time - $start_time;
	my $min = $sec / 60;
	$sec %= 60;
	$time = sprintf("%02d:%02d", $min, $sec);

	print " | " . fg_color(3) . $puzzle_stat . $color_off . " | " . fg_color(5) . "$moves moves, $time" . $color_off . "\n";
	print fg_color(6) . "Plain:  " . $color_off;
	my $i = 0;
	for(@wipkey) {
		if($_ eq '_') {
			if($blue[$i]) {
				print " ";
			} else {
				print $_;
			}
		} elsif($seen{$_} > 1) {
			print bg_color(1) . $_ . $color_off;
			$dups++;
		} else {
			print fg_color($blue[$i] ? 4 : 2) . $_ . $color_off;
		}
		$i++;
	}

	#print "| " . fg_color(3) . "$moves moves, $time" . $color_off . "\n";
	print " | " . fg_color(6) . "Pool: " . fg_color(2);

	my %pool;
	$pool{$_} = 1 for "A".."Z";
	for(0..25) {
		my $l = $wipkey[$_];
		if($l ne '_') {
			$pool{$l} = 0;
		}
	}
	my @k = grep { $pool{$_} > 0 } sort keys %pool;
	print join("", @k) . (" " x (26 - @k));

	print "$color_off\n";
	if($dups) {
		print fg_color(1) . "DUPS!" . $color_off . " ";
	}
}

sub print_help {
	my $bright = $bright_colors ? "ON" : "OFF";
	my $sortorder = $sort_alpha ? "alphabetical" : "numeric";

	print $clrscr;
	print <<EOF;
All commands case-insensitive, and must be followed by the Enter key.

To substitute one letter, enter the ciphertext letter followed by the
plaintext letter, e.g. "ab" if you think A is the cipher for B.

To substitute multiple letters, enter the ciphertext letters, a space,
and the same number of plaintext letters. E.g. if you think "JDW"
represents "THE", enter "jdw the". This still counts as one move.

To remove a substitution (e.g. if you guessed wrong), enter the
ciphertext letter by itself.

Other commands are prefixed by a slash, and can be abbreviated to just
their first letter:

/bright - Toggle bright colors. Currently $bright.
/giveup - Shows you the answer. And calls you a loser.
/new    - Abandon the current puzzle and generate a new one.
/pause  - Stops the timer. You can't view the puzzle while paused.
/quit   - Quit the game.
/reset  - Reset the puzzle (remove all your substitutions).
/sort   - Toggle "Counts:" sort order. Currently $sortorder.
/undo   - Undo your last move. Multiple levels of undo are supported.
EOF
	print "Press Enter";
	prompt;
	print $clrscr;
}

sub push_undo_state {
	push @undo_stack, join("", @wipkey);
}

sub pop_undo_state {
	if(!@undo_stack) {
		print "Can't undo from this point.\n";
		return;
	}

	# only count it as a move if it works
	$moves++; 
	my $state = pop @undo_stack;
	@wipkey = split "", $state;
}

sub confirm_quit {
	print "Really quit?";
	if(prompt_yn "", 0) {
		return 1;
	} else {
		print $clrscr;
		return 0;
	}
}

sub substitute {
	$moves++;
	push_undo_state;
	my $ciph = uc shift;
	my $plain = uc shift;
	if(length $ciph != length $plain) {
		print "Invalid substitution: '$ciph' and '$plain' are not the same length\n";
		return;
	}
	my @c = split "", $ciph;
	my @p = split "", $plain;
	for(my $i = 0; $i < @c; $i++) {
		@wipkey[ord($c[$i]) - 65] = $p[$i];
	}
}

sub remove_subst {
	$moves++;
	push_undo_state;
	my $c = uc shift;
	@wipkey[ord($c) - 65] = '_';
}

sub pause {
	print $clrscr;
	print "$SELF paused.\nPress Enter to continue";
	my $pause_start = time;
	prompt;
	$start_time += (time - $pause_start);
	print $clrscr;
}

sub give_up {
	if($already_enciphered) {
		print "Sorry, *I* don't know the answer.\n";
		return;
	}
	my $got = prompt_yn "Are you SURE you want to chicken out", 0;
	print $clrscr;
	return unless $got;
	for(0..25) {
		$wipkey[ord($key[$_]) - 65] = chr($_ + 65);
	}
	$gave_up = 1;
}

sub do_command {
	my $cmd = uc(shift || "");
	chomp $cmd;
	$cmd =~ s/^\s+//;
	$cmd =~ s/\s+$//;
	return if $cmd eq "";

	for($cmd) {
		m,^[a-z]$,i && do { remove_subst($_); return; };
		m,^([a-z])([a-z])$,i && do { substitute($1, $2); return; };
		m,^([a-z]+)\s+([a-z]+)$,i && do { substitute($1, $2); return; };

		m,^/?\?, && do { print_help; return; };
		m,^/h(?:elp)?,i && do { print_help; return; };

		m,^/quit$,i && do { print "Bye.\n"; exit 0; };

		m,^/q$,i && do {
			print "Bye.\n", exit 0 if confirm_quit;
			return;
		};

		m,^/b(?:old)?,i && do {
			$bright_colors = !$bright_colors;
			print "Bright colors now " . ($bright_colors ? "ON" : "OFF") . "\n";
			return;
		};

		m,^/u(?:ndo)?,i && do { pop_undo_state; return; };

		m,^/n(?:ew)?,i && do {
			if(prompt_yn("Abandon this puzzle?", 0)) {
				$already_enciphered = 0;
				init_puzzle;
			}
			return;
		};

		m,^/r(?:eset)?,i && do { $moves++; push_undo_state; init_wipkey; return; };

		m,^/p(?:ause)?,i && do { pause; return; };

		m,^/s(?:ort)?,i && do { $sort_alpha = !$sort_alpha; return; };

		m,^/give,i && do { give_up; return; };
	}
	print "Unknown command, try ? for help.";
}

sub play_again {
	print "| " . fg_color(6) . "Play again" . $color_off;
	exit 0 unless prompt_yn "", 1;
}

sub congratulations {
	print fg_color(2) . "Congratulations! " . fg_color(5) . "You solved it.    " . $color_off;
	play_again;
}

sub insult_user {
	print fg_color(1) . "YOU LOSE." . fg_color(5) . " Here is the solution.    " . $color_off;
	play_again;
}

sub print_desktop_file {
	print <<EOF;
[Desktop Entry]
Type=Application
Name=CryptoKwot
GenericName=Crypto Puzzle
Comment=Try to solve randomly generated ciphers
Exec=$SELF
Icon=$SELF
Terminal=true
Categories=Game;LogicGame;
EOF
}

# The icon is the question mark the Doctor (from Dr. Who) wore on his
# lapel during the 80s. It's a 64x64 4-color GIF.
sub print_icon {
	die "$SELF: not printing binary GIF data to your TTY, please redirect\n" if -t STDOUT;
	open my $pipe, "|base64 -d" or die "$SELF: can't run base64\n";
	print $pipe <<EOF;
R0lGODlhQABAAPEDAP7+/sQMFOy6vQAAACH5BAUAAAMALAAAAABAAEAAAAL/nI+pa+DvmJy0Noix
3TxlIIRi1pVTJgTqygqaCV8PS9fuE5tQzdt4zpnRRKme6gYAWoSrj8MY+CkZzADSWfRFpgvm1bPj
SbmIZ3PbrbLGZCZbEaa9p47sHAwSJ8mH+vneZyaH1pZ39CIhuEbI5Te4l2jYwkgnuXJD4TgJWQig
lWl5yNmoKAqadUlZiRqlWuZZA5gT9hUJ+8j3+nGql4snG8hq5eq7oaYCXGwr3DqqbFxq+lzywJw8
Hai56IxNVa3H3a17PHxd/MFcKz6uvUnc/W2EuA7GXG4+3Y48Tw8Xqt4vEitMASu0I1jwxL93CQcc
ZJjwYbiGFwZCLCiRosAhRBcDZtSYxuJEih9B1uM4smFJk8FQsgTz6WU2JzJnkqhpEx9GmjgdKkLS
c+XLaK2C3lpkNGZNNTrpVQvR1CnPnjI6ligAADs=
EOF
	close $pipe;
}

### main()
if(@ARGV) {
	for($ARGV[0]) {
		if(/^--?(?:\?|h)/) {
			exec "perldoc $0";
			exit 1;
		} elsif(/^--?man/) {
			exec "pod2man --stderr -s6 -cUrchlaysStuff -r$VERSION -u $0";
			exit 1;
		} elsif(/^--?desk/) {
			print_desktop_file;
			exit 0;
		} elsif(/^--?icon/) {
			print_icon;
			exit 0;
		} elsif(/^--?p(?:uzzle=)?(.+)$/) {
			@ARGV = ( $1 );
			$already_enciphered = 1;
		}
	}
}

die "$SELF: standard input is not a TTY.\n" unless -t;

for("$ENV{HOME}/.cryptokwotrc") {
	if(-e $_) {
		do $_;
		my $err = $@ if $@;
		$err = $! unless $err;
		if($err) {
			warn "$0: can't parse $_: $err\n";
			print "Press Enter to continue";
			prompt;
		}
	} else {
		# quietly create the config with default options.
		open my $conf, ">$_";
		print $conf <<EOF;
# .cryptokwotrc - config file for $SELF

###
# How should the letter counts be sorted? 0 = by letter frequency,
# 1 = alphabetical order.
\$sort_alpha = $sort_alpha;

###
# Most users will want this disabled, especially if they use a white
# terminal background.

\$bright_colors = $bright_colors;

###
# Minimum and maximum lines for a fortune to be used as a puzzle.
# Really short quotes are harder (maybe impossible) to decipher, since
# there's less structure. If max_lines is set higher than 8, you'll
# need a taller terminal (8 is the max for a 24-line window). Each
# puzzle line takes up 2 terminal lines, plus the rest of the UI is 7
# lines, so if you want e.g. 10-line puzzles, you'll need 10*2+7 = 27
# lines. No attempt is made to enforce the terminal size limit!

\$min_lines = $min_lines;
\$max_lines = $max_lines;

###
# If you like, you can adjust the fortune command to have it only use
# certain fortune files. Example: "fortune startrek linuxcookie". Most
# Linux distros keep the files in /usr/share/games/fortunes.

\$fortune_cmd = "$fortune_cmd";

###
# If dup_check is enabled, cryptokwot will keep track of which fortunes
# it's already used, so you'll get a new one every game. This requires
# the Digest::MD5 module, which is part of core perl (ships with the
# perl source code). If this module is missing, dup_check will be disabled,
# but cryptokwot will still work.
\$dup_check = $dup_check;

###
# Please don't remove this line:
1;
EOF
	}
}

check_externals;
init_terminal;
init_puzzle;
while(1) {
	print_puzzle;
	print_counts;
	print_status;
	if($gave_up) {
		$gave_up = 0;
		insult_user;
		init_puzzle;
	} elsif($solved) {
		mark_dup;
		congratulations;
		init_puzzle;
	} else {
		print fg_color(2) . "move" . $color_off;
		do_command(prompt);
	}
}

=pod

=head1 NAME

cryptokwot - substitution cipher game

=head1 SYNOPSIS

B<cryptokwot>

B<cryptokwot> [<I<file>> | B<--puzzle=>I<file> ]

B<cryptokwot> B<--help|--man|--desktop|--icon>

=head1 DESCRIPTION

Game in which the player must decipher a quotation that's been
enciphered with a randomly generated simple substitution cipher.

If you've ever done a newspaper Cryptoquote or Cryptoquip, this will
be familiar.

B<cryptokwot> doesn't solve the puzzle for you; it basically replaces
pencil and paper, allowing you to assign plaintext letters to
ciphertext ones interactively.

By default, the plaintext comes from the command B<fortune -a>. This
command will be re-run repeatedly until B<cryptokwot> finds that the
output is between 3 and 8 lines of text. The command and the numbers
can be changed; see B<CONFIG FILE>, below.

If a I<file> argument is given, it will be used as the plaintext. This
is mostly intended for debugging. Once the puzzle is solved, answering
B<y> to the I<Play again?> prompt will go back to generating plaintext as
described above.

The B<--puzzle=>I<file> option works similarly, but it assumes the
file has already been enciphered. It's intended for use with existing
puzzles, typed in from newspapers or copy/pasted from websites. In
this mode, B<cryptokwot> doesn't know the plaintext nor the key, so it
can't tell you when you've won.

=head1 OPTIONS

All game options are controlled by the config file (see below), so
there aren't a lot of command-line options. The --man, --icon, --desktop
options are intended for packagers.

=over 4

=item B<--help>

Prints this help text, via B<perldoc>(1).

=item B<--man>

Prints this help text as a man page, via B<pod2man>(1). Suggested use:

  cryptokwot --man > cryptokwot.6

Then B<cryptokwot.6> can be installed in e.g. /usr/man/man6 or
/usr/share/man/man6 or wherever your OS keeps its man pages.

=item B<--icon>

Print the cryptokwot icon to standard output, which should be redirected
to a file. It's a GIF image, so suggested use is:

  cryptokwot --icon > cryptokwot.gif

If you're packaging cryptokwot, use e.g. ImageMagick's convert(1) to
turn the GIF into whatever image format your OS likes icons to be in
(probably PNG).

=item B<--desktop>

Print a FreeDesktop compliant .desktop file to standard output. Suggested
use:

  cryptokwot --desktop > cryptokwot.desktop

If you're a packager, place cryptokwot.desktop wherever your OS keeps
its .desktop files. This will allow KDE/Gnome/XFCE/etc users to launch
cryptokwot from their start menu (or equivalent).

=back

=head1 GAME RULES

You should read the in-game help, accessible via the B</help>
command. The following is a more formalized description of the game.

The object of the game is to decipher the message. Secondary
objectives are to minimize the number of moves made and the amount of
time spent.

The plaintext is enciphered according to a randomly generated
key. However, the key is not 100% random: in particular, it's
guaranteed that no letter will be enciphered as itself.

During play, the game keeps track of how many moves you've made and
the amount of time you've been playing. The I</pause> command stops
the timer (and removes the puzzle from view, so you can't cheat).

Each move consists of:

=over 4

=item -

A substitution of one or more letters. The ciphertext letters are
always given first. These always count as one move, no matter how many
letters you enter.

=item -

A removal of a substitution (done by entering the single ciphertext letter
to remove). Counts as one move.

=item -

The /undo command. This restores the state of the game to what it was
before your last move, and itself counts as one move. You can /undo
all the way back to the starting state.

=item -

The /reset command. Counts as one move. Clears all your ciphertext
to plaintext mappings. This is similar to executing /undo repeatedly,
except that /reset only costs one move, and can itself be undone with
/undo.

=back

Other commands (/pause, etc) don't count as moves. Errors also don't count
as moves.

=head1 CONFIG FILE

The config file is B<~/.cryptokwotrc>. If it doesn't exist, B<cryptokwot>
will create it, with the default options.

The options you can set in the config file are the minimum and maximum
lines the puzzle can be, whether or not to use bright colors, and
the command to use to get quotes (default is B<fortune -a>).

For more information, see the comments in the default config file (not
going to repeat all that stuff here).

=head1 LIMITATIONS

You can call these bugs if you like...

1. B<cryptokwot> only works with ASCII text files with lines <= 80
columns. Traditionally, B<fortune(6)> files have always been English
written in ASCII, wrapped somewhere around 72 columns. If you have
oddball fortune files that don't meet the wrap requirement, you
could try setting $fortune_cmd to something like:

	fortune <args> | fmt

You could also try piping it to iconv(1) or uconv(1) to transliterate
accented characters to ASCII, if it's non-English. YMMV on whether the
text is still readable after transliteration (I literally have no idea
how much e.g. a French or German speaker would miss the accent marks,
as I do not speak French or German).

2. There's no way to change the colors, if you don't like them (other
than toggling the bright setting, or of course editing the code). The
game should be fully playable without colors, so you can try e.g.

	TERM=vt100 cryptokwot

3. The UI could be made a little friendlier, e.g. by using curses. I
don't think it would add much to the game, and I'd rather avoid
the external dependency (yes, I know, fortune and tput are already
external deps, but they're part of any halfway-complete Linux
distribution, whereas perl-Curses might not be).

4. There is too much documentation. Nobody's ever going to read it all,
are they?

=head1 AUTHOR

cryptokwot was written by B. Watson <yalhcru@gmail.com> and released
under the WTFPL: Do WTF you want with this.

=cut

__END__

Rest of the file is boring documentation. I've always been a fan
of the cryptoquote puzzle in the newspaper, and I wrote a primitive
version of this in the early 90s, in C, for my own use... but it was
*really* primitive, and I never was happy with it.

This rewrite/reimplementation was inspired by me happening across a
project called cryptoslam: http://sourceforge.net/projects/cryptoslam/

cryptokwot doesn't share any code with either my paleolithic C
implementation or cryptoslam (which is in C++).

cryptoslam looks like an incomplete and abandoned project, so all the
criticism below shouldn't be taken as some kind of indictment of the
author. I'm just giving my reasons for spending so much (too much)
time on this.

cryptoslam works, but has issues:

It sometimes encodes a letter as itself. This changes the whole
strategy of the game: If you have an A by itself, and you know A can't
represent itself, it's just about guaranteed that A represents I. But
with cryptoslam, you don't have any such assurance. Every newspaper
crypto(quote|gram) I've ever seen has the "no self-encoding" rule,
so cryptoslam fails here.

cryptoslam's UI design makes it frustrating to use. There's no Undo
command, and if you accidentally press R (reset) or Q (quit), you lose
all your work. Also, there's no multiple-substition. To me it's nicer
to be able to say "bxq the" when you've figured out which word is
"the".

cryptoslam does a letter frequency count, but it's on a separate Tools
screen so you can't see it while you look at the puzzle.

cryptoslam starts up with a help screen, which is fine, I guess, but
it's an unskippable intro (you have to press T for tools, then G for
generate, before you can play the game).

cryptoslam does use color a bit, but IMO doesn't make good use of
it. Hopefully I've done a little better here.

cryptoslam doesn't keep track of your score (# of moves and time).

cryptoslam's puzzle generation is unnecessarily slow, due to using a
temporary file for fortune's output. Even worse, it writes it in the
current directory as tmp.tmp, and doesn't remove it. Even worse than
that, if it can't create the tmp file, there's no error message (or
actually there is, visible *very* briefly), it just re-encrypts the
welcome text and presents that as the new puzzle. Over and over again.

One thing cryptoslam does do, that cryptokwot doesn't: it has a "save
game" function. I decided this was overkill (but added the Pause
feature, to accomplish basically the same thing).
