/*  TRAVELLER1.PL  */
 
 
:- module traveller.
 
 
:- public run/2,
          go_on/1,
          total_load/2,
          carries/3,
          fuel/2,
          tank_size/2,
          max_load/2,
          cash/2,
          at/2,
          square/2,
          building/2,
          joins/2,
          in/2,
          loop/1,
          sells_fuel/2,
          buys/2,
          sells/2,
          unit_volume/2,        
          adjacent/2,
          clockwise/2,
          distance/3,
          distance_2/3.
 
:- dynamic total_load/2,
           carries/3,
           fuel/2,
           tank_size/2,
           max_load/2,
           cash/2,
           at/2.
 
/*
SPECIFICATION
-------------
 
This is the world-manager for the trading game of Supplement 4. It
exports a predicate for running the game, several predicates that the
world-manager dynamically updates, giving information about the player's
current state, and the board description predicates.
 
Note that this module also loads the route-finder, exported by ROUTE.PL.
 
PUBLIC run( T+, Square+ ):
 
Run player named T, starting at Square. This predicate retracts all the
dynamic predicates for the same player T, and asserts new ones, changing
them as the game proceeds. As well as asserting and retracting dynamic
predicates, the world manager also updates the Tutor's textbase, using
DB.PL.
 
For a version of Traveller that does not do so, see FAST_TRAVELLER.PL.
 
PUBLIC go_on( T+ ):
 
Continue player T, using whatever dynamic predicates already exist.
 
DYNAMIC PUBLIC total_load( T?, Vol? ):
State predicate: Vol is T's total load volume, in cubic feet.
 
DYNAMIC PUBLIC max_load( T?, Vol? ):
State predicate: Vol is T's maximum load capacity, in cubic feet. This
is asserted at the start of a run, but remains constant thereafter.
 
DYNAMIC PUBLIC carries( T?, Good?, Qty? ):
State predicate: Qty is T's stock of Good, in units.
 
DYNAMIC PUBLIC fuel( T?, Qty? ):
State predicate: Qty is T's stock of fuel, in units.
 
DYNAMIC PUBLIC tank_size( T?, Qty? ):
State predicate: Qty is T's maximum fuel tank capacity, in units. This
is asserted at the start of a run, but remains constant thereafter.
 
DYNAMIC PUBLIC cash( T?, C? ):
State predicate: Qty is T's current cash, in pounds sterling.
 
DYNAMIC PUBLIC at( T?, Square? ):
State predicate: Square is T's current location.
 
PUBLIC square( Square?, X?, Y? ):
Board description: X and Y are Square's coordinates. This and the other
board description predicates are all loaded here from BOARD.PL, which is
generated from BOARD.DAT.
 
PUBLIC building( Square?, Building? ):
Board description: Building is the name of a building on Square.
 
PUBLIC joins( Square1?, Square2? ):
Board description: True if Square1 joins Square2. For clockwise to work,
the arguments must be ordered so that if the squares are in the same
loop, Square2 is clockwise of Square1.
 
PUBLIC in( Square?, Loop? ):
Board description: True if Square is in Loop.
 
PUBLIC loop( Loop? ):
Board description: True if loop is the name of a loop.
 
PUBLIC sells_fuel( Building?, Price? ):
Board description: True if Building sells fuel at Price per unit.
 
PUBLIC sells( Building?, Good?, Price? ):
Board description: True if Building sells Good at Price per unit.
 
PUBLIC buys( Building?, Good?, Price? ):
Board description: True if Building buys Good at Price per unit.
 
PUBLIC adjacent( Square1?, Square2? ):
Board description: True if Square1 joins Square2 or Square2 joins
Square1.
 
PUBLIC clockwise( Square1?, Square2? ):
Board description: True if Square2 is in the same loop as square 2, and
immediately clockwise of it.
 
PUBLIC distance( Square1+, Square2+, D- ):
Board description: True if D is the Euclidean distance between the two
squares.
 
PUBLIC distance_2( Square1+, Square2+, D2- ):
Board description: True if D2 is the square of the Euclidean distance
between the two squares. More efficient than distance (which takes a
square root).
 
PUBLIC unit_volume( Good?, Vol? ):
Board (or at least, world) description. True if Vol is the number of
cubic feet one unit of Good occupies.
*/
 
 
/*
IMPLEMENTATION
--------------
 
Not much to this.
*/
 
 
:- needs
   act / 4,         /*  Written by the student.  */
   adda / 1,
   at / 2,
   building / 2,
   buys / 3,
   carries / 3,
   cash / 2,
   del / 1,
   forall / 2,
   fuel / 2,
   in / 2,
   joins / 2,
   max_load / 2,
   non_binding_call / 1,
   output / 1,
   say / 2,
   sells / 3,
   sells_fuel / 2,
   square / 3,
   tank_size / 2,
   total_load / 2,
   unit_volume / 2,
   warn / 2.                     
 
 
/*
        Load the board.
*/
:- lib( board ).
 
 
/*
        Load the route-finder.
*/
:- lib( route ).
 
 
/*  distance_2( S1+, S2+, D- ):
        (Distance)^2 between squares S1 and S2 is D.
*/
distance_2( S1, S2, D ) :-
    square( S1, X1, Y1 ),
    square( S2, X2, Y2 ),
    D is (X1-X2)^2 + (Y1-Y2)^2.
 
 
/*  distance( S1+, S2+, D- ):
        Distance between squares S1 and S2 is D.
*/
distance( S1, S2, D ) :-
    square( S1, X1, Y1 ),
    square( S2, X2, Y2 ),
    D is sqrt( (X1-X2)^2 + (Y1-Y2)^2 ).
 
 
/*  adjacent( S1?, S2? ):
        Square S1 is adjacent to S2.
*/
adjacent( S1, S2 ) :-
    joins( S1, S2 ).
 
adjacent( S1, S2 ) :-
    joins( S2, S1 ).
 
 
/*  clockwise( M?, N? ):
        Square N is one clockwise of M. To make this work, the board
        generator emits joins so that the second argument is clockwise
        of the first, provided that the loops are all defined with the
        segment going clockwise.
 
        This predicate will not work if any square is allowed to be in
        more than one loop at the same time, e.g. if two loops
        intersect.
*/
clockwise( M, N ) :-
    in( M, Region ),
    joins( M, N ),
    in( N, Region ).
 
 
/*  run( T+, N+ ):
        Run player T, starting from square N.
*/
run( T, N ) :-
    not( clause(act(T,_,_,_),_) ),
    !,
    warn( run, 'There are no clauses for'...T<>'\'s actions' ).
 
run( T, N ) :-
    not( square(N,_,_) ),
    !,
    warn( run, N...'is not a square' ).
 
run( T, N ) :-
    initialise( T, N ),
    check_halt( T ),
    run_1( T ).
 
 
/*  go_on( T+ ):
        Continue with player T from where he left off.
*/
go_on( T ) :-
    not( clause(act(T,_,_,_),_) ),
    !,
    warn( go_on, 'There are no clauses for'...T<>'\'s actions' ).
 
go_on( T ) :-
    check_facts( T ),
    check_halt( T ),
    run_1( T ).
 
 
/*  run_1( T+ ):
        Run player T for ever or until out of the game. You may want to
        make this a repeat-fail loop if your system can't optimise the
        tail recursion.
*/
run_1( T ) :-
    act( T, Act, Arg1, Arg2 ),
    !,
    do_action( Act, Arg1, Arg2, T, Status ),
    continue( Status ).
 
run_1( T ) :-
    warn( run, T...'\'s clause(s) for "act" failed' ).
 
 
continue( stop ) :-
    !,
    output( 'Game over.'~ ).
 
continue( _ ) :-
    check_halt( T ),
    run_1( T ).
 
 
/*  do_action( Act+, Arg1+, Arg2+, T+, Status- ):
        Check Act, Arg1 and Arg2 from act, and if OK, update the world
        and player. Status becomes stop if the player is out of the
        game, otherwise ok.
*/
do_action( move, Arg1, Arg2, T, Status ) :-
    !,
    move( T, Arg1, Status ).
 
do_action( buy, fuel, Arg2, T, Status ) :-
    !,
    buy_fuel( T, Arg2, Status ).
 
do_action( buy, Arg1, Arg2, T, Status ) :-
    !,
    buy( T, Arg1, Arg2, Status ).
 
do_action( sell, Arg1, Arg2, T, Status ) :-
    !,
    sell( T, Arg1, Arg2, Status ).
 
do_action( Act, Arg1, Arg2, T, stop ) :-
    warn( run, T...'produced an unrecognised act "'<>Act<>'"' ).
 
 
/*  initialise( T+, N+ ):
        Initialise the player T to start at square N.
*/
initialise( T, N ) :-
    del_nf( total_load(T,_) ),
    forall( carries(T,_,_), del(carries(T,_,_)) ),
    del_nf( fuel(T,_) ),
    del_nf( tank_size(T,_) ),
    del_nf( max_load(T,_) ),
    del_nf( cash(T,_) ),
    del_nf( at(T,_) ),
    adda( total_load(T,0) ),
    forall(
            unit_volume(Good,_)
          ,
            adda( carries(T,Good,0) )
          ),
    adda( fuel(T,20) ),
    adda( tank_size(T,20) ),
    adda( max_load(T,1000) ),
    adda( cash(T,5000) ),
    adda( at(T,N) ).
 
 
/*  check_facts( T+ ):
        Check that T's facts are all there. Message and fail if not.
*/
check_facts( T ) :-
    check( total_load(T,_)  ), 
    check( fuel(T,_) ),
    check( tank_size(T,_) ),
    check( max_load(T,_) ),
    check( cash(T,_) ),
    check( at(T,_) ),
    forall(
            check( carries(T,Good,0) )
          ,
            true
          ).
 
 
/*  check( Clause+ ):
        If there's a clause for Clause, succeed.
        Else warn and fail.
*/
check( Clause ) :-
    non_binding_call( Clause ),
    !.
 
check( Clause ) :-
    warn( go_on, 'You need a clause for'...Clause ),
    fail.
 
 
/*  move( T+, N+, Status- ):
        Do a move of player T to N. Set Status accordingly.
*/
move( T, N, stop ) :-
    at(T,N),
    !,
    warn( move, T...'is already at'...N ).
 
move( T, N, stop ) :-
    at(T,N0),
    not( adjacent(N0,N) ),
    !,
    warn( move, T...'is not in an adjacent square to'...N ).
 
move( T, N, stop ) :-
    fuel( T, F ),
    F < 1,
    !,
    warn( move, T...'has less than one unit of fuel left' ).
 
move( T, N, ok ) :-
    fuel( T, F0 ),
    F is F0-1,
    del( fuel(T,_) ),
    adda( fuel(T,F) ),
 
    del( at(T,_) ),
    adda( at(T,N) ),
 
    say( move, T...'has moved to'...N ).
 
 
/*  buy_fuel( T+, Qty+, Status- ):
        Do a buy fuel of player T. Set Status accordingly.
*/
buy_fuel( T, Qty, stop ) :-
    at( T, N ),
    not(( building( N, B ), sells_fuel(B,_) )),
    !,
    warn( buy_fuel, T...'is not at a fuel station' ).
 
buy_fuel( T, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    sells_fuel( B, Price ),
    Qty is intof(Qty0),
    cash( T, CurrentCash ),
    Cost is Price*Qty,
    (
        CurrentCash < Cost
    ->
        warn( buy_fuel, T...'does not have enough money' ),
        Status = stop
    ;
        fuel( T, CurrentFuel ),
        tank_size( T, TankSize ),
        FuelTotal is CurrentFuel+Qty,
        (
            FuelTotal > TankSize
        ->
            NewFuel = TankSize,
            warn( buy_fuel, T...'has tried to overfill his tank' )
        ;
            NewFuel = FuelTotal
        ),
        NewCash is CurrentCash - Cost,
        del( cash(T,_) ),
        adda( cash(T,NewCash) ),
 
        del( fuel(T,_) ),
        adda( fuel(T,NewFuel) ),
 
        Status = ok,
 
        say( buy_fuel, T...'has bought'...Qty...'units of fuel' )
    ).
 
 
/*  buy( T+, Good+, Qty+, Status- ):
        Do a buy of Good in Qty for player T. Set Status accordingly.
*/
buy( T, Good, Qty, stop ) :-
    at( T, N ),
    not(( building( N, B ), sells(B,Good,_) )),
    !,
    warn( buy, T...'is not at'...a_(Good)...'seller' ).
 
buy( T, Good, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    sells( B, Good, Price ),
 
    Qty is intof(Qty0),
 
    unit_volume( Good, UnitVol ),
    total_load( T, TotalLoadSoFar ),
    ExtraLoad is Qty*UnitVol,
    IntendedTotalLoad is TotalLoadSoFar + ExtraLoad,
    max_load( T, Capacity ),
 
    Cost is Price*Qty,
    cash( T, CurrentCash ),
 
    (
        IntendedTotalLoad > Capacity
    ->
        warn( buy, T...'has exceeded his lorry capacity' ),
        Status = stop
    ;
        CurrentCash < Cost
    ->
        warn( buy, T...'does not have enough money' ),
        Status = stop
    ;
        NewCash is CurrentCash - Cost,
        del( cash(T,_) ),
        adda( cash(T,NewCash) ),
 
        del( total_load(T,_) ),
        adda( total_load(T,IntendedTotalLoad) ),
 
        carries( T, Good, CurrentStockOfGood ),
        NewStock is CurrentStockOfGood + Qty,
        del( carries(T,Good,_) ),
        adda( carries(T,Good,NewStock) ),
 
        Status = ok,
 
        say( buy, T...'has bought'...Qty...'units of'...Good )
    ).
 
 
/*  sell( T+, Good+, Qty+, Status- ):
        Do a sell of Good in Qty for player T. Set Status accordingly.
*/
sell( T, Good, Qty, stop ) :-
    at( T, N ),
    not(( building( N, B ), buys(B,Good,_) )),
    !,
    warn( sell, T...'is not at'...a_(Good)...'buyer' ).
 
sell( T, Good, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    buys( B, Good, Price ),
 
    Qty is intof(Qty0),
 
    carries( T, Good, CurrentStockOfGood ),
    (
        Qty > CurrentStockOfGood
    ->
        warn( sell, T...'does not have enough of'...Good ),
        Status = stop
    ;
        unit_volume( Good, UnitVol ),
        total_load( T, TotalLoadSoFar ),
        SoldLoad is Qty*UnitVol,
 
        NewTotalLoad is TotalLoadSoFar - SoldLoad,
        del( total_load(T,_) ),
        adda( total_load(T,NewTotalLoad) ),
 
        NewStockOfGood is CurrentStockOfGood - Qty,
        del( carries(T,Good,_) ),
        adda( carries(T,Good,NewStockOfGood) ),
 
        Gain is Price*Qty,
        cash( T, CurrentCash ),
        NewCash is CurrentCash + Gain,
        del( cash(T,_) ),
        adda( cash(T,NewCash) ),
 
        Status = ok,
 
        say( sell, T...'has sold'...Qty...'units of'...Good )
    ).
 
 
/*  warn( Act+, Message+ ):
        Warn about illegal state reached during an Act.
*/
warn( Act, Message ) :-
    output( 'During'...a_(Act)<>':'~ ),
    output( Message<>'.'~ ).
 
 
/*  say( Act+, Message+ ):
        Report briefly on new state (in Message) reached after an Act.
*/
say( Act, Message ) :-
    output( 'After'...a_(Act)<>':'~ ),
    output( Message<>'.'~ ).
 
 
/*  check_halt( T+ ):
        Called after each update.
*/
check_halt( T ) :-
    describe(T).
 
 
/*  describe( T+ ):
        Give an English account of player T's state.
*/
describe( T ) :-
 
    at( T, N ),
    output( T...'is on square'...N ),
    (
        building( N, B )
    ->
        output( ' ('<>B<>')' )
    ;
        true
    ),
    output( '.'~ ),
 
    fuel( T, Fuel ),
    tank_size( T, TS ),
    output( '  Fuel'...Fuel...'in tank size'...TS<>'.'~ ),
 
    cash( T, Cash ),
    output( '  Cash'...Cash<>'.'~ ),
 
    total_load( T, Total ),
    max_load( T, Capacity ),
    output( '  Total load'...Total...'cu ft in lorry size'...Capacity<>'.'~ ),
 
    forall(
            carries( T, Good, Stock )
          ,
            (
            unit_volume( Good, UnitVol ),
            Vol is Stock * UnitVol,
            output( '    Stock of'...Good...'='...Stock...'units'...
                    '('<>Vol...'cu ft).'~ )
            )
          ),
 
    nl.
 
 
/*  del_nf( Clause+ ):
        Call 'del'; succeed irrespective.          
*/
del_nf( Clause ) :-
    del( Clause ),
    !.
 
del_nf( Clause ).
 
 
:- endmodule.
