#!/usr/bin/perl -w
########################################################################
# COMP2521 19T0 ... the Fury of Dracula
# 19T0.dracula/webfiles/index.pl: show rounds, etc.
#
#     /
#     /roundXX
#     /roundXX/rank/hunter
#     /roundXX/rank/dracula
#     /roundXX/gNNNNNN
#     /roundXX/gNNNNNN/compile
#     /roundXX/gNNNNNN/gMMMMMM
#     /roundXX/gNNNNNN/gMMMMMM/raw
#     /roundXX/gNNNNNN/gMMMMMM/pastplays

use v5;
use strict;
use warnings;

use utf8;
use feature 'unicode_strings';
binmode STDOUT, ":utf8";

use Cwd;
use Encode qw{encode decode};
use File::Basename;
use HTML::Entities qw{encode_entities_numeric};

my $COURSE  = 'cs2521';
my $SESSION = '19T0';
my $FS_ROOT = "/web/${COURSE}ass/$SESSION.dracula";
my $ROOT = "/~${COURSE}ass/$SESSION.dracula";
my $ROOT_PL = "$ROOT/index.pl";

my $DEBUG = 0;

sub route($);
sub tmpl($;%);
sub depathifier($);
sub escaped($);
sub trimmed($);
sub say;
sub __404();

sub list_rounds();
sub round_stats($);
sub round_ranks($$);
sub round_group_home($$);
sub group_lines_to_table($);
sub round_group_compile($$);
sub round_log($$$);
sub round_log_raw($$$);
sub round_log_past_plays($$$);
sub round_log_content($$$);
sub round_log_parse($$);
sub round_log_turn($);
sub round_log_describe_play_and_move($);
sub round_log_game_end($);

########################################################################
# the most hopeless templating system ... in the world
my %TEMPLATES = ();
{
	my $capture = "";
	while (<DATA>) {
		chomp;
		if (/^%% (.*)$/) { $capture = $1; next; }
		$TEMPLATES{$capture} = "" unless exists $TEMPLATES{$capture};
		$TEMPLATES{$capture} .= "$_\n";
	}
};

sub tmpl($;%) {
	my ($tmpl, %opts) = @_;
	$tmpl = $TEMPLATES{$tmpl};
	$opts{'ROOT_PL'} = $ROOT_PL;
	for my $k (keys %opts) {
		my $v = $opts{$k};
		$tmpl =~ s#\{\{\s?$k\s?\}\}#$v#gs;
	}
	$tmpl
}

sub escaped($) {
	($_) = @_;
	s/&/&amp;/mg; s/</&lt;/mg; s/>/&gt;/mg;
	return $_
}

sub trimmed($) {
	($_) = @_;
	s/^  //mg;
	return $_
}

sub depathifier($) {
	($_) = @_;
	s#/home/cs2521ass/19T0.dracula/round\d\d[a-z]?/compiled/##mg;
	return $_
}

########################################################################
# oh yeah
my $STATUS = undef;
my $CONTENT_TYPE = 'text/html';
my $OUTPUT_BUFFER = "";
sub say { $OUTPUT_BUFFER .= join '', @_; }
sub __404() {
	$STATUS = "404 Not Found";
	say tmpl 'header';
	say tmpl '404';
	say tmpl 'footer';
	goto finish;
}


########################################################################
# the worst application router ... in the world.

sub route($) {
	my ($pathi) = @_;

	my $RE_ROUND = qr#round(\d{2}[a-z]?)#;
	my $RE_RANK  = qr#rank/(hunter|dracula)#;
	my $RE_GROUP = qr#(g\d{6}|z\d{7})#;

	if ($pathi =~ m#^/?$#) {
		list_rounds();

	} elsif ($pathi =~ m#^/$RE_ROUND$#) {
		round_stats($1);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_RANK$#) {
		round_ranks($1, $2);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_GROUP$#) {
		round_group_home($1, $2);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_GROUP/compile$#) {
		round_group_compile($1, $2);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_GROUP/$RE_GROUP$#) {
		round_log($1, $2, $3);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_GROUP/$RE_GROUP/raw$#) {
		round_log_raw($1, $2, $3);

	} elsif ($pathi =~ m#^/$RE_ROUND/$RE_GROUP/$RE_GROUP/pastplays$#) {
		round_log_past_plays($1, $2, $3);

	} else {
		__404;
	}
}


########################################################################
sub mtime($) { (stat($_[0]))[9] }

sub list_group_item($$;$$) {
	my ($href, $lhs, $rhs, $rhs_style) = @_;

	my @classes = qw{list-group-item list-group-item-action};
	push @classes,
	  "d-flex justify-content-between align-items-center"
	  if $rhs;

	$_ = "<a class=\"@classes\" href=\"${href}\">";
	$_ .= $lhs;
	$_ .= " <span class=\"badge $rhs_style\">" if $rhs;
	$_ .= $rhs if $rhs;
	$_ .= "</span>" if $rhs;
	$_ .= "</a>\n";
	return $_;
}


########################################################################
sub load_gid_map() {
	my %gid_to_name = ();
	open my $fh, '<', "$FS_ROOT/gid_to_groupname.txt"
		or die "couldn't read 'gid_to_groupname.txt': $!";
	while ($_ = $fh->getline) {
		chomp; s/^\s+//; s/\s+$//;
		my ($gid, $gname) = split "\t", $_, 2;
		$gid_to_name{$gid} =
		  encode_entities_numeric
		  decode('UTF-8', $gname);
	}
	$fh->close;
	return \%gid_to_name;
}


sub show_update() {
	say "<div class=\"card my-2\">\n";
	say "  <div class=\"card-body\">\n";
	if (open my $fh, "<", "$FS_ROOT/update.inc") {
		say join('', $fh->getlines);
		$fh->close;
	} else {
		say "    uhh.... what's up, doc\n";
	}
	say "  </div>\n";
	say "</div>\n";
}


########################################################################
# another language, another Fury-of-Dracula map implementation...
my %LOCATIONS = (
	'AS' => ['ADRIATIC_SEA', '', 'SEA'],
	'AL' => ['ALICANTE', '', 'LAND'],
	'AM' => ['AMSTERDAM', '', 'LAND'],
	'AT' => ['ATHENS', '', 'LAND'],
	'AO' => ['ATLANTIC_OCEAN', '', 'SEA'],
	'BA' => ['BARCELONA', '', 'LAND'],
	'BI' => ['BARI', '', 'LAND'],
	'BB' => ['BAY_OF_BISCAY', '', 'SEA'],
	'BE' => ['BELGRADE', '', 'LAND'],
	'BR' => ['BERLIN', '', 'LAND'],
	'BS' => ['BLACK_SEA', '', 'SEA'],
	'BO' => ['BORDEAUX', '', 'LAND'],
	'BU' => ['BRUSSELS', '', 'LAND'],
	'BC' => ['BUCHAREST', '', 'LAND'],
	'BD' => ['BUDAPEST', '', 'LAND'],
	'CA' => ['CADIZ', '', 'LAND'],
	'CG' => ['CAGLIARI', '', 'LAND'],
	'CD' => ['CASTLE_DRACULA', '', 'LAND'],
	'CF' => ['CLERMONT_FERRAND', '', 'LAND'],
	'CO' => ['COLOGNE', '', 'LAND'],
	'CN' => ['CONSTANTA', '', 'LAND'],
	'DU' => ['DUBLIN', '', 'LAND'],
	'ED' => ['EDINBURGH', '', 'LAND'],
	'EC' => ['ENGLISH_CHANNEL', '', 'SEA'],
	'FL' => ['FLORENCE', '', 'LAND'],
	'FR' => ['FRANKFURT', '', 'LAND'],
	'GA' => ['GALATZ', '', 'LAND'],
	'GW' => ['GALWAY', '', 'LAND'],
	'GE' => ['GENEVA', '', 'LAND'],
	'GO' => ['GENOA', '', 'LAND'],
	'GR' => ['GRANADA', '', 'LAND'],
	'HA' => ['HAMBURG', '', 'LAND'],
	'IO' => ['IONIAN_SEA', '', 'SEA'],
	'IR' => ['IRISH_SEA', '', 'SEA'],
	'KL' => ['KLAUSENBURG', '', 'LAND'],
	'LI' => ['LEIPZIG', '', 'LAND'],
	'LE' => ['LE_HAVRE', '', 'LAND'],
	'LS' => ['LISBON', '', 'LAND'],
	'LV' => ['LIVERPOOL', '', 'LAND'],
	'LO' => ['LONDON', '', 'LAND'],
	'MA' => ['MADRID', '', 'LAND'],
	'MN' => ['MANCHESTER', '', 'LAND'],
	'MR' => ['MARSEILLES', '', 'LAND'],
	'MS' => ['MEDITERRANEAN_SEA', '', 'SEA'],
	'MI' => ['MILAN', '', 'LAND'],
	'MU' => ['MUNICH', '', 'LAND'],
	'NA' => ['NANTES', '', 'LAND'],
	'NP' => ['NAPLES', '', 'LAND'],
	'NS' => ['NORTH_SEA', '', 'SEA'],
	'NU' => ['NUREMBURG', '', 'LAND'],
	'PA' => ['PARIS', '', 'LAND'],
	'PL' => ['PLYMOUTH', '', 'LAND'],
	'PR' => ['PRAGUE', '', 'LAND'],
	'RO' => ['ROME', '', 'LAND'],
	'SA' => ['SALONICA', '', 'LAND'],
	'SN' => ['SANTANDER', '', 'LAND'],
	'SR' => ['SARAGOSSA', '', 'LAND'],
	'SJ' => ['SARAJEVO', '', 'LAND'],
	'SO' => ['SOFIA', '', 'LAND'],
	'JM' => ['ST_JOSEPH_AND_ST_MARYS', '', 'LAND'],
	'ST' => ['STRASBOURG', '', 'LAND'],
	'SW' => ['SWANSEA', '', 'LAND'],
	'SZ' => ['SZEGED', '', 'LAND'],
	'TO' => ['TOULOUSE', '', 'LAND'],
	'TS' => ['TYRRHENIAN_SEA', '', 'SEA'],
	'VA' => ['VALONA', '', 'LAND'],
	'VR' => ['VARNA', '', 'LAND'],
	'VE' => ['VENICE', '', 'LAND'],
	'VI' => ['VIENNA', '', 'LAND'],
	'ZA' => ['ZAGREB', '', 'LAND'],
	'ZU' => ['ZURICH', '', 'LAND'],
);

sub is_valid_location($) { return exists $LOCATIONS{$_[0]}; }
sub is_virtual_location($) {
	my ($id) = @_;
	return $id =~ /^HI|TP|D[1-5]|[CS]\?|\?\?/;
}

########################################################################
sub list_rounds() {
	say tmpl 'header';
	say "<div class=\"list-group\">\n";

	my @rounds =
		sort { mtime($a) <=> mtime($b) }
		glob "$FS_ROOT/round*/compilation_logs";

	for my $round_path (@rounds) {
		my $round = $round_path;
		$round =~ s@/compilation_logs$@@;
		$round = basename $round;
		$round =~ s/round//;

		say list_group_item(
			"$ROOT_PL/round${round}",
			"Round ${round}",
			scalar localtime(mtime($round_path)),
			($round =~ /[a-z]/)
				? "badge-primary"
				: "badge-info");
	}

	say "</div>\n";

	show_update;
	say tmpl 'footer';
}

########################################################################
sub round_stats($) {
	my ($round) = @_;
	my $round_dir = "$FS_ROOT/round${round}";
	return __404 if not -e $round_dir;

	my $gid_to_name = load_gid_map;

	say tmpl 'header';
	say tmpl 'round_stats_head',
		"round" => $round,
		"ran_at" => scalar localtime(mtime("$round_dir/compilation_logs"))
		;

	say "<div class=\"list-group\">\n";

	for my $sub (<$round_dir/compilation_logs/*>) {
		my ($group, $n, $s) = fileparse $sub;
		$group =~ s/\.compilation_log//;

		my $hunter_data = "${round_dir}/${group}.hunter.txt";
		my $dracula_data = "${round_dir}/${group}.dracula.txt";
		my $compile_status = -e $hunter_data and -e $dracula_data;

		say list_group_item(
			"$ROOT_PL/round${round}/${group}",
			"${group}: ".$gid_to_name->{$group},
			($compile_status
				? "compile ok"
				: "compile error"),
			($compile_status
				? "badge-success"
				: "badge-danger"));
	}

	say "</div>\n";
	say tmpl 'footer';
}

########################################################################
sub round_ranks($$) {
	my ($round, $h6_d7) = @_;
	return __404 if not -e "$FS_ROOT/round${round}";

	my $file = "$FS_ROOT/round${round}/".$h6_d7."-rankings";
	return __404 if not -e $file;

	my $gid_to_name = load_gid_map;

	say tmpl 'header';
	if ($h6_d7 eq "hunter") {
		say tmpl 'round_ranks_hunter', "round" => $round;
	} else {
		say tmpl 'round_ranks_dracula', "round" => $round;
	}

	say tmpl 'round_ranks_table_header';
	open my $fh, "<", $file;
	while ($_ = $fh->getline) {
		chomp;
		my ($rank, $round, $hunter, $avg, $disq, $avgrounds) =
			split ':', $_, 6;
		say tmpl 'round_ranks_table_row',
			"round" => $round,
			"rank" => $rank,
			"hunter" => $hunter,
			"hunter_name" => $gid_to_name->{$hunter},
			"avg_score" => (sprintf '%0.1f', $avg),
			"avg_rounds" => (sprintf '%0.1f', $avgrounds),
			"disqualified" => $disq
			;
	}
	$fh->close;
	say tmpl 'round_ranks_table_footer';

	say tmpl 'explain_ranks';
	say tmpl 'footer';
}


########################################################################
sub round_group_home($$) {
	my ($round, $group) = @_;
	my $round_dir = "${FS_ROOT}/round${round}";
	return __404 if not -e $round_dir;
	return __404 if not -e "${round_dir}/compilation_logs/${group}.compilation_log";

	my $gid_to_name = load_gid_map;

	say tmpl 'header';
	say tmpl 'round_group_home_header',
		"round" => $round,
		"group" => $group,
		"group_name" => $gid_to_name->{$group}
		;

	my $hunter_data = "${round_dir}/${group}.hunter.txt";
	my $dracula_data = "${round_dir}/${group}.dracula.txt";

	if (not -e $hunter_data or not -e $dracula_data) {
		say "<a class=\"btn btn-danger\" ";
	} else {
		say "<a class=\"btn btn-success\" ";
	}
	say "href=\"$ROOT_PL/round${round}/${group}/compile\">";
	say "compile log</a>";

	if (-e $hunter_data) {
		say "<h4>... as Hunters</h4>\n";
		group_lines_to_table $hunter_data;
	}

	if (-e $dracula_data) {
		say "<h4>... as Dracula</h4>\n";
		group_lines_to_table $dracula_data;
	}

	say tmpl 'footer';
}

sub group_lines_to_table($) {
	my ($data) = @_;
	say tmpl 'group_line_table_header';

	open my $fh, "<", $data;
	while ($_ = $fh->getline) {
		chomp;
		my ($dracula, $url, $score, $nrounds, $hunter_dq, $encounter,
			$traps, $teleport, $hides, $double, $land_traps, $sea_turns,
			$land_vampires, $mature_vampires, $vamp_kill)
				= split ':', $_, 15;

		# round00c/logs/h-g014917-d-g014898.log.xz
		my ($round, $logs, $file) = split '/', $url, 3;
		$file =~ s@\.log\.xz$@@;
		my ($_h, $hh, $_d, $dd) = split '-', $file, 4;
		$url = "$ROOT_PL/${round}/${hh}/${dd}";

		say tmpl 'group_line_table_row',
			"_round" => $round,
			"hunter" => $hh,
			"dracula" => $dd,
			"opponent" => $dracula,
			"score" => $score,
			"n_rounds" => $nrounds,
			"disqualified" => $hunter_dq,
			"encounter_dracula" => $encounter,
			"encounter_trap" => $traps,
			"dracula_tp" => $teleport,
			"dracula_hi" => $hides,
			"dracula_dn" => $double,
			"land_traps" => $land_traps,
			"sea_turns" => $sea_turns,
			"land_vamps" => $land_vampires,
			"mature_vamps" => $mature_vampires,
			"killed_vamps" => $vamp_kill
			;
	}
	$fh->close;

	say tmpl 'group_line_table_footer';
}

########################################################################
sub round_group_compile($$) {
	my ($round, $group) = @_;
	my $round_dir = "${FS_ROOT}/round${round}";
	return __404 if not -e $round_dir;
	my $compile_log = "${round_dir}/compilation_logs/${group}.compilation_log";
	return __404 if not -e $compile_log;

	my $gid_to_name = load_gid_map;

	say tmpl 'header';
	say tmpl 'round_group_compile_header',
		"round" => $round,
		"group" => $group,
		"group_name" => $gid_to_name->{$group}
		;

	say "<pre style=\"white-space:pre-wrap\">";
	open my $fh, "<", $compile_log;
	say escaped depathifier (join '', $fh->getlines);
	$fh->close;
	say "</pre>";

	say tmpl 'footer';
}

########################################################################
sub round_log($$$) {
	my ($round, $hunter, $dracula) = @_;

	my $gid_to_name = load_gid_map;

	say tmpl 'header';
	say tmpl 'round_log_content_header',
		"round" => $round,
		"hunter" => $hunter,
		"hunter_name" => $gid_to_name->{$hunter},
		"dracula" => $dracula,
		"dracula_name" => $gid_to_name->{$dracula}
		;

	my $content = round_log_content $round, $hunter, $dracula;
	my $state = {};
	for my $line (split "\n", escaped trimmed $content) {
		$state = round_log_parse $state, $line;
	}

	say tmpl 'footer';
}

sub round_log_raw($$$) {
	my ($round, $hunter, $dracula) = @_;
	my $content = round_log_content $round, $hunter, $dracula;

	$CONTENT_TYPE = 'text/plain';
	say $content;
}

sub round_log_past_plays($$$) {
	my ($round, $hunter, $dracula) = @_;
	my $content = round_log_content $round, $hunter, $dracula;

	$CONTENT_TYPE = 'text/plain';
	my @lines = split "\n", $content;
	$content = join '', grep { $_ =~ m#revealedPastPlays:# } @lines;
	$content =~ s#^.*revealedPastPlays: ##;
	say $content;
}

########################################################################
sub round_log_content($$$) {
	my ($round, $hunter, $dracula) = @_;

	my $round_dir = "${FS_ROOT}/round${round}";
	return __404 if not -e $round_dir;

	my $log = "${round_dir}/logs/h-${hunter}-d-${dracula}.log.xz";
	return __404 if not -e $log;

	open my $fh, "xzcat $log |"
		or die "couldn't read logfile $log";
	my $text =
		depathifier
		join('',
			map { decode('UTF-8', $_) }
			$fh->getlines
		);
	$fh->close;

	return $text;
}

########################################################################
sub format_round_id($) { return sprintf '%03d', $_[0] }

sub round_log_parse($$) {
	my ($state, $line) = @_;

	# init the state machine?
	if (not exists $state->{'STATE'}) {
		$state->{'STATE'} = 'init';
		$state->{'header_printed'} = 0;
		$state->{'linum'} = 1;
		$state->{'have_score'} = 0;

		# set game time counters
		$state->{'time'} = 0;

		$state->{'output'} = '';

		$state->{'score'} = 366;
		$state->{'round'} = 0;
		$state->{'game_turn'} = 0;
		$state->{'player'} = -1;
		$state->{'past_plays'} = "";

		$state->{'players'} = [
			{ 'location' => '??',
			  'health' => 9,
			  'time' => 0,
			  'disqualified' => 0,
			  'timeout' => 0 },

			{ 'location' => '??',
			  'health' => 9,
			  'time' => 0,
			  'disqualified' => 0,
			  'timeout' => 0 },

			{ 'location' => '??',
			  'health' => 9,
			  'time' => 0,
			  'disqualified' => 0,
			  'timeout' => 0 },

			{ 'location' => '??',
			  'health' => 9,
			  'time' => 0,
			  'disqualified' => 0,
			  'timeout' => 0 },

			{ 'location' => '??',
			  'health' => 40,
			  'time' => 0,
			  'disqualified' => 0,
			  'timeout' => 0 },
		];

		$state->{'play'} = {
			'move' => '??',
			'message' => '',
			'timer' => 0
		};

		$state->{'move'} = {
			'move' => '??',
			'message' => '',
			'timer' => 0
		};
	}

	my $LOCATION_RE = qr{[A-Z0-9\?]{2}}i;
	my $PLAYER_RE = qr{[0-4]};
	my $HEADER_START_RE =
	  qr{^\s+The FURY of DRACULA.*$};
	my $HEADER_END_RE =
	  qr{^\.{27}$};
	my $TURN_START_RE =
	  qr{^-{74}$};
	my $RUNNING_PLAYER_RE =
	  qr{^&gt; Running player ($PLAYER_RE)\.\.\. $};
	my $PLAYER_IS_DISQUALIFIED_RE =
	  qr{^Player is currently disqualified. Making a move on their behalf.$PLAYER_RE$};
	my $RUNNING_WITH_INPUT_RE =
	  qr{^Running with input: "past_plays": "(.*)"$};
	# qr{^Running with input: "pastPlays": "(.*)"$};
	my $PROGRAM_TIMED_OUT_RE =
	  qr{^Program was terminated after maximum allowed time.$};
	my $PROGRAM_BROKE_RE =
	  qr{^Program exited with code: (-?\d+)$};
	my $PROGRAM_OUTPUT_BARRIER_RE =
	  qr{^={62}$};
	my $PROGRAM_OUTPUT_RE =
	  qr{^Program Output (\d+): };
	my $MOVE_DATA_PARSE_ERR_RE =
	  qr{^Move data parse error: "(.*)"$};
	my $CRASHED_BEFORE_MOVE_RE =
	  qr{^This usually means your program crashed before making a move.$};
	my $MOVE_ERROR_RE =
	  qr{^Move:  \{\}$};
	my $MOVE_BEGIN_RE =
	  qr{^Move:  \{$};
	my $MOVE_MOVE_RE =
	  qr{^  "move": "($LOCATION_RE?)",?$};
	my $MOVE_MESSAGE_RE =
	  qr{^  "message": "(.*)?",?$};
	my $MOVE_TIMER_RE =
	  qr{^  "timer": (\d+),?$};
	my $MOVE_END_RE =
	  qr{^\}$};
	my $GAME_SCORE_RE =
	  qr{^&gt; Game score: (-?\d+)$};
	my $MOVING_FROM_RE =
	  qr{^&gt; Moving from: ($LOCATION_RE|undefined)$};
	my $LIFE_POINTS_RE =
	  qr{^&gt; Life Points: (-?\d+)$};
	my $BLOOD_POINTS_RE =
	  qr{^&gt; Blood Points: (-?\d+)$};
	my $LEGAL_DRAC_MOVES_RE =
	  qr{^&gt; Legal Drac moves:(( $LOCATION_RE)*)$};
	my $DISQUALIFIED_RE =
	  qr{^&gt; Player ($PLAYER_RE) disqualified for illegal move.$};
	my $MADE_MOVE_RE =
	  qr{^&gt; Player ($PLAYER_RE) made move:$};
	my $TURN_MOVE_START_RE =
	  qr{^&gt; \+\+\+ ?$}; # oh my god why
	my $TURN_MOVE_MOVE_RE =
	  qr{^  move: ($LOCATION_RE?)$};
	my $TURN_MOVE_MESSAGE_RE =
	  qr{^  message: (.*)$};
	my $TURN_MOVE_TIMER_RE =
	  qr{^  timer: (\d+)$};
	my $TURN_MOVE_END_RE =
	  qr{^&gt; --- ?$}; # oh my god why
	my $DEFAULT_DRACULA_MOVE_RE =
	  qr{^Default Dracula Move: "($LOCATION_RE)"$};
	my $NO_PLACE_ENCOUNTER_RE =
	  qr{^Dracula does not place an Encounter this turn...$};
	my $CITY_CONTAINS_MINIONS_RE =
	  qr{^City contains \d vampires and \d traps already.$};
	my $DEFAULT_HUNTER_MOVE_RE =
	  qr{^Default Hunter Move "($LOCATION_RE)"$};
	my $ROUND_END_RE =
	  qr{^&gt; End of Round:  (\d+)$};
	my $ROUND_START_RE =
	  qr{^&gt; Starting Round:  (\d+)$};
	my $END_OF_GAME_RE =
	  qr{^&gt; End of game: $};
	my $END_SCORE_RE =
	  qr{^  score: (-?\d+)$};
	my $END_PAST_PLAYS_RE =
	  qr{^  revealedPastPlays:(( [GSHMD]$LOCATION_RE[\.MTDV]{4})+)$};
	my $END_MESSAGES_RE =
	  qr{^  messages: \[(.*)\]$};
	my $GAME_OVER_RE =
	  qr{^&gt; Game Over$};
	study $line;

	my $player = $state->{'players'}->[$state->{'player'}];

	printf '[%20s] (%4d) %s'."\n",
		$state->{'STATE'}, $state->{'linum'}++, $line if $DEBUG;
	################################
	if ($state->{'STATE'} eq 'init') {
		if ($line =~ $HEADER_START_RE) {
			$state->{'STATE'} = 'header_inside';
			say tmpl 'round_log_parsed_header';
			$state->{'header_printed'}++;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'header_inside') {
		if ($line =~ $HEADER_END_RE) {
			$state->{'STATE'} = 'header_following';
		} else {
			# continue!
			goto rlp_end;
		}

	################################
	} elsif ($state->{'STATE'} eq 'header_following'
		or $state->{'STATE'} eq 'turn_end'
		or $state->{'STATE'} eq 'round_start') {

		if ($line =~ $TURN_START_RE) {
			# the engine makes it as hard as possible to handle this >:(
			round_log_describe_play_and_move $state
				if $state->{'STATE'} ne 'header_following';

			$state->{'STATE'} = 'turn_start';
			$state->{'player'} += 1; $state->{'player'} %= 5;
			round_log_turn $state;

			$state->{'play'} =
				{ 'move' => '??', 'message' => '', 'timer' => 0 };
			$state->{'move'} =
				{ 'move' => '??', 'message' => '', 'timer' => 0 };
			$state->{'legal_drac_moves'} = '';

		} elsif ($line =~ $ROUND_END_RE) {
			goto rlp_malformed if ($state->{'round'} != $1);
			$state->{'STATE'} = 'round_end';

		} elsif ($line =~ $DEFAULT_DRACULA_MOVE_RE) {
			goto rlp_malformed if $state->{'player'} != 4;
			$player->{'location'} = $1;

		} elsif ($line =~ $NO_PLACE_ENCOUNTER_RE) {
			goto rlp_malformed if $state->{'player'} != 4;
		} elsif ($line =~ $CITY_CONTAINS_MINIONS_RE) {
			goto rlp_malformed if $state->{'player'} != 4;

		} elsif ($line =~ $DEFAULT_HUNTER_MOVE_RE) {
			goto rlp_malformed if $state->{'player'} == 4;
			$player->{'location'} = $1;

		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'round_end') {
		if ($line =~ $ROUND_START_RE) {
			goto rlp_malformed if ($state->{'round'} != ($1 - 1));
			$state->{'STATE'} = 'round_start';
			$state->{'round'} = $1;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_start') {
		if ($line =~ $RUNNING_PLAYER_RE) {
			goto rlp_malformed if ($state->{'player'} != $1);
			$state->{'STATE'} = 'turn_input';

		} elsif ($line =~ $PLAYER_IS_DISQUALIFIED_RE) {
			$state->{'STATE'} = 'turn_end';
			goto rlp_malformed
			  unless $player->{'disqualified'};
			$player->{'disqualified'}++;

		} elsif ($line =~ $END_OF_GAME_RE) {
			$state->{'STATE'} = 'game_end_+++';

		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_input') {
		if ($line =~ $RUNNING_WITH_INPUT_RE) {
			$state->{'STATE'} = 'turn_output_pre';
			$state->{'past_plays'} = $1;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_output_pre') {
		if ($line =~ $PROGRAM_OUTPUT_BARRIER_RE) {
			$state->{'STATE'} = 'turn_output';
			$state->{'output'} = '';

		} elsif ($line =~ $PROGRAM_TIMED_OUT_RE) {
			$player->{'timeout'}++;

		} elsif ($line =~ $PROGRAM_BROKE_RE) {
			$player->{'broken'}++;

		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_output') {
		if ($line =~ $PROGRAM_OUTPUT_BARRIER_RE) {
			$state->{'STATE'} = 'turn_output_post';
			chomp $state->{'output'};
			say tmpl 'player_output',
				'round.id' =>
			  		format_round_id($state->{'round'}).
					(qw{G S H M D})[$state->{'player'}],
				'output' => $state->{'output'},
				'output.length' => length $state->{'output'},
				'past_plays' => $state->{'past_plays'},
				;
		  } else {
			$line =~ s@^Program Output \((\d+)\): @@;
			$state->{'output'} .= $line;
			$state->{'output'} .= "\n";
		}

	################################
	} elsif ($state->{'STATE'} eq 'turn_output_post') {
		if ($line =~ $MOVE_BEGIN_RE) {
			$state->{'STATE'} = 'turn_play_move_msg_timer';

		} elsif ($line =~ $MOVE_DATA_PARSE_ERR_RE) {
			$state->{'STATE'} = 'move_data_parse_err';
			$player->{'broken'}++;

		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'move_data_parse_err') {
		if ($line =~ $MOVE_ERROR_RE) {
			# disqualify
			$state->{'STATE'} = 'il_gs_ddisq_disq';
		}

	################################
	} elsif ($state->{'STATE'} eq 'turn_play_move_msg_timer') {
		if ($line =~ $MOVE_MOVE_RE) {
			$state->{'play'}->{'move'} = $1;

		} elsif ($line =~ $MOVE_MESSAGE_RE) {
			$state->{'play'}->{'message'} = $1;

		} elsif ($line =~ $MOVE_TIMER_RE) {
			$state->{'play'}->{'timer'} = $1;
			$state->{'time'} += $1;
			$player->{'time'} += $1;

		} elsif ($line =~ $MOVE_END_RE) {
			$state->{'STATE'} = 'il_gs_ddisq_disq';

		} else { goto rlp_malformed; }

	################################
	# initloc or gamescore or ddisq or disq
	} elsif ($state->{'STATE'} eq 'il_gs_ddisq_disq') {
		if ($line =~ $GAME_SCORE_RE) {
			$state->{'have_score'} = 1;
			$state->{'score'} = $1;

		} elsif ($line =~ $MOVING_FROM_RE) {
			$state->{'STATE'} = 'turn_health';
			$state->{'move'}->{'timer'} = $1;

		} elsif ($line =~ $LEGAL_DRAC_MOVES_RE) {
			$state->{'legal_drac_moves'} = $1;

		} elsif ($line =~ $DISQUALIFIED_RE) {
			goto rlp_malformed if ($state->{'player'} != $1);
			$state->{'STATE'} = 'turn_move';
			$player->{'disqualified'}++;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_health') {
		if ($line =~ $LIFE_POINTS_RE && $state->{'player'} != 4) {
			$state->{'STATE'} = 'turn_made_move';
			$player->{'health'} = $1;

		} elsif ($line =~ $BLOOD_POINTS_RE && $state->{'player'} == 4) {
			$state->{'STATE'} = 'turn_made_move';
			$player->{'health'} = $1;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_made_move') {
		if ($line =~ $MADE_MOVE_RE) {
			goto rlp_malformed if ($state->{'player'} != $1);
			$state->{'STATE'} = 'turn_move';
		} elsif ($line =~ $LEGAL_DRAC_MOVES_RE) {
			goto rlp_malformed if ($state->{'player'} != 4);
			# XXX do something?
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'turn_move') {
		if ($line =~ $TURN_MOVE_START_RE) {

		} elsif ($line =~ $TURN_MOVE_MOVE_RE) {
			$state->{'move'}->{'last'} = 'move';
			$player->{'location'} = $1;
			$state->{'move'}->{'move'} = $1;

		} elsif ($line =~ $TURN_MOVE_MESSAGE_RE) {
			$state->{'move'}->{'last'} = 'message';
			$state->{'move'}->{'message'} = $1;

		} elsif ($line =~ $TURN_MOVE_TIMER_RE) {
			$state->{'move'}->{'last'} = 'timer';
			$state->{'move'}->{'timer'} = $1;

		} elsif ($line =~ $TURN_MOVE_END_RE) {
			$state->{'STATE'} = 'turn_end';

		} elsif ($state->{'move'}->{'last'} eq 'message') {
			$state->{'move'}->{'message'} .= "\n". $line;

		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_end_+++') {
		if ($line =~ $TURN_MOVE_START_RE) {
			$state->{'STATE'} = 'game_end_score';
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_end_score') {
		if ($line =~ $END_SCORE_RE) {
			$state->{'STATE'} = 'game_end_pastplays';
			$state->{'score'} = $1;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_end_pastplays') {
		if ($line =~ $END_PAST_PLAYS_RE) {
			$state->{'STATE'} = 'game_end_messages';
			$state->{'past_plays'} = $1;
			$state->{'past_plays'} =~ s/^ //;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_end_messages') {
		if ($line =~ $END_MESSAGES_RE) {
			$state->{'STATE'} = 'game_end_---';
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_end_---') {
		if ($line =~ $TURN_MOVE_END_RE) {
			$state->{'STATE'} = 'game_over';
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'game_over') {
		if ($line =~ $GAME_OVER_RE) {
			$state->{'STATE'} = 'terminal';
			round_log_game_end $state;
		} else { goto rlp_malformed; }

	################################
	} elsif ($state->{'STATE'} eq 'terminal') {
		# terminal state, goodbye

	################################
	} else {
		$state->{'STATE'} = "???".$state->{'STATE'};
rlp_malformed:
		print "!!! malformed\n" if $DEBUG;
		say tmpl 'round_log_parsed_malformed',
			'state' => $state->{'STATE'},
			'line' => $line,
			;
		# say $line; say "\n";
	}

rlp_end:
	return $state;
}

########################################################################
sub round_log_turn($) {
	my ($state) = @_;

	my $curr = " class=\"bg-success\"";
	my $hilight = " class=\"bg-light\"";
	my $disq = " class=\"bg-warning\"";

	my @players = qw{G S H M D};

	my $turn = $state->{'round'};
	my $prevturn = $turn - ($state->{'player'} == 0);
	$prevturn = format_round_id $prevturn;
	$prevturn .= $players[($state->{'player'} - 1) % 5];

	my $nextturn = $turn + ($state->{'player'} == 4);
	$nextturn = format_round_id $nextturn;
	$nextturn .= $players[($state->{'player'} + 1) % 5];

	my %tmpl_params = (
		'round' => $state->{'round'},
		'round.id' => format_round_id $state->{'round'},
		'round.prev' => format_round_id $state->{'round'} - 1,
		'round.next' => format_round_id $state->{'round'} + 1,
		'round.player' => $players[$state->{'player'}],
		'round.prevturn' => $prevturn,
		'round.nextturn' => $nextturn,
		'score' => $state->{'have_score'} ? $state->{'score'} : '',
	);

	for my $i (0..4) {
		my $P = $players[$i];
		my $p = $state->{'players'}->[$i];

		$tmpl_params{"$P.hi"} =
		  ( $p->{'disqualified'} ? $disq :
			( $state->{'player'} == $i ? $curr : ''));

		$tmpl_params{"$P.loc"} =
			$p->{'location'};

		$tmpl_params{"$P.loc.hi"} = '';
		$tmpl_params{"$P.loc.hi"} = " class=\"bg-info\""
			if $i != 4 and $p->{'location'} eq "JM";
		$tmpl_params{"$P.loc.hi"} = " class=\"bg-light\""
			if $i == 4 and is_virtual_location $p->{'location'};

		$tmpl_params{"$P.hp"} = $p->{'health'};
		$tmpl_params{"$P.hp"} .= " LP" if $i != 4;
		$tmpl_params{"$P.hp"} .= " BP" if $i == 4;
		$tmpl_params{"$P.hp"} = '' if $p->{'disqualified'};
		$tmpl_params{"$P.hp.hi"} = '';
		$tmpl_params{"$P.hp.hi"} = " class=\"bg-danger\""
			if $i != 4 and $p->{'health'} <= 3;
		$tmpl_params{"$P.hp.hi"} = " class=\"bg-danger\""
			if $i == 4 and $p->{'health'} <= 10;
	}

	say tmpl 'round_log_turn_status', %tmpl_params;
}

########################################################################
sub round_log_describe_play_and_move($) {
	my ($state) = @_;

	my %tmpl_params = (
		'play_mmt' => (tmpl 'mmt', %{$state->{'play'}}),

		'illegal_move' => '',
		'player_crashed' => '',
		'player_timeout' => '',
		'player_disqualified' => '',
	);

	my $player = $state->{'players'}->[$state->{'player'}];
	if ($player->{'disqualified'} == 1) {
		# player just disqualified
		$tmpl_params{'illegal_move'} =
			tmpl 'illegal_move_message',
				'move' => $state->{'play'}->{'move'},
				'legal_moves' => $state->{'legal_drac_moves'};
	}

	if ($player->{'disqualified'} >= 1) {
		$tmpl_params{'player_disqualified'} =
			tmpl 'player_disqualified_message',
				'move' => $player->{'location'};
		$tmpl_params{'play_mmt'} = '';
		$tmpl_params{'move_mmt'} = '';
	}

	$tmpl_params{'player_timeout'} = tmpl 'player_timed_out_message'
		if ($player->{'timeout'});
	$tmpl_params{'player_crashed'} = tmpl 'player_crashed_message'
		if ($player->{'broken'});

	say tmpl 'round_log_describe_play_and_move', %tmpl_params;
}

########################################################################
sub round_log_game_end($) {
	my ($state) = @_;

	my %tmpl_params = (
		'rounds' => $state->{'round'},
		'score' => $state->{'score'},
		'past_plays' => $state->{'past_plays'},
	);

	my @players = qw{G S H M D};
	for my $i (0..4) {
		my $P = $players[$i];
		my $p = $state->{'players'}->[$i];

		$tmpl_params{"$P.time"} = $p->{'time'};

		my $percent = 100 * $p->{'time'} / $state->{'time'};
		$tmpl_params{"$P.pc"} = sprintf '%3.1f', $percent;
		$tmpl_params{"$P.width"} = sprintf '%3.1f', $percent;
	}

	say tmpl 'game_over', %tmpl_params;
}

########################################################################
my $path = "";
if (exists $ENV{'PATH_INFO'}) {
	$path = $ENV{'PATH_INFO'};
} elsif ($#ARGV == 0) {
	$DEBUG = 1;
	$path = $ARGV[0];
}

$path =~ s#/$##;
route $path;

finish:
print "Status: $STATUS\n" if defined $STATUS;
print "Content-type: $CONTENT_TYPE; charset=utf-8\n\n";
print $OUTPUT_BUFFER;
1;

########################################################################
########################################################################
########################################################################

__END__;
%% 404

<div class="alert alert-danger w-100 mx-auto text-center">
  <h4>Not found</h4>
  You've stumbled off the trail!
</div>

%% header
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="stake" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
  <meta name="author" content="COMP2521 19T0" />

  <title>COMP2521 19T0 / The Fury of Dracula / The Hunt</title>
  <style type="text/css">code{white-space: pre;}</style>
  <link rel="stylesheet" href="/~cs2521/19T0/assets/dracula.css" />
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
    crossorigin="anonymous" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" />
  <style type="text/css">
.drac-header
  { background-image: linear-gradient(to right, #270907 40%, #3D0A07 60%)
  ; background-color: #270907 }
.drac-header > img { margin: 0 auto; }
main { max-width: 120ex !important; margin-left: auto; margin-right: auto; }
pre  { padding: .5rem; border-radius: .25rem; }
pre, code, tt { color: #e9ecef; background-color: #343a40; }

.faq dd { margin-left: 2rem; }
.faq dd p:first-child { margin-bottom: 0 }
footer.bg-dark { background-color: #343a40 !important; }
  </style>
</head>
<body class="d-flex flex-column" style="min-height: 100vh">
<header class="drac-header text-center mb-2">
  <img src="/~cs2521/19T0/assignments/dracula/Pics/drac.png" alt="The Fury of Dracula" />
</header>

<div class="text-center">
<p class="lead">
<small class="text-muted">
  courtesy Richard Buckland, John Shepherd, and many tutors
</small>
</p>
<h1><a href="/~cs2521ass/19T0.dracula/index.pl" class="font-weight-bold text-danger">The Hunt</a></h1>
</div>
<main class="container">

%% footer
</main>

<footer class="mt-3 py-3 text-center no-print bg-dark">
  <p class="text-muted">
    <strong>COMP2521 19T0: Data Structures and Algorithms</strong>
    is brought to you by <br />
    the <a href="https://www.cse.unsw.edu.au/">School of Computer Science and Engineering</a>
    at the <a href="https://www.unsw.edu.au/">University of New South Wales</a>, Sydney.<br />
    For all enquiries, please email the class account at
    <a href="mailto:cs2521@cse.unsw.edu.au">cs2521@cse.unsw.edu.au</a><br />

    <small>CRICOS Provider 00098G</small>
  </p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.29.4/js/jquery.tablesorter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
	$('[data-toggle="tooltip"]').tooltip();
	$("table").tablesorter();
});
</script>
</body>
</html>

%% round_stats_head
<h2 class="text-center">
round {{ round }}<br />
<small>(ran at {{ ran_at }})</small>
</h2>
<p class="text-center">
rank by
<a href="{{ROOT_PL}}/round{{round}}/rank/hunter">hunter</a> /
<a href="{{ROOT_PL}}/round{{round}}/rank/dracula">dracula</a>
</p>

%% round_ranks_hunter
<h2 class="text-center">
<small><a href="{{ROOT_PL}}/round{{round}}">
	round {{ round }}</a></small><br />
Hunter Ranks
</h2>
<p class="text-center">
rank by
<strong>hunter</strong> /
<a href="{{ROOT_PL}}/round{{round}}/rank/dracula">dracula</a>
</p>

%% round_ranks_dracula
<h2 class="text-center">
<small><a href="{{ROOT_PL}}/round{{round}}">
	round {{ round }}</a></small><br />
Dracula Ranks
</h2>
<p class="text-center">
rank by
<a href="{{ROOT_PL}}/round{{round}}/rank/hunter">hunter</a> /
<strong>dracula</strong>
</p>

%% round_ranks_table_header
<table class="table table-striped table-sm">
<thead>
<tr><th width="10%">Rank</th>
	<th>Team</th>
	<th width="10%">Score</th>
	<th width="10%">Avg<br/>Rounds</th>
	<th width="10%">Disq?</th>
</tr></thead>
<tbody>

%% round_ranks_table_row
<tr><td>{{ rank }}</td>
	<td><a href="{{ROOT_PL}}/round{{round}}/{{hunter}}">{{ hunter }}</a>
		{{ hunter_name }}</td>
	<td>{{ avg_score }}</td>
	<td>{{ avg_rounds }}</td>
	<td>{{ disqualified }}</td>
</tr>

%% round_ranks_table_footer
</tbody>
</table>

%% explain_ranks
<p class="text-center">
  Score = total_score/games_played.<br />
  <small>(where total_score = sum(game_score - disqual_factor);<br />
    for hunters, disqual_factor = 0.25, for dracula, 1)</small><br />
  Score listed for Dracula is 366 - hunter_score.
</p>
<p class="text-center">
  For hunters, the total number of disqualifications<br/>
  is up to 4x the number of games.
</p>

%% round_group_home_header
<h2 class="text-center">
<small><a href="{{ROOT_PL}}/round{{round}}">
	round {{ round }}</a></small><br />
<span class="badge badge-dark">{{ group }}</span>
{{ group_name }}
</h2>

%% round_group_compile_header
<h2 class="text-center">
<small><a href="{{ROOT_PL}}/round{{round}}">
	round {{ round }}</a></small><br />
<small><a href="{{ROOT_PL}}/round{{round}}/{{group}}">
	<span class="badge badge-dark">{{ group }}</span>
	{{ group_name }}</a></small><br />
compile log
</h2>

%% group_line_table_header
<table class="table table-striped table-sm">
<thead>
<tr><th colspan="4">General</th>
	<th colspan="2">Encounters</th>
	<th colspan="5">Dracula's Actions</th>
	<th colspan="3">Vampires</th>
</tr>
<tr><th>Opponent</th>
	<th title="The final game score.">Score</th>
	<th title="The number of rounds this game continued for.">#Rounds</th>
	<th title="Was this group's submission disqualified?">Disq.?</th>

	<th title="Number of times hunter(s) encountered Dracula.">Drac.</th>
	<th title="Number of times hunter(s) encountered traps.">Traps</th>

	<th title="Teleport">TP</th>
	<th title="Hide">HI</th>
	<th title="Double-backs">D<em>n</em></th>
	<th title="Number of traps Dracula set.">Traps</th>
	<th title="Number of turns Dracula was at sea.">@Sea</th>

	<th title="Number of vampires Dracula created.">V</th>
	<th title="Number of vampires that matured.">M!</th>
	<th title="Number of vampires killed by Hunters.">K.</th>
</tr>
</thead>
<tbody>

%% group_line_table_row
<tr><td><a href="{{ROOT_PL}}/{{_round}}/{{hunter}}/{{dracula}}">{{ opponent }}</a></td>
	<td>{{ score }}</td>
	<td>{{ n_rounds }}</td>
	<td>{{ disqualified }}</td>
	<td>{{ encounter_dracula }}</td>
	<td>{{ encounter_trap }}</td>
	<td>{{ dracula_tp }}</td>
	<td>{{ dracula_hi }}</td>
	<td>{{ dracula_dn }}</td>
	<td>{{ land_traps }}</td>
	<td>{{ sea_turns }}</td>
	<td>{{ land_vamps }}</td>
	<td>{{ mature_vamps }}</td>
	<td>{{ killed_vamps }}</td>
</tr>

%% group_line_table_footer
</tbody>
</table>

%% round_log_content_header
<h2 class="text-center">
<small><a href="{{ROOT_PL}}/round{{round}}">
	round {{ round }}</a></small><br />
<strong><a class="btn btn-primary" href="{{ROOT_PL}}/round{{round}}/{{hunter}}">{{ hunter }}</a>
{{ hunter_name }}</strong>
vs
<strong><a class="btn btn-danger" href="{{ROOT_PL}}/round{{round}}/{{dracula}}">{{ dracula }}</a>
{{ dracula_name }}</strong>
<br /><small>
<a href="#end">end</a> /
<a href="{{ROOT_PL}}/round{{round}}/{{hunter}}/{{dracula}}/raw">raw</a> /
<a href="{{ROOT_PL}}/round{{round}}/{{hunter}}/{{dracula}}/pastplays">pastplays</a>
</small>
</h2>

%% round_log_parsed_header
<style>pre i{font-style:normal;}pre:hover i{color:red;}</style>
<pre style="width:26ex;margin:0 auto;line-height:1.1;text-align:center">
…the FURY of DRACULA…
        _..._        
      .'     '.      
     ; __   __ ;     
     |/  \ /  \|     
   |\| -- ' -- |/|   
   |(| \<i>o</i>| |<i>o</i>/ |)|   
   _\|    U    |/_   
.-'  | ,.___., |  '-.
\    ;  V'-'V  ;    /
 `\   \       /   /` 
   `\  '-...-'  /`   
     `\  / \  /`     
       `\\_//`       
</pre>

%% round_log_turn_status
<div class="row">
<div class="col-3 text-right">
  <div class="btn-group mt-5" role="group">
    <a class="btn btn-secondary" href="#round{{round.prev}}{{round.player}}" title="Previous round">
	  <i class="fa fa-fw fa-chevron-circle-up"></i></a>
	<a class="btn btn-dark" href="#round{{round.prevturn}}" title="Previous turn">
	  <i class="fa fa-fw fa-caret-up"></i></a>
  </div>
</div>
<div class="col-6">
<table class="table text-center" id="round{{round.id}}{{round.player}}">
<tbody>
<tr><th width="20%">round {{ round }}</th>
	<th width="15%" title="Lord Godalming"{{G.hi}}>G</th>
	<th width="15%" title="Dr Seward"{{S.hi}}>S</th>
	<th width="15%" title="Van Helsing"{{H.hi}}>H</th>
	<th width="15%" title="Mina Harker"{{M.hi}}>M</th>
	<th width="15%" title="Dracula"{{D.hi}}>D</th></tr>

<tr><td></td>
	<td{{G.loc.hi}}>{{ G.loc }}</td>
	<td{{S.loc.hi}}>{{ S.loc }}</td>
	<td{{H.loc.hi}}>{{ H.loc }}</td>
	<td{{M.loc.hi}}>{{ M.loc }}</td>
	<td{{D.loc.hi}}>{{ D.loc }}</td></tr>

<tr><th class="bg-light">{{ score }}</th>
	<td{{G.hp.hi}}>{{ G.hp }}</td>
	<td{{S.hp.hi}}>{{ S.hp }}</td>
	<td{{H.hp.hi}}>{{ H.hp }}</td>
	<td{{M.hp.hi}}>{{ M.hp }}</td>
	<td{{D.hp.hi}}>{{ D.hp }}</td></tr>
</tbody>
</table>
</div>
<div class="col-3">
  <div class="btn-group mt-5" role="group">
    <a class="btn btn-dark" href="#round{{round.nextturn}}" title="Next turn">
      <i class="fa fa-fw fa-caret-down"></i></a>
    <a class="btn btn-secondary" href="#round{{round.next}}{{round.player}}" title="Next round">
      <i class="fa fa-fw fa-chevron-circle-down"></i></a>
  </div>
</div>
</div>

%% player_output
<div id="round{{round.id}}-accordion">
<div class="card">
<div class="card-header">
<button class="btn btn-link" data-toggle="collapse" data-target="#round{{round.id}}-pastplays">
pastPlays input
</button>
</div>
<div id="round{{round.id}}-pastplays" class="collapse show" data-parent="#round{{round.id}}-accordion">
<pre style="white-space: pre-wrap;" class="card-body mb-0">{{ past_plays }}</pre>
</div>
</div>

<div class="card">
<div class="card-header">
<button class="btn btn-link" data-toggle="collapse" data-target="#round{{round.id}}-output">
Program output ({{ output.length }})
</button>
</div>
<div id="round{{round.id}}-output" class="collapse" data-parent="#round{{round.id}}-accordion">
<pre style="white-space: pre-wrap;" class="card-body mb-0">{{ output }}</pre>
</div>
</div>
</div>

%% illegal_move_message

<p class="text-danger">
Illegal move '{{move}}'; legal moves were {{ legal_moves }}
</p>

%% player_crashed_message

<p class="text-warning">
Player crashed.
</p>

%% player_timed_out_message

<p class="text-warning">
Player reached move timeout (1.5s).
</p>

%% player_disqualified_message

<p class="text-danger">
Player is disqualified.
Moving to {{ move }} on their behalf.
</p>

%% mmt
<h5>AI move</h5>
<dl class="row">
<dt class="col-3">Move</dt><dd class="col-9">{{ move }}</dd>
<dt class="col-3">Message</dt><dd class="col-9">{{ message }}</dd>
<dt class="col-3">Timer</dt><dd class="col-9">{{ timer }} μsec</dd>
</dl>

%% round_log_describe_play_and_move
<div class="row">
<div class="col-6">
{{ play_mmt }}
</div>

<div class="col-6">
{{ illegal_move }}
{{ player_crashed }}
{{ player_timeout }}
{{ player_disqualified }}
</div>
</div>

%% round_log_parsed_malformed

<div class="alert alert-danger w-100 mx-auto text-center">
  <h4>internal error: malformed log</h4>
  <p>get in touch with us so we can sort this out...<br/>
  please make sure you provide the URL of this page,<br/>
  and this piece of text:</p>
  <pre>[{{state}}] {{line}}</pre>
</div>

%% game_over

<p class="lead text-center" id="end">
Game over after {{ rounds }} rounds;
final score {{ score }}.
</p>

Revealed past plays:
<pre style="white-space: pre-wrap;">{{ past_plays }}</pre>

Execution times:
<div class="progress my-1" style="height:1rem">
<div class="progress-bar bg-primary mr-2" style="width:{{G.width}}%"></div>
Lord Godalming: {{G.time}} μsec ({{G.pc}}%)
</div>
<div class="progress mb-1" style="height:1rem">
<div class="progress-bar bg-success mr-2" style="width:{{S.width}}%"></div>
Dr Seward: {{S.time}} μsec ({{S.pc}}%)
</div>
<div class="progress mb-1" style="height:1rem">
<div class="progress-bar bg-info mr-2" style="width:{{H.width}}%"></div>
Van Helsing: {{H.time}} μsec ({{H.pc}}%)
</div>
<div class="progress mb-1" style="height:1rem">
<div class="progress-bar bg-warning mr-2" style="width:{{M.width}}%"></div>
Mina Harker: {{M.time}} μsec ({{M.pc}}%)
</div>
<div class="progress my-1" style="height:1rem">
<div class="progress-bar bg-danger mr-2" style="width:{{D.width}}%"></div>
Dracula: {{D.time}} μsec ({{D.pc}}%)
</div>
