# $Id: Action.pm,v 1.5 2006/11/04 10:11:11 mike Exp $

# Action.pm - an action in a Scott Adams game.

package Games::ScottAdams::Action;
use strict;


sub new {
    my $class = shift();
    my($verb, $noun, $num) = @_;

    return bless {
	verb => $verb,
	noun => $noun,
	num => $num,		# 0-based index into Game's list of actions
				### I don't think we actually use this
	comment => undef,	# optional comment to be written through
	cond => [],		# array of conditions to be satisfied
	res => [],		# array of results to be executed
    }, $class;
}


sub verb {
    my $this = shift();
    return $this->{verb};
}

sub noun {
    my $this = shift();
    return $this->{noun};
}

sub comment {
    my $this = shift();
    my($name) = @_;

    my $old = $this->{comment};
    if (defined $name) {
	$this->{comment} = $name;
    }
    return $old;
}


# We'd like to compile these up front so we can complain about
# unrecognised condition and actions while we still know where we are
# in the source file.  Unfortunately, we can't do it in general as the
# action may refer to the names of rooms or items that have not yet
# been defined.  So all we can do at this stage is remember them for
# later.
#
sub add_cond {
    my $this = shift();
    my($text) = @_;

    push @{ $this->{cond} }, $text;
}


sub add_result {
    my $this = shift();
    my($text) = @_;

    push @{ $this->{res} }, $text;
}


# PRIVATE to the compile() method.
sub ARG_NONE { 0 }		# no argument
sub ARG_NUM { 1 }		# argument specifies a flag
sub ARG_ROOM { 2 }		# argument identifies a room
sub ARG_ITEM { 3 }		# argument identifies an item
sub ARG_ITEMROOM { 4 }		# arguments identify an item and a room
sub ARG_ITEMITEM { 5 }		# arguments identify two items

use vars qw(%_cond %_res);	# Global as they need to be visible to "sad"
%_cond = (
	     carried =>		[ 1, ARG_ITEM ],
	     here =>		[ 2, ARG_ITEM ],
	     accessible =>	[ 3, ARG_ITEM ],
	     at =>		[ 4, ARG_ROOM ],
	     '!here' =>		[ 5, ARG_ITEM ],
	     '!carried' =>	[ 6, ARG_ITEM ],
	     '!at' =>		[ 7, ARG_ROOM ],
	     flag =>		[ 8, ARG_NUM ],
	     '!flag' =>		[ 9, ARG_NUM ],
	     loaded =>		[ 10, ARG_NONE ],
	     '!loaded' =>	[ 11, ARG_NONE ],
	     '!accessible' =>	[ 12, ARG_ITEM ],
	     exists =>		[ 13, ARG_ITEM ],
	     '!exists' =>	[ 14, ARG_ITEM ],
	     counter_le =>	[ 15, ARG_NUM ],
#	     counter_ge =>	[ 16, ARG_NUM ],
	     counter_gt =>	[ 16, ARG_NUM ],
		#   ###	The documentation accompanying the scottfree
		#	interpreter says that condition 16 tests for
		#	current counter's value greater than or equal
		#	to the argument, but inspection of the source
		#	shows that it actually tests for strictly
		#	greater-than.
	     '!moved' =>	[ 17, ARG_ITEM ],
	     moved =>		[ 18, ARG_ITEM ],
	     counter_eq =>	[ 19, ARG_NUM ],
	     );

%_res = (
	    get =>		[ 52, ARG_ITEM ],
	    drop =>		[ 53, ARG_ITEM ],
	    moveto =>		[ 54, ARG_ROOM ],
	    destroy =>		[ 55, ARG_ITEM ],
	    set_dark =>		[ 56, ARG_NONE ],
	    clear_dark =>	[ 57, ARG_NONE ],
	    set_flag =>		[ 58, ARG_NUM ],
	    destroy2 =>		[ 59, ARG_ITEM ],
		# Same as 55 in ScottFree
	    clear_flag =>	[ 60, ARG_NUM ],
	    die =>		[ 61, ARG_NONE ],
	    put =>		[ 62, ARG_ITEMROOM ],
	    game_over =>	[ 63, ARG_NONE ],
	    look =>		[ 64, ARG_NONE ],
	    score =>		[ 65, ARG_NONE ],
	    inventory =>	[ 66, ARG_NONE ],
	    set_0 =>		[ 67, ARG_NONE ],
	    clear_0 =>		[ 68, ARG_NONE ],
	    refill_lamp =>	[ 69, ARG_NONE ],		### UNTESTED
	    clear_screen =>	[ 70, ARG_NONE ],		### UNTESTED
	    save_game =>	[ 71, ARG_NONE ],
	    swap =>		[ 72, ARG_ITEMITEM ],
	    continue =>		[ 73, ARG_NONE ],		### UNTESTED
		# Automatic -- is there ever any need to use it explicitly?
	    superget =>		[ 74, ARG_ITEM ],		### UNTESTED
	    put_with =>		[ 75, ARG_ITEMITEM ],
	    look2 =>		[ 76, ARG_NONE ],		### UNTESTED
		# Same as 64 in ScottFree
	    decrease_counter =>	[ 77, ARG_NONE ],
	    print_counter =>	[ 78, ARG_NONE ],
	    set_counter =>	[ 79, ARG_NUM ],
	    swap_loc_default =>	[ 80, ARG_NONE ],
	    select_counter =>	[ 81, ARG_NUM ],		### UNTESTED
		# Current counter is swapped with specified backup counter
	    add_counter =>	[ 82, ARG_NUM ],		### UNTESTED
	    subtract_counter =>	[ 83, ARG_NUM ],		### UNTESTED
	    print_noun =>	[ 84, ARG_NONE ],
	    print_noun_nl =>	[ 85, ARG_NONE ],
	    nl =>		[ 86, ARG_NONE ],
	    swap_loc =>		[ 87, ARG_NUM ],
	    pause =>		[ 88, ARG_NONE ],
	    special =>		[ 89, ARG_NUM ],
		# This is special -- see ../../../../scottfree/Definition
	    );


sub compile {
    my $this = shift();
    my($game) = @_;

    my $verb = $game->resolve_verb($this->verb());
    my $noun = $this->noun();
    if ($verb == 0) {
	# This is a %occur, so the noun is a percentage probability
	$noun = 100 if !$noun;
    } else {
	$noun = $game->resolve_noun($noun);
    }

    my @condval = ( 150*$verb + $noun );
    foreach my $cond (@{ $this->{cond} }) {
	my($opcode, $arg) = _lookup($game, $cond, 'condition', \%_cond);
	$arg = 0 if !defined $arg;
	push @condval, $opcode + 20*$arg;
    }

    die "Oops!  SA format doesn't support >5 conditions in an action"
	if @condval > 6;

    # Now gather results, with parameters going on the end of @condval
    #warn "handling results:\n" . join ('', map {"\t$_\n"}
    #				       @{ $this->{res} });
    my @resval;
    foreach my $res (@{ $this->{res} }) {
	my($opcode, @arg) = _lookup($game, $res, 'result', \%_res);
	push @resval, [ $opcode, @arg ];
    }

    # Right.  This is slightly tricky.  We now want to pack all the
    # results, together with their parameters, into as few action
    # octuplets as possible.  We have four result slots available in
    # the first one, together with zero or more parameter slots
    # remaining in the condition area; thereafter, each action
    # octuplet offers four more result slots together with five
    # parameter slots in the condition area (which of course is one
    # more than we'll ever need.)
    my @conds;			# list of completed octuplets
    my $argslot = @condval;	# 0-based index within current octuplet
    my $resslot = 0;		# 0-based index into "virtual array"
    push @condval, map { 0 } 1..(8-@condval);

    for (my $i = 0; $i < @resval; $i++) {
	my $res = $resval[$i];
	my($opcode, @arg) = @$res;
	@arg = grep { defined } @arg;

	### Seems like 6 in next line should be 5.  Think harder.
	if ($argslot + @arg > 6 || $resslot == 4 ||
	    ($resslot == 3 && $i < @resval-1)) {
	    # Current octuplet is full: skip to next
	    my $cindex = 6 + int($resslot/2);
	    $condval[$cindex] +=
		($resslot % 2 == 0 ? 150 : 1) * 73;
	    push @conds, join(' ', @condval);
	    @condval = map { 0 } 1..8;
	    $argslot = 1;	# because slot 0 holds verb & noun
	    $resslot = 0;
	}

	my $cindex = 6 + int($resslot/2);
	$condval[$cindex] +=
	    ($resslot % 2 == 0 ? 150 : 1) * $opcode;
	$resslot++;
	foreach my $arg (@arg) {
	    if (!defined $arg) {
		print STDERR "", "arg in '@arg' (", scalar(@arg), ") undef\n";
	    }
	    $condval[$argslot] = 20*$arg;
	    $argslot++;
	}
    }

    push @conds, join(' ', @condval);
#print STDERR "", "returning conds: ", join(' -- ', @conds), "\n";
    return @conds;
}


sub _lookup {
    my($game, $text, $caption, $href) = @_;

    $text =~ s/^\s+//;
    my($op, $arg) = split /\s+/, $text, 2;
    if ($op eq 'msg') {
	# This check is a hack, but does spot an otherwise subtle bug
	die "Oops!  `msg' used as a condition (missing %result line?)"
	    if $caption eq 'condition';

	my $mnum = $game->resolve_message($arg);
	return ($mnum <= 51 ? $mnum : $mnum+50);
    }

    my $ref = $href->{$op};
    die "unrecognised $caption op '$op'"
	if !defined $ref;

    my($opcode, $argtype) = @$ref;
    if ($argtype == ARG_NONE) {
	return ($opcode);
    } elsif ($argtype == ARG_NUM) {
	# Numeric argument already has the right numeric value.
    } elsif ($argtype == ARG_ROOM) {
	$arg = $game->resolve_room($arg, 'action');
    } elsif ($argtype == ARG_ITEM) {
	$arg = $game->resolve_item($arg, 'action');
    } elsif ($argtype == ARG_ITEMROOM) {
	my($arg1, $arg2) = split /\s+/, $arg, 2;
	$arg1 = $game->resolve_item($arg1, 'action');
	$arg2 = $game->resolve_room($arg2, 'action');
	return ($opcode, $arg1, $arg2);
    } elsif ($argtype == ARG_ITEMITEM) {
	my($arg1, $arg2) = split /\s+/, $arg, 2;
	$arg1 = $game->resolve_item($arg1, 'action');
	$arg2 = $game->resolve_item($arg2, 'action');
	return ($opcode, $arg1, $arg2);
    } else {
	die "unsupported argument type $argtype for op '$op'";
    }

    return ($opcode, $arg);
}


1;
