The basic predicates are square
, building
,
joins
, in
, loop
, sells_fuel
,
buys
and sells
, plus three ones derived from them: next
,
clockwise
and distance
.
Together, these describe a simple street layout which you can view here. It has squares containing shops and fuel stations. Unlike Monopoly, the squares are not confined to the outside of the board, but can form streets, some looped back on themselves, wandering over its interior.
The design of the game is discussed here.
More specifically, the predicates have the meanings given below:
square(S,X,Y)
:
S is the number of a square, lying at co-ordinate X,Y.
Each square has a unique number; these are integers (whole
numbers), and work upwards from 1. Adjacent squares do not necessarily
have adjacent numbers.
building(S,B)
:
B is the name of the building in square number S.
joins(S1,S2)
: whereas square
defines where squares lie and what they
contain, joins
defines how they are connected. There is one clause,
joins(S1,S2)
for every pair of connected squares. To make things
simple, squares can only be connected by vertical, horizontal, or
45-degree diagonal lines.
in(S,L)
:
The board contains several streets that curve back on
themselves, forming closed loops.
Each loop has a name (you will see why
later). There is one clause, in(S,L)
for every square S that lies in a
loop called L. You can think of the loops as being distinct regions of a
city, such as the City in London or the central part of Oxford, except
that to keep things simple, they do not contain any roads apart from the
loop itself.
loop(L)
:
This defines the names of the loops. There is one
clause for each loop.
next(S1,S2)
:
This is true if S1 joins S2. Note that it is
defined in terms of joins
:
S1 is next to S2 if S1 joins S2 or S2 joins
S1.
The import of this is that if you want to find out about connected
squares, you should use next
, not joins
.
clockwise(S1,S2)
:
This is true if square S1 and S2 are both in
the same loop,
and S2 is immediately next to, and clockwise of, S1. Its
purpose is to give you a simple way of finding your way round loops. It
is defined in terms of joins
,
and only works because I took care, when
generating the board definition, to make sure that the
joins
facts
always had their arguments in an order which would make it work.
distance(S1,S2,D)
,
This sets D to the distance between S1 and
S2. It uses is
, an operator for doing arithmetic.
The formula is the standard one for working out distance in
terms of X
and Y co-ordinates: X^2
means X squared, and sqrt(...)
means the
square root of.
If you care to look at the diagram of the board, it should become clear what these facts say.
There are three kinds of building: wholesalers, retailers, and fuel-stations. Each retailer sells one (and one only) of five types of good to the public: coal, diamonds, glasses (tableware, not eyewear), peaches, and televisions. The retailers buy their stocks from traders, who in turn buy from wholesalers. By buying cheap at a wholesaler and selling dear to a retailer, a smart trader can make a hefty profit. Wholesalers, retailers, and fuel-stations are described by the facts below:
sells(B,Good,Price)
: B names a wholesaler, and will be
one of the names given as the first argument of building
.
Good is the
type of product sold. Like retailers, each wholesaler deals
in only one kind of product. Price is the price at which the wholesaler
sells one unit of the product to a trader (i.e. the price at which the
trader buys it).
buys(B,Good,Price)
:
B names a retailer. As before, Good
is the product type. This time, Price is the price at which
the retailer buys from the trader, i.e. the price at which the trader
sells it.
sells_fuel(B,Price)
:
This tells you which buildings are fuel
stations. Price is the price per unit of fuel, and B is the name of the
building.
Before showing you how to program a trader, I need to say some more about prices. Goods are bought and sold in ``units''. So a trader may tell Prolog that it wants to buy 20 units of coal, or to sell 100 units of diamonds. To make Traveller more realistic, you can think of these units as being sacks (of coal); individual boxes (for diamonds and televisions); boxes of six (glasses); boxes of twenty-four (peaches). However, it doesn't matter how many things are inside; you can only buy in whole units.
The buying and selling prices given by buys
and sells
are all quoted
as the price per unit.
Products have volumes, measured in cubic feet. These are given by the
only fact I haven't yet mentioned:
unit_volume(Good,V)
.
V is the
volume occupied by one unit of Good (all goods of the same type have
the same unit volume - containerisation). This matters because as a
trader, you drive a lorry whose size is strictly limited. It has a
capacity of 1000 cubic feet. You can always fill it up to this amount,
provided you have the money, but you cannot cram in any more above the limit.
When you start off, you are given £ 5000 in cash, to spend as you will. There is no upper limit on your cash - you can accumulate indefinitely - but you are not allowed to go below zero. There are no credit facilities in this game.
Your lorry has a fuel tank whose capacity is twenty units. When you start off, it is full. At each stage in the game, you are allowed to do one of four things:
To buy or sell, you must be on the same square as the person you're dealing with. You can only buy (fuel or tradeable goods) if you have enough money; otherwise you will be thrown out of the game for fraud. If you try to buy more fuel than you have room for in your tank, you will certainly spend the money; your tank will be filled, and any excess will just be spilt. However, if you try to buy more of a good than you have room for, the transaction will be forbidden for safety reasons, and you will be kicked out as an unsafe driver.
You can only sell a good if you have enough of it in your lorry. If you haven't, you will again be thrown out for fraud.
Each trader begins with a full tank. Initial cash is
£ 5000, and initial loads are all zero. The total load is
displayed in
cubic feet, and the stock of each good is displayed both in units and in
cubic feet, using unit_volume
to provide conversion factors.
trader is on square 67. Fuel 20 in tank size 20. Cash 5000. Total load 0 cu ft in lorry size 1000. Stock of televisions = 0 units (0 cu ft). Stock of peaches = 0 units (0 cu ft). Stock of glasses = 0 units (0 cu ft). Stock of diamonds = 0 units (0.0 cu ft). Stock of coal = 0 units (0 cu ft).
As the game continues, you will see a summary of the actions the trader takes at each turn, and the result on his fuel, cash, position, and stocks. Wait until the trader makes his way to The Hub, and has gone through a few cycles of buying and selling; when you have seen enough, you can interrupt the game.
Now, how can you write a trader of your own? Well, when
you call run
, it
starts by asserting some clauses which describe the trader's
state. These are:
at(T,Square)
:
T is the name of the trader, and Square is the
number of the square he is on. This always starts off as the second
argument to run
.
carries(T,Good,Qty)
:
T is the name of the trader, Good is the name
of one of his goods (coal
, diamonds
, glasses
, peaches
or
televisions
)
and Qty is the number of units he has. This predicate was
originally called load
, but I have changed it to avoid clashes with the
load
command.
cash(T,Cash)
:
T is the name of the trader, and Cash is the
amount of cash he has.
fuel(T,Qty)
:
T is the name of the trader, and Qty is the number
of units of fuel he has.
max_load(T,Vol)
:
T is the name of the trader, and Vol is the
maximum volume his lorry can hold. In this version of Traveller, it is
1000 cubic feet.
tank_size(T,Qty)
:
T is the name of the trader, and Qty is
the maximum number of units he can get into his tank. In this version of
Traveller, it's 20.
total_load(T,Vol)
:
T is the name of the trader, and Vol is
the total load in cubic feet.
The name T of the trader is the first argument to
run
. So for run(trader,67)
, you get clauses
at(trader,67). cash(trader,5000). fuel(trader,20). carries(trader,coal,0). carries(trader,diamonds,0). carries(trader,glasses,0). carries(trader,peaches,0). carries(trader,televisions,0). max_load(trader,1000). tank_size(trader,20). total_load(trader,0).Note that, apart from
carries
, there is only one clause of each kind.
Having done this, run
then tries to find out what the trader's first action is to be. Will he
move to a new square, buy something, or sell something? It
does so by calling (i.e. by asking itself the question)
act( T, Action, Arg1, Arg2 )
where T is the name of the trader. The file TRADER contained clauses
I
have written for act
: to play Traveller, you must write some for
your trader.
When act
is called,
it must set Action to one of move
, buy
, or sell
. If Action is
move
, Arg1 must become the next square to move to, and Arg2 must
become the atom dummy
. If Action is buy
or sell
, Arg1 must become
the name of a good, and Arg2 must become the quantity in units that is
to be bought or sold. Finally, if Action is buy
, Arg1 can also be
fuel
. In this case, the trader is buying fuel, not a good for resale.
1st November 2008.