[ Jocelyn Ireson-Paine's Home Page | Try Traveller | Traveller PHP script | Traveller for the student | About the design ]

Traveller source

traveller.pl - the main module

/*  traveller.pl  */


:- dynamic total_load/2,
           carries/3,
           fuel/2,
           tank_size/2,
           max_load/2,
           cash/2,
           at/2,
           flooded/1,
           blockaded/1.

/*
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 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. 

PUBLIC go_on( T+ ):
Continue player T, using whatever dynamic predicates already exist.

PUBLIC flood( Squares+, Prob+ ):
With probability Prob, flood each square in Squares.

PUBLIC dry( Squares+ ):
Dry all flooded squares in Squares.

PUBLIC blockade( Squares+, Prob+ ):
With probability Prob, blockade each square in Squares with petrol
protestors.

PUBLIC unblockade( Squares+ ):
Remove blockades from all squares in Squares.

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 squares( Region+, Squares? ):
Board description: Squares is a list of all squares in Region. If
Region='all', gives all squares on the board.

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.
*/


?- [board].
?- [route].
% Load the board and route-finder.

?- [boardhtml].
% Load the code that generates HTML output.


/*  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 ).


squares( all, Squares ) :-
    bagof( N, Adj1^Adj2^square(N,Adj1,Adj2), Squares ).

squares( Region, Squares ) :-
    bagof( N, in(N,Region), Squares ).


/* step_and_display( N+, T+ ):
     N is either a square or the clauses describing
     the state of a previous run. If a square, step_and_display
     initialises the trader, otherwise continues from
     before. It then runs it one step, and then sends
     the English description, board, and Prolog state
     to the output, where PHP will split off the
     state and save it in the session for the next run.
*/
step_and_display( N, T ) :- 
  step( N, T, PostState ),
  describe( T ), nl,
  write('=========='), nl,
  write_board, nl,
  write('=========='), nl,
  write_state_in_prolog(PostState), nl.
  

/* step( N+, T+, PostState- ):
     Like step_and_display, but returns the
     new state in PostState. It is also left
     as assertions in the database.
*/
step( N, T, PostState ) :-
    integer(N), !,
    consult_trader( T ),
    initialise( T, N ),
    step_from_asserted_state( T, PostState ).

step( PreState, T, PostState ) :-
    consult_trader( T ),
    assert_state_in_prolog( PreState ),
    step_from_asserted_state( T, PostState ).


/* consult_trader( T+ ):
     Load the source for trader T from its file,
     assumed to have a file body whose name is the same.
*/
consult_trader( T ) :-
    concat_atom( ['/home/popx/traveller/', T, '.pl'], File ),   
    consult( File ).


/* step_from_asserted_state( T+, PostState- ):
     Like step, but assumes the pre-move
     state has already been asserted into the database.
*/
step_from_asserted_state( T, PostState ) :-
    ( check_facts( T ) -> 
      ( not( clause(act(T,_,_,_),_) ) ->
          warn( step, 'There are no clauses for ~w\'s actions', [T] )
      ;
          ( act( T, Act, Arg1, Arg2 ) ->
              do_action( Act, Arg1, Arg2, T, Status ),
              ( Status = stop -> PostState = stop ; state_in_prolog(PostState) )
          ;
              warn( step, '~w\'s clause(s) for "act" failed', [T] )
          )
      )
    ;
      true % Message already given.
    ).


/*  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( step, '~w produced an unrecognised act ~w', [T,Act] ).


/*  initialise( T+, N+ ):
        Initialise the player T to start at square N.
*/
initialise( T, N ) :-
    retractall( total_load(T,_) ),
    forall( carries(T,_,_), retractall(carries(T,_,_)) ),
    retractall( fuel(T,_) ),
    retractall( tank_size(T,_) ),
    retractall( max_load(T,_) ),
    retractall( cash(T,_) ),
    retractall( at(T,_) ),
    asserta( total_load(T,0) ),
    forall(
            unit_volume(Good,_)
          ,
            asserta( carries(T,Good,0) )
          ),
    asserta( fuel(T,20) ),
    asserta( tank_size(T,20) ),
    asserta( max_load(T,1000) ),
    asserta( cash(T,5000) ),
    asserta( 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 ) :-
    not(not( Clause )), % non-binding call.
    !.

check( Clause ) :-
    warn( step, 'You need a clause for ~w', [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, '~w is already at ~w', [T,N] ).

move( T, N, stop ) :-
    at(T,N0),
    not( adjacent(N0,N) ),
    !,
    warn( move, '~w is not in an adjacent square to ~w', [T,N] ).

move( T, N, stop ) :-
    fuel( T, F ),
    F < 1,
    !,
    warn( move, '~w has less than one unit of fuel left', [T] ).

move( T, N, Status ) :-
    flooded(N),
    R is random(1000),
    R < 500,
    !,
    ( R < 250 ->
        fuel( T, F0 ),
        F is F0-1,
        retractall( fuel(T,_) ),
        asserta( fuel(T,F) ),
        % Still decrease the fuel, to make things more difficult.
        warn( move, '~w is stuck in a flooded square!', [T] ),
        Status = ok
    ; 
        warn( move, '~w has drowned in the floods!', [T] ),
        Status = stop
    ).

move( T, N, Status ) :-
    blockaded(N),
    R is random(1000),
    R < 500,
    ( R < 250 ->
        fuel( T, F0 ),
        F is random(F0),
        % Random amount of fuel less than F0.
        retractall( fuel(T,_) ),
        asserta( fuel(T,F) ),
        QtyStolen is F0 - F,
        warn( move, '~w has ~w units of fuel stolen by petrol-starved motorists!', [T,QtyStolen] ),        
        Status = ok
    ;
        fuel( T, F0 ),
        F is F0-1,
        retractall( fuel(T,_) ),
        asserta( fuel(T,F) ),
        % Still decrease the fuel, to make things more difficult.
        warn( move, '~w is stuck behind a convoy of fuel protesters!', [T] ),
        Status = ok
    ).

move( T, N, ok ) :-
    fuel( T, F0 ),
    F is F0-1,
    retractall( fuel(T,_) ),
    asserta( fuel(T,F) ),

    retractall( at(T,_) ),
    asserta( at(T,N) ),

    say( move, '~w has moved to ~w<BR>', [T,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, '~w is not at a fuel station<BR>', [T] ).

buy_fuel( T, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    sells_fuel( B, Price ),
    Qty is round(Qty0),
    cash( T, CurrentCash ),
    Cost is Price*Qty,
    (
        CurrentCash < Cost
    ->
        warn( buy_fuel, '~w does not have enough money<BR>', [T] ),
        Status = stop
    ;
        fuel( T, CurrentFuel ),
        tank_size( T, TankSize ),
        FuelTotal is CurrentFuel+Qty,
        (
            FuelTotal > TankSize
        ->
            NewFuel = TankSize,
            warn( buy_fuel, '~w has tried to overfill his tank<BR>', [T] )
        ;
            NewFuel = FuelTotal
        ),
        NewCash is CurrentCash - Cost,
        retractall( cash(T,_) ),
        asserta( cash(T,NewCash) ),

        retractall( fuel(T,_) ),
        asserta( fuel(T,NewFuel) ),

        Status = ok,

        say( buy_fuel, '~w has bought ~w units of fuel<BR>', [T,Qty] )
    ).


/*  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, '~w is not at a seller for ~w<BR>', [T,Good] ).

buy( T, Good, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    sells( B, Good, Price ),

    Qty is round(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, '~w has exceeded his lorry capacity<BR>', [T] ),
        Status = stop
    ;
        CurrentCash < Cost
    ->
        warn( buy, '~w does not have enough money<BR>' ),
        Status = stop
    ;
        NewCash is CurrentCash - Cost,
        retractall( cash(T,_) ),
        asserta( cash(T,NewCash) ),

        retractall( total_load(T,_) ),
        asserta( total_load(T,IntendedTotalLoad) ),

        carries( T, Good, CurrentStockOfGood ),
        NewStock is CurrentStockOfGood + Qty,
        retractall( carries(T,Good,_) ),
        asserta( carries(T,Good,NewStock) ),

        Status = ok,

        say( buy, '~w has bought ~w units of ~w<BR>', [T,Qty,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, '~w is not at a buyer for ~w<BR>', [T,Good] ).

sell( T, Good, Qty0, Status ) :-
    at( T, N ),
    building( N, B ),
    buys( B, Good, Price ),

    Qty is round(Qty0),

    carries( T, Good, CurrentStockOfGood ),
    (
        Qty > CurrentStockOfGood
    ->
        warn( sell, '~w does not have enough of ~w<BR>', [T,Good] ),
        Status = stop
    ;
        unit_volume( Good, UnitVol ),
        total_load( T, TotalLoadSoFar ),
        SoldLoad is Qty*UnitVol,

        NewTotalLoad is TotalLoadSoFar - SoldLoad,
        retractall( total_load(T,_) ),
        asserta( total_load(T,NewTotalLoad) ),

        NewStockOfGood is CurrentStockOfGood - Qty,
        retractall( carries(T,Good,_) ),
        asserta( carries(T,Good,NewStockOfGood) ),

        Gain is Price*Qty,
        cash( T, CurrentCash ),
        NewCash is CurrentCash + Gain,
        retractall( cash(T,_) ),
        asserta( cash(T,NewCash) ),

        Status = ok,

        say( sell, '~w has sold ~w units of ~w<BR>', [T,Qty,Good] )
    ).


/*  warn( Act+, Message+, Vars+ ):
        Warn about illegal state reached during an Act.
*/
warn( Act, Message, Vars ) :-
    format( 'During ~w:~n<BR>', [Act] ),
    format( Message, Vars ), nl.


/*  say( Act+, Message+, Vars+ ):
        Report briefly on new state (in Message and Vars) reached after an Act.
*/
say( Act, Message, Vars ) :-
    format( 'After ~w:~n<BR>', [Act] ),
    format( Message, Vars ), nl.


/*  describe( T+ ):
        Give an English account of player T's state.
*/
describe( T ) :-

    at( T, N ),
    format( '~w is on square ~w', [T,N] ),
    (
        building( N, B )
    ->
        format( ' (~w)', [B] )
    ;
        true
    ),
    format( '.<BR>~n' ),

    fuel( T, Fuel ),
    tank_size( T, TS ),
    format( '  Fuel ~w in tank size ~w.~n<BR>', [Fuel,TS] ),

    cash( T, Cash ),
    format( '  Cash ~w.~n<BR>', [Cash]),

    total_load( T, Total ),
    max_load( T, Capacity ),
    format( '  Total load ~w cu ft in lorry size ~w.~n<BR>', [Total,Capacity] ),

    forall(
            carries( T, Good, Stock )
          ,
            (
            unit_volume( Good, UnitVol ),
            Vol is Stock * UnitVol,
            format( '    Stock of ~w = ~w units (~w cu ft).~n<BR>', [Good,Stock,Vol] )
            )
          ),

    nl.


/* assert_state_in_prolog( Clauses+ ):
     Clauses is a list of lists, each inner
     list being a list of clauses with the
     same functor and arity. The predicate asserts
     them into the database.
*/
assert_state_in_prolog( [] ) :- !.

assert_state_in_prolog( [Clauses|RestClauses] ) :-
    forall( member(Clause,Clauses), 
            assert(Clause)
    ),
    assert_state_in_prolog( RestClauses ).


/* write_state_in_prolog( Clauses+ ):
     Clauses has the same form as above.
     This writes out the list in a form that
     can be read back.
*/
write_state_in_prolog( Clauses ) :-
    write( Clauses ).


/* state_in_prolog( Clauses- ):
     Returns the clauses making up the trader's
     state in Clauses.
*/
state_in_prolog( Clauses ) :-
    list( [ total_load(_,_),
            fuel(_,_),
            tank_size(_,_),
            max_load(_,_),
            cash(_,_),
            at(_,_),
            carries(_,_,_)
          ],
          Clauses
    ).
    

list( [], [] ) :- !.

list( [Head|Heads], [Clauses|RestClauses] ) :-
    findall( C, 
             ( clause(Head,Body), (Body=true -> C=Head ; C=(Head:-Body) ) ),
             Clauses 
    ),
    list( Heads, RestClauses ).
  

flood( Squares, Prob ) :-
    forall( 
            member( N, Squares )
          ,
            ( random(1000) < Prob*1000 -> 
                ( not( flooded(N) ) -> assert( flooded(N) ) ; true )
            ;
              true
            )
          ).


dry( Squares ) :-
    forall( 
            member( N, Squares )
          ,
            retractall( flooded(N) )
          ).


blockade( Squares, Prob ) :-
    forall( 
            member( N, Squares )
          ,
            ( random(1000) < Prob*1000 -> 
                ( not( blockaded(N) ) -> assert( blockaded(N) ) ; true )
            ;
              true
            )
          ).


unblockade( Squares ) :-
    forall( 
            member( N, Squares )
          ,
            retractall( blockaded(N) )
          ).

boardhtml.pl - the HTML generator

/*  boardhtml.pl  */
 

write_board :-
  max_x(MaxX),
  max_y(MaxY),
  make_table(MaxX,MaxY).


make_table( MaxX, MaxY ) :-
  write( '<TABLE>' ), nl,
  make_table_from( 1, MaxX, MaxY ),
  write( '</TABLE>' ), nl.


make_table_from( Y, MaxX, MaxY ) :-
  Y =< MaxY, !,
  Yn is Y + 1,
  make_table_from( Yn, MaxX, MaxY ),
  make_row( Y, MaxX ).
  % Write rows in reverse order so that square 1 is at the top.

make_table_from( _, _, _ ).


make_row( Y, MaxX ) :-
  write( '<TR>' ),
  make_row_from( 1, Y, MaxX ),
  write( '</TR>' ).


make_row_from( X, Y, MaxX ) :-
  X =< MaxX,
  make_cell( X, Y ),
  Xn is X + 1,
  make_row_from( Xn, Y, MaxX ).

make_row_from( _, _, _ ).


make_cell( X, Y ) :-
  square( N, X, Y ), !,
  make_used_cell( N, X, Y ).

make_cell( X, Y ) :-
  write( '<TD>&nbsp;</TD>' ).


make_used_cell( N, X, Y ) :-
    ( at( T, N ) ->
      write( '<TD BGCOLOR=YELLOW>' )
    ;
      write( '<TD BGCOLOR=#CCFFFF>' )
    ),
    write( '<FONT SIZE=-3>' ),
       ( building( N, Name ) ->
         write( Name ),    
         ( buys( Name, Good, Price ) ->
           write(' buys '), write(Good), write(' at '), write(Price) 
         ; 
           true
         ),
         ( sells( Name, Good, Price ) ->
           write(' sells '), write(Good), write(' at '), write(Price) 
         ;
           true
         ),
         ( sells_fuel( Name, Price ) ->
           write(' sells fuel at '), write(Price) 
         ;
           true
         )
       ;
         true
       ),
    write( '</FONT>' ),
    write( '</TD>' ).


max_x( X ) :-
  square( N, X, Y ),
  not(( square(_,X2,_), X2>X )). 


max_y( Y ) :-
  square( N, X, Y ),
  not(( square(_,_,Y2), Y2>Y )). 


route.pl - the routefinder

/*  route.pl  */


/*
SPECIFICATION
-------------

This is a route-finding module for the trading game. It uses best-first
search to find the best route from one square to another.


PUBLIC route( S1+, S2+, Route- ):

S1 and S2 are square numbers. Route will be instantiated to the shortest
route between the two squares; if they are not connected, the predicate
fails. It does not do any error checking, so illegal square numbers, etc
may have undefined results.
*/


/*
IMPLEMENTATION
--------------

The code is adapted from program 18.7 in "The Art of Prolog" by Sterling
and Shapiro. Changes are:

1) I have specialised the predicate to the trading game, so it assumes
it can call "adjacent" for instance instead of "move", and it doesn't call
"legal". Also the goal is now passed as an argument.

2) The evaluation function is distance squared, and frontiers are sorted
in ascending order of value (a change to the 2nd clause of insert), not
descending order.

3) update_frontier had a bug, by which if "member(State1,History)"
succeeded, its negation failed, thus causing the whole 1st clause to
fail. I have remedied this with the if.

4) Treatment of history was naff, in that the initial state is put into
the history list by route (S&S state that this is necessary), but it
gets put in again in the recursive call to solve_best. It had indeed to
go in so that update_frontier would work, but this is more neatly
handled by passing [S0|History] rather than History to it.
*/


route( S1, S2, Route ) :-
    distance_2( S1, S2, Value ),
    solve_best( [state(S1,[],Value)], S2, [], Route ).


solve_best( [state(Goal,Path,Value)|Frontier], Goal, History, Moves ) :-
    reverse( Path, Moves ),
    !.

solve_best( [state(S0,Path,Value)|Frontier], Goal, History, FinalPath ) :-
    findall( SNext, adjacent(S0,SNext), Nexts ),
    update_frontier( Nexts, S0, Goal, Path, [S0|History], Frontier, Frontier1 ),
    solve_best( Frontier1, Goal, [S0|History], FinalPath ).


update_frontier( [S1|RestSs], S0, Goal, Path, History, F, F1 ) :-
    distance_2( S1, Goal, Value ),
    (
        not( member(S1,History) )
    ->
        insert( state(S1,[S1|Path],Value), F, F0 ),
        update_frontier( RestSs, S0, Goal, Path, History, F0, F1 )
    ;
        update_frontier( RestSs, S0, Goal, Path, History, F, F1 )
    ).

update_frontier( [], S0, Goal, P, H, F, F ).


/*  insert( S+, States+, NewStates- ):
        Insert S into States giving NewStates, such that S is placed
        in correct order (ascending) of value.
*/
insert( S0, [], [S0] ) :- !.

insert( S0, [S1|Ss], [S0,S1|Ss] ) :-
    less_than( S0, S1 ),
    !.

insert( S0, [S1|Ss], [S0|Ss] ) :-
    equals( S1, S0 ),
    !.

insert( S0, [S1|Ss], [S1|Ss1] ) :-
    !,
    insert( S0, Ss, Ss1 ).


equals( state(S0,P,V), state(S0,P1,V) ).


less_than( state(S1,P1,V1), state(S2,P2,V2) ) :-
    S1 \= S2,
    V1 < V2.

board.pl - the board

/*  board.pl  */


/*  This is the board for the trading game. See traveller.pl for a
    description of the predicates.
*/

square(1, 2, 12).
square(2, 3, 13).
square(3, 4, 13).
square(4, 5, 13).
square(5, 6, 12).
square(6, 5, 11).
square(7, 4, 11).
square(8, 3, 11).
square(9, 2, 5).
square(10, 3, 6).
square(11, 4, 6).
square(12, 5, 6).
square(13, 6, 5).
square(14, 5, 4).
square(15, 4, 4).
square(16, 3, 4).
square(17, 8, 12).
square(18, 9, 13).
square(19, 10, 13).
square(20, 11, 13).
square(21, 12, 12).
square(22, 11, 11).
square(23, 10, 11).
square(24, 9, 11).
square(25, 9, 7).
square(26, 10, 8).
square(27, 11, 7).
square(28, 10, 6).
square(29, 4, 10).
square(30, 4, 9).
square(31, 4, 8).
square(32, 4, 7).
square(33, 7, 12).
square(34, 10, 10).
square(35, 10, 9).
square(36, 13, 12).
square(37, 14, 12).
square(38, 15, 11).
square(39, 15, 10).
square(40, 15, 9).
square(41, 15, 8).
square(42, 15, 7).
square(43, 15, 6).
square(44, 15, 5).
square(45, 15, 4).
square(46, 15, 3).
square(47, 14, 2).
square(48, 13, 2).
square(49, 12, 2).
square(50, 11, 2).
square(51, 10, 2).
square(52, 9, 2).
square(53, 8, 2).
square(54, 7, 2).
square(55, 6, 2).
square(56, 5, 2).
square(57, 4, 2).
square(58, 3, 2).
square(59, 2, 2).
square(60, 8, 7).
square(61, 7, 8).
square(62, 7, 9).
square(63, 12, 7).
square(64, 13, 6).
square(65, 13, 5).
square(66, 10, 5).
square(67, 9, 4).
square(68, 8, 4).

building(1, 'Fine Things').
building(2, 'Ride Rite').
building(3, 'L d F TV').
building(4, 'KrystalKlear').
building(5, 'Green\'s').
building(6, 'Pirana Petrol').
building(8, 'ClearView').
building(9, 'Argent\'s').
building(10, 'No Bull Please').
building(12, 'Greatfields').
building(13, 'Diamond Gallery').
building(14, 'The Glass House').
building(16, 'Good Taste').
building(18, 'Cookham\'s').
building(20, 'Burnham\'s').
building(22, 'Charrett\'s').
building(24, 'Yugo').
building(30, 'Coker\'s').
building(32, 'Major\'s').
building(33, 'Gifts \'n Glasses').
building(36, 'Scargill\'s').
building(40, 'Truckmate').
building(41, 'First Fruits').
building(43, 'I.G.Knight').
building(44, 'J L B TV').
building(45, 'Aurum').
building(48, 'A.R.L.').
building(50, 'Edison\'s').
building(51, 'Pippin\'s').
building(52, 'A.N.Ode').
building(53, 'Reflections').
building(54, 'B.T.U.Knight').
building(55, 'Slack\'s').
building(56, 'Burnett\'s').
building(57, 'Lucent').
building(58, 'Caprice').
building(59, 'Maxwell\'s').
building(60, 'Goldsmith\'s').
building(61, 'Wheelers').
building(63, 'Freds Fuel').
building(66, 'Collier\'s').

joins(1, 2).
joins(2, 3).
joins(3, 4).
joins(4, 5).
joins(5, 6).
joins(6, 7).
joins(7, 8).
joins(8, 1).
joins(9, 10).
joins(10, 11).
joins(11, 12).
joins(12, 13).
joins(13, 14).
joins(14, 15).
joins(15, 16).
joins(16, 9).
joins(17, 18).
joins(18, 19).
joins(19, 20).
joins(20, 21).
joins(21, 22).
joins(22, 23).
joins(23, 24).
joins(24, 17).
joins(25, 26).
joins(26, 27).
joins(27, 28).
joins(28, 25).
joins(7, 29).
joins(29, 30).
joins(30, 31).
joins(31, 32).
joins(32, 11).
joins(5, 33).
joins(33, 17).
joins(23, 34).
joins(34, 35).
joins(35, 26).
joins(21, 36).
joins(36, 37).
joins(37, 38).
joins(38, 39).
joins(39, 40).
joins(40, 41).
joins(41, 42).
joins(42, 43).
joins(43, 44).
joins(44, 45).
joins(45, 46).
joins(46, 47).
joins(47, 48).
joins(48, 49).
joins(49, 50).
joins(50, 51).
joins(51, 52).
joins(52, 53).
joins(53, 54).
joins(54, 55).
joins(55, 56).
joins(56, 57).
joins(57, 58).
joins(58, 59).
joins(26, 35).
joins(35, 34).
joins(34, 23).
joins(25, 60).
joins(60, 61).
joins(61, 62).
joins(27, 63).
joins(63, 64).
joins(64, 65).
joins(28, 66).
joins(66, 67).
joins(67, 68).

in(1, 'The Hub').
in(2, 'The Hub').
in(3, 'The Hub').
in(4, 'The Hub').
in(5, 'The Hub').
in(6, 'The Hub').
in(7, 'The Hub').
in(8, 'The Hub').
in(9, 'Beverly Hills').
in(10, 'Beverly Hills').
in(11, 'Beverly Hills').
in(12, 'Beverly Hills').
in(13, 'Beverly Hills').
in(14, 'Beverly Hills').
in(15, 'Beverly Hills').
in(16, 'Beverly Hills').
in(17, 'Colliery Row').
in(18, 'Colliery Row').
in(19, 'Colliery Row').
in(20, 'Colliery Row').
in(21, 'Colliery Row').
in(22, 'Colliery Row').
in(23, 'Colliery Row').
in(24, 'Colliery Row').
in(25, 'The Plain').
in(26, 'The Plain').
in(27, 'The Plain').
in(28, 'The Plain').

loop('The Hub').
loop('Beverly Hills').
loop('Colliery Row').
loop('The Plain').

sells_fuel('Freds Fuel', 32).
sells_fuel('Wheelers', 30).
sells_fuel('Truckmate', 40.59).
sells_fuel('Yugo', 35).
sells_fuel('Pirana Petrol', 80.03).
sells_fuel('Ride Rite', 50).

buys('Caprice', diamonds, 1600).
buys('Burnett\'s', coal, 30.0).
buys('Slack\'s', coal, 21.57).
buys('B.T.U.Knight', coal, 19).
buys('Reflections', glasses, 58).
buys('Pippin\'s', peaches, 9.5).
buys('Gifts \'n Glasses', glasses, 60.09).
buys('Major\'s', coal, 24.99).
buys('Charrett\'s', coal, 20.39).
buys('Good Taste', peaches, 8.03).
buys('The Glass House', glasses, 57.0).
buys('Diamond Gallery', diamonds, 1500).
buys('ClearView', televisions, 111.13).
buys('KrystalKlear', glasses, 55.97).

sells('Collier\'s', coal, 8.4).
sells('Goldsmith\'s', diamonds, 350).
sells('Maxwell\'s', televisions, 90.09).
sells('Lucent', glasses, 47.5).
sells('A.N.Ode', televisions, 92.5).
sells('Edison\'s', televisions, 93.49).
sells('A.R.L.', televisions, 95.0).
sells('Aurum', diamonds, 390).
sells('J L B TV', televisions, 98.74).
sells('I.G.Knight', coal, 5.09).
sells('First Fruits', peaches, 4.23).
sells('Scargill\'s', coal, 5.09).
sells('Coker\'s', coal, 8.01).
sells('Burnham\'s', coal, 9.59).
sells('Cookham\'s', coal, 13.79).
sells('Greatfields', peaches, 4.23).
sells('No Bull Please', glasses, 47.99).
sells('Argent\'s', diamonds, 400).
sells('Green\'s', peaches, 5.01).
sells('L d F TV', televisions, 100.39).
sells('Fine Things', glasses, 50.37).

unit_volume(coal, 50).
unit_volume(diamonds, 0.5).
unit_volume(glasses, 10).
unit_volume(peaches, 2).
unit_volume(televisions, 10).

trader.pl - the trader

/*  trader.pl  */



act( trader, Act, Arg1, Arg2 ) :-
    at( trader, Here ),
    in( Here, 'The Hub' ),
    hub_act( trader, Act, Arg1, Arg2 ).

act( trader, Act, Arg1, Arg2 ) :-
    on_way_to_hub_act( trader, Act, Arg1, Arg2 ).


on_way_to_hub_act( trader, buy, fuel, Qty ) :-
    at_fuel_station( trader, Price ),
    fuel_needed( trader, Qty ),
    Qty >= 1.

on_way_to_hub_act( trader, move, Next, dummy ) :-
    at( trader, Here ),
    nearest_gateway( Here, 'The Hub', HubGate ),
    route( Here, HubGate, Route ),
    Route = [ Next | _ ].


hub_act( trader, buy, fuel, Qty ) :-
    at_fuel_station( trader, Price ),
    Price < 60,
    fuel_needed( trader, Qty ),
    Qty >= 1.

hub_act( trader, buy, glasses, Qty ) :-
    at_seller( trader, glasses, Price ),
    quantity_purchasable( trader, glasses, Price, Qty ),
    Qty >= 1.

hub_act( trader, sell, glasses, Qty ) :-
    at_buyer( trader, glasses, Price ),
    carries( trader, glasses, Qty ),
    Qty >= 1.

hub_act( trader, move, Next, dummy ) :-
    at( trader, Here ),
    clockwise( Here, Next ).


at_seller( T, Good, Price ) :-
    at( T, Square ),
    square_sells( Square, Good, Price ).


at_fuel_station( T, Price ) :-
    at( T, Square ),
    square_sells_fuel( Square, Price ).


at_buyer( T, Good, Price ) :-
    at( T, Square ),
    square_buys( Square, Good, Price ).


square_sells( Square, Good, Price ) :-
    building( Square, Shop ),
    sells( Shop, Good, Price ).


square_sells_fuel( Square, Price ) :-
    building( Square, Shop ),
    sells_fuel( Shop, Price ).


square_buys( Square, Good, Price ) :-
    building( Square, Shop ),
    buys( Shop, Good, Price ).


cheapest_seller_in_region( Good, Region, Square ) :-
    in( Square, Region ),
    square_sells( Square, Good, Price ),
    not((
            in( Other, Region ),
            Other \= Square,
            square_sells( Other, Good, OtherPrice ),
            OtherPrice < Price
       )).


dearest_buyer_in_region( Good, Region, Square ) :-
    in( Square, Region ),
    square_buys( Square, Good, Price ),
    not((
            in( Other, Region ),
            Other \= Square,
            square_buys( Other, Good, OtherPrice ),
            OtherPrice > Price
       )).


fuel_needed( T, Qty ) :-
    fuel( T, F ),
    tank_size( T, TS ),
    Qty is TS - F.


 quantity_purchasable( T, Good, Price, Qty ) :-
    cash( T, CashNow ),
    FuelSafeCash is CashNow - 8*80,
    QtyAffordable is FuelSafeCash / Price,
    unit_volume( Good, UnitVolume ),
    Volume is QtyAffordable*UnitVolume,
    total_load( T, TotalLoad ),
    max_load( T, Capacity ),
    CapacityLeft is Capacity-TotalLoad,
    least( CapacityLeft, Volume, VolumeCarriable ),
    Qty is VolumeCarriable/UnitVolume.


least( A, B, A ) :-
    A < B.

least( A, B, B ) :-
    A >= B.


nearest_gateway( From, Region, Gateway ) :-
    gateway( Region, Gateway ),
    distance( Gateway, From, D ),
    not((
            gateway( Region, Other ),
            Other \= Gateway,
            distance( Other, From, D1 ),
            D1 < D
       )).


gateway( Region, Square ) :-
    in( Square, Region ),
    adjacent( Square, Other ),
    not( in(Other,Region) ).

1st November 2008