!--------------------------------------------------------------------------
!                            THE MAGIC TOYSHOP
!                    Copyright (c) 1995 by Gareth Rees
!                               July 1995
!--------------------------------------------------------------------------
! INTRODUCTION
!
! This was an entry for the 1995 short interactive fiction contest, though
! not really a serious piece of fiction.  It is instead a gratuitous
! sequence of puzzles purely for puzzles' sake.  Some of the puzzles
! pastiche famous puzzles from other adventure games: the gnomon and
! sundial from Trinity; the robot mouse and the featureless mahogany rod
! from Curses; the egg from the Unnkulia series.  Others are well-known
! games and puzzles with silly twists (towers of Hanoi, noughts and
! crosses).  Still others are games that were fun to code up (mostly highly
! derivative).
!
! As of July 1995 my electronic mail address was <[email protected]>, but
! if you have trouble getting hold of me, try the Usenet newsgroups
! rec.arts.int-fiction and rec.games.int-fiction.
!--------------------------------------------------------------------------
! LICENCE
!
! This program is free software; you can redistribute it and/or modify it
! under the terms of the GNU General Public License as published by the
! Free Software Foundation; either version 2 of the License, or (at your
! option) any later version.
!
! This program is distributed in the hope that it will be useful, but
! without any warranty; without even the implied warranty of merchant-
! ability or fitness for a particular purpose.  See the GNU General Public
! License for more details.
!--------------------------------------------------------------------------
! CONTENTS
!
! 1.1 About the game
! 1.2 Attributes, properties, actions and grammar
! 1.3 Asking questions
! 1.4 Entry points
! 1.5 Plot & scoring system
! 1.6 The toyshop
!
! 2.1 Making the robot mouse
! 2.2 Noughts and crosses
! 2.3 The gnomon
! 2.4 The towers of Hanoi
! 2.5 Dots and boxes
! 2.6 The robot mouse
! 2.7 Tea-time
! 2.8 Dodgems
! 2.9 The infernal machine
!--------------------------------------------------------------------------

Constant Story "THE MAGIC TOYSHOP";
Constant Headline "^A fun game for all the family^by Gareth Rees^New \
   players should type ~help~^";
Replace FullScoreSub;
Constant MAX_SCORE 20;
Constant TASKS_PROVIDED;

Include "parser";
[ FullScoreSub; MyFullScore(); ];
Include "verblib";
Include "grammar";


!--------------------------------------------------------------------------
! 1.1 ABOUT THE GAME
!--------------------------------------------------------------------------

[ HelpSub;
   "~The Magic Toyshop~ is an entry in the 1995 interactive fiction \
   programming competition. It isn't a serious story by any means, but \
   you might have fun playing with some of the gadgets herein. Thanks to \
   Michael Kinyon for finding bugs and offering suggestions.^^~The Magic \
   Toyshop~ is copyright (c) 1995 by Gareth Rees, and may be freely \
   distributed and used under the terms of version 2 of the GNU General \
   Public Licence or, at your option, any later version.^^Some of the \
   ASCII graphics can be turned off using the command ~plain~ and on \
   again using ~pretty~. There are hints provided in the game.";
];

Verb meta "help" * -> Help;


!--------------------------------------------------------------------------
! 1.2 ATTRIBUTES, PROPERTIES, ACTIONS AND GRAMMAR
!--------------------------------------------------------------------------

Attribute is_disk;     ! identifies disks for the Towers of Hanoi game
Attribute is_peg;      ! identifies pegs on the Towers of Hanoi board
Attribute sticky;      ! if an object has had glue added to it

Property next;         ! next object in a linked list
Property stuck_to;     ! which other disk a disk is stuck to
Property state;        ! general state property
Property prev_num;     ! previous number property (for Hanoi board)
Property puzzle_state; ! 0 unseen; 1 seen; 2 attempted; 3 solved
Property puzzle_pre;   ! prerequisites for the puzzle to be available
Property puzzle_name;  ! name of a puzzle (for fullscore command)

Fake_Action Reset;     ! reset a puzzle to its initial state
Fake_Action Invoke;    ! start up a new puzzle
Fake_Action Display;   ! print a display

Global pretty = 1;     ! 1 iff ASCII graphics are used by preference
Global help = 0;       ! 1 iff game boards print help information

[ PlainSub; pretty = 0; "Special effects turned off."; ];
[ PrettySub; pretty = 1; "Special effects turned on."; ];
[ HelpOnSub; help = 1; "Help turned on."; ];
[ HelpOffSub; help = 0; "Help turned off."; ];

Verb meta "plain" * -> Plain;
Verb meta "pretty" * -> Pretty;
Extend "help"
   * "on" -> HelpOn
   * "off" -> HelpOff;


!--------------------------------------------------------------------------
! 1.3 ASKING QUESTIONS
!--------------------------------------------------------------------------
! Here are some replacements for the various conversational grammar entries
! that allow conversation topics to be parsed as objects within some scope
! (namely the AskQuestions object); thus "ask catharine about noughts and
! crosses" will work.  Topics representing puzzles are moved into this
! scope when they are seen for the first time, so that clues aren't
! available before the puzzle has been encountered.
!
! This approach is somewhat tricky to make correct in general; here, it's
! ok for "ask catharine about and" to reply "Which do you mean, the noughts
! and crosses, or the dots and boxes?", but in a less frivolous game
! identification of conversational subjects in this way might be frowned
! upon.  There may be problems if there are several people to talk to; thus
! "say noughts" might produce "Who do you want to say the noughts and
! crosses to?" whereas "say aardvark" would produce "Who do you want to say
! that to?" (if there were no topic for aardvarks).  You could give the
! noughts and crosses topic the name "that", so that this error message
! wouldn't reveal the valid topics, but then you'd have to make sure that
! no word appeared in two topics, otherwise you'd get error messages of the
! form "Which do you mean, that or that?".
!
! See my example game "Encyclopedia Frobozzica" (look in the /programming/
! inform/examples/ directory at the IF-archive) for more details about
! talking to characters in Inform games.
!--------------------------------------------------------------------------

[ AskScope;
   if (scope_stage == 1) rfalse;
   if (scope_stage == 2) {
       ScopeWithin(AskQuestions);
       rtrue;
   }
   "** Error: that input should have matched a later line in grammar **";
];

[ QuestionSub; if (RunLife(noun,##Ask)~=0) rfalse; "No reply."; ];
[ RQuestionSub; <<Question second noun>>; ];
[ NoQuestionSub; <<Question noun 0>>; ];

[ ConTopicPrep prep w; consult_from = wn;
 do w=NextWordStopped(); until (w==prep or -1); if (w==-1) return -1;
 wn--; consult_words = wn-consult_from;
 if (consult_words==0) return -1; return 0; ];
[ ConTopicTo; return ConTopicPrep('to'); ];
[ ConTopicAt; return ConTopicPrep('at'); ];

Extend "ask" replace
   * creature "about" scope=AskScope   -> Question
   * creature "for" scope=AskScope     -> Question
   * creature scope=AskScope           -> Question
   * creature ConTopic                 -> NoQuestion;

Extend "say" replace
   * scope=AskScope "to" creature      -> RQuestion
   * scope=AskScope "at" creature      -> RQuestion
   * ConTopicTo "to" creature          -> NoQuestion
   * ConTopicAt "at" creature          -> NoQuestion
   * ConTopic "to" creature            -> NoQuestion;

Extend "tell" replace
   * creature "about" scope=AskScope   -> Question
   * creature "about" ConTopic         -> NoQuestion
   * creature scope=AskScope           -> Question
   * creature ConTopic                 -> NoQuestion;

Object  AskQuestions "questions";

Nearby  QHello "that" has proper
with   name "hello" "good" "morning" "afternoon" "day" "hi";
Nearby  QToyshop "that" has proper
with   name "toy" "shop" "toyshop" "store";
Nearby  QExit "that" has proper
with   name "exit" "way" "out" "door" "help";
Nearby  QNiece "that" has proper
with   name "niece" "isabelle" "present" "gift" "birthday";


!--------------------------------------------------------------------------
! 1.4 ENTRY POINTS
!--------------------------------------------------------------------------

[ Initialise;
   location = Toyshop;
   StartDaemon(Toyshop);

   "^^You were looking for a birthday present for your niece Isabelle, \
   that was it. The toy stores along Regent Street were blaring out pop \
   music and the window displays were garish scenes of animated computer \
   violence. But down a dim Victorian arcade you came across a different \
   kind of toyshop, with a peeling rocking-horse behind a grimy \
   window. Perhaps in here, you thought...^^";
];


!--------------------------------------------------------------------------
! 1.5 PLOT & SCORING SYSTEM
!--------------------------------------------------------------------------
! Each puzzle is associated with an object by the `Puzzles' array.  The
! state of the puzzle can be
!
!   0 puzzle not yet seen
!   1 puzzle has been seen but not really attempted yet
!   2 player attempted the puzzle and encountered the difficulty
!   3 puzzle has been solved
!
! The distinction between 1 and 2 is so that Catharine doesn't give away
! hints until the player has at least had a go.
!
! Each puzzle can have prerequisites, that is, puzzles which need to be
! solved before this one can be seen: for example, the dots and boxes
! puzzle can't be seen until the noughts and crosses puzzle has been solved
! (to prevent there being confusion over the two pieces of paper).
!
! The `NewPuzzle' function picks a new puzzle, but also prevents there
! being too many puzzles available at once.  The puzzles appear in the
! order they are given in the `Puzzles' array, subject to preconditions
! being satisfied.
!
! The graph of puzzles follows (a puzzle depends for its solution on the
! puzles above it).  Some of the connections are enforced by the
! proconditions to ensure that there isn't too much happening at once; some
! other connections are enforced by objects needing to be present that
! result from the solution of previous puzzles.  The dotted connections
! show that a puzzle only needs to be present, not solved, in order to
! solve another.
!
!                             Start
!                 ._____________|___________.
!                 |                         |
!             Assembling                 Noughts
!             the mouse                 & crosses
!         ._______|_____________________.   |
!         |       |          |          |   |
!         |       |        Towers       |   |
!         |       |       of Hanoi      |   |
!         |       |  . . . . |          |   |
!         |       |  |                  |   |
!         |     Gnomon &                Dots &
!         |     sundial                 boxes
!         |       |__________________.    |
!         |       |                  |    |
!         |      Egg                 Dodgems
!         |   . . |
!         |   |
!         Robot
!         mouse
!           |
!           |
!         Chest
!
! It will be seen that it isn't necessary to solve the Egg, the Towers of
! Hanoi, Dodgems or Dots and Boxes to win.  It is necessary to solve
! Noughts and Crosses, because of constraints on which puzzles are present.
!--------------------------------------------------------------------------

Constant NPUZZLES 9;
Array Puzzles --> CardboardBox OXPaper Gnomon RobotMouse DBPaper HanoiBoard
   DodgemsPaper Hamper PuzzleChest;

[ NewPuzzle
   reply  ! 1 iff Catharine should comment (INPUT)
   a      ! count of puzzles in progress
   b      ! count of available puzzles
   c      ! first available puzzle to be found
   i j;   ! loop counters

   ! count the puzzle in progress and available; pick first available
   for (: i < NPUZZLES: i++) {
       if ((Puzzles-->i).puzzle_state == 1 or 2) a++;
       if ((Puzzles-->i).puzzle_state == 0) {
           for (j = 0: j < ((Puzzles-->i).#puzzle_pre)/2: j++)
               if ((((Puzzles-->i).&puzzle_pre)-->j).puzzle_state ~= 3)
                   jump NextPuzzle;
           b ++;
           if (b == 1) c = i;
           .NextPuzzle;
       }
   }
   if (reply == 1) {
       if (a >= 3)
           print_ret "~I think that ", (EnglishNumber) a, " puzzles on \
               the go at once is plenty,~ says Catharine.";
       if (b == 0)
           "~I'm right out of puzzles at the moment,~ says Catharine.";
   }
   if (a >= 3 || b == 0) rtrue;
   <<Invoke Puzzles-->c>>;
];

[ MyFullScore
   a   ! count of solved puzzles
   b   ! count of puzzle in progress
   i;  ! loop counter

   for (i = 0: i < NPUZZLES: i++) {
       switch ((Puzzles-->i).puzzle_state) {
        1,2: b ++;
        3: a ++;
       }
   }

   ScoreSub();
   if (score == 0) rfalse;

   if (a > 0) {
       print "^You ";
       if (deadflag == 0) print "have ";
       print "solved ", (EnglishNumber) a, " puzzle";
       if (a > 1) print "s";
       print ":^^";
       for (i = 0: i < NPUZZLES: i++)
           if ((Puzzles-->i).puzzle_state == 3)
               print "    ", (string) (Puzzles-->i).puzzle_name, "^";
   }
   if (b > 0) {
       print "^You ";
       if (deadflag == 0) print "are";
       else print "were";
       print " still working on ", (EnglishNumber) b, " puzzle";
       if (b > 1) print "s";
       print ":^^";
       for (i = 0: i < NPUZZLES: i++)
           if ((Puzzles-->i).puzzle_state == 1 or 2)
               print "    ", (string) (Puzzles-->i).puzzle_name, "^";
   }
];


!--------------------------------------------------------------------------
! 1.6 THE TOYSHOP
!--------------------------------------------------------------------------

Object  Toyshop "Toyshop"
has    light
with   name "toyshop" "emporium" "shop" "store",
       description "Who knows what might be hidden in the dark rafters \
           and shadowy corners of this emporium?",
       state 0,
       daemon [;
           self.state = self.state + 1;
           switch (self.state) {
            1: give Catharine proper;
               "^~Welcome,~ says the young woman, ~My father usually \
               runs the store, but I'm afraid he isn't here today and \
               I'm looking after the store. My name is Catharine.~";
            2: NewPuzzle();
            9: self.state = 1;
           }
       ],
       before [;
        Exit: <<Go out_obj>>;
       ],
       cant_go "You turn to leave, but alarmingly you are unable to find \
           a way out.";

Nearby  ToyChest "oak chest"
has    container openable lockable locked static
with   name "oak" "chest" "oaken",
       article "an",
       describe [;
           if (self has open) print "^An oak chest stands open";
           else print "^There's a closed oak chest";
           print_ret " in the ", (DirectionName) self.state, " corner.";
       ],
       before [;
        Unlock:
           if (second ~= BrassKey) rfalse;
           <<Invoke PuzzleChest>>;
       ],
       state ne_to,
       each_turn [;
           if (random(5) ~= 1) rtrue;
           switch(random(4)) {
            1: self.state = ne_to;
            2: self.state = nw_to;
            3: self.state = se_to;
            4: self.state = sw_to;
           }
       ];

Nearby  Catharine "Catharine"
has    animate female transparent
with   name "catharine" "girl" "woman" "hair" "pigtail" "young" "catherine",
       describe [;
           if (self has proper) print "^Catharine ";
           else print "^A young woman ";
           switch (self.state) {
            0: "stands attentively nearby.";
            1: "sits on the floor nearby.";
           }
       ],
       short_name [;
           if (self has proper) print "Catharine";
           else print "young woman";
           rtrue;
       ],
       description "She is a young woman, in her late teens perhaps. She \
           is wearing a white crinoline dress with a hoop skirt, and her \
           long black hair is tied into a pigtail.",
       state 0,
       life [;
        Kiss: "~You are a sweet thing,~ she says.";
        Attack:
           deadflag = 1;
           "You punch her in the face. She screams and falls to the \
           floor. One hand shielding her cheek, she looks up with \
           frightened eyes at you towering over her. ~I thought -~ she \
           starts, and then you want no more of this, and turn to \
           flee. Your foot catches on the open chest, which wasn't at \
           all where you were expecting it to be, and you tumble into \
           it. The lid shuts with a reassuringly final thud.";
        Ask:
           if (second == 0 || self hasnt proper)
               print_ret (The) self, " doesn't reply.";
           if (second.next ~= 0) {
               if (second.next.puzzle_state == 1)
                   "~But you haven't even tried to solve that puzzle on \
                   your own!~ exclaims Catharine. ~I think you ought to \
                   have a go first, and then if you get stuck, maybe I \
                   can help you.~";
               if (second.next.puzzle_state == 3)
                   "~You've already solved that,~ says Catharine. ~I \
                   don't think you need any help from me.~";
               if (second.state >= (second.#description)/2)
                   "~I don't think I should give you any more help with \
                   that,~ says Catharine. ~If I gave the game away, it \
                   would only spoil your fun!~";
               print (string) ((second.&description)-->(second.state));
               second.state = second.state + 1;
               new_line;
               rtrue;
           }
           switch (second) {
            QHello: "~Good afternoon,~ says Catharine.";
            QExit: "~Looking to leave, already?~ says Catharine. ~But \
               you only just arrived!~";
            QNiece: "~I'm sure that we have just the thing for your \
               niece,~ says Catharine.";
            QToyshop: "~A lovely shop, isn't it?~ says Catharine. ~My \
               father and I, we do feel under pressure to modernise, but \
               we're resisting as much as we can. We feel that the best \
               puzzles and games are the ones you have to work at; ones \
               which don't give instant gratification. Yes, I know \
               that's a bit patronising, but there you are.~";
           }
       ],
       before [;
        Shake: "~Pleased to meet you,~ says Catharine, shaking your \
            hand.";
       ];

Object  Dress "crinoline dress" Catharine
has    concealed worn
with   name "crinoline" "dress" "hoop" "skirt",
       before [;
        Take,Remove: print_ret "That seems to belong to ", (the)
           Catharine, ".";
        ! Added after testing. Poor Catharine, to suffer this ignominy:
        LookUnder: "You get down on hands and knees to attempt this \
           task, when Catharine stamps on your hand and you yelp in \
           pain. ~I'm sorry,~ she says. ~What are you doing down there? \
           Have you lost your cuff-link?~ You stand up, shame-faced.";
       ];


!--------------------------------------------------------------------------
! 2.1 MAKING THE ROBOT MOUSE
!--------------------------------------------------------------------------
! This is just here to provide a decent excuse for the glue to be present
! (because it is needed for the Towers of Hanoi puzzle).
!--------------------------------------------------------------------------

Object  BoxQuestion "Airfix model"
with   name "airfix" "model" "card" "cardboard" "box" "glue" "gadget" "wheel",
       next CardboardBox,
       state 0,
       description "~It's a little trifle, I admit,~ says Catharine, \
           ~but you might like it, and so might your niece. I don't want \
           to tell you what it is until you've made it, as that would \
           spoil the surprise~."
           "~I did say it would need some assembly,~ says \
           Catharine. ~Doesn't glue usually come with these models? I \
           wonder where it's got to?~";

Object  CardboardBox "cardboard box"
has    container openable
       ! general if found the glue
with   name "card" "cardboard" "box",
       puzzle_name "The Airfix model",
       puzzle_state 0,
       capacity 3,
       description "The box is very faded, perhaps as a result of lying \
           unpurchased on a shelf for too many years. You can make out \
           the words ~Airfix~ and ~voice activated~, and a picture of \
           something that might be a model car.",
       invent [;
           if (inventory_stage == 2) {
               if (self has open) print " (which is open)";
               else print " (which is closed)";
               rtrue;
           }
       ],
       before [;
        Examine: PrintOrRun(self,description); rtrue;
        Empty:
           if (self notin player)
               "You should try picking it up first.";
        Search:
           if (self has open)
               "You can't make out much inside the box. Maybe you could \
               just try to empty it.";
        Shake:
           if (children(self) > 0)
               "Something rattles inside the box.";
           if (self hasnt general) {
               give self general;
               move Glue to self;
               "You give the box a vigourous shake, and you hear a \
               satisfactory rattle.";
           }
        Invoke:
           move self to player;
           move BoxQuestion to AskQuestions;
           self.puzzle_state = 1;
           score ++;
           "^Catharine opens the chest and roots around inside it. ~I \
           wonder if your niece would like something like this?~ she \
           says. ~It's quite old, but I think it still works.~ She \
           closes the chest and shows you a small cardboard box. She \
           holds it up to her ear and shakes it. ~Sounds like it's all \
           there. It does require some assembly, though,~ she adds, and \
           hands the box to you.";
       ],
       after [;
        Open: print_ret "You open the cardboard box.";
       ];

Class   MousePartClass
with   parse_name [ w n ok i len;
           do {
               ok = 0;
               w = NextWord();
               if (w == 'gadgets') {
                   ok = 1;
                   parser_action=##PluralFound;
               } else {
                   len = (self.#name)/2;
                   for (i = 0: ok == 0 && i < len: i++)
                       if ((self.&name)-->i == w)
                           ok = 1;
               }
               if (ok == 1) n++;
           } until (ok == 0);
           return n;
       ],
       before [;
        PutOn,Tie,Insert:
           if (second ~= self.next) rfalse;
           if (self hasnt sticky && second hasnt sticky)
               "You bring the two gadgets together, and after a bit of \
               fiddling you make them fit, but without you to hold them \
               together they immediately fall apart again.";
           remove MousePartA;
           remove MousePartB;
           CardboardBox.puzzle_state = 3;
           score ++;
           print "You bring the two gadgets together, and they \
               adhere. The resulting object looks not at all like a car, \
               and quite a lot like a mouse.^";
           <<Invoke RobotMouse>>;
       ],
       each_turn [;
           if (self in CardboardBox) rfalse;
           self.each_turn = NULL;
           CardboardBox.puzzle_state = 2;
       ];

Nearby  MousePartA "grey gadget"
class  MousePartClass
with   name "grey" "gray" "gadget",
       description "It's a piece of metal, covered on one side with what \
           looks like a piece of grey carpet. Who knows what it could be \
           for?",
       next MousePartB;

Nearby  MousePartB "wheeled gadget"
class  MousePartClass
with   name "wheel" "wheeled" "gadget" "wheels" "rubber",
       description "It's a piece of metal with four rubber wheels. Who \
           knows what it could be for?",
       next MousePartA;

Object  Glue "tube of glue"
       ! general if seen the instructions
with   name "tube" "of" "glue",
       description "A little tube with a nozzle on one end. Writing \
           along the side reads ~Super Safe Wonder Glue! Sticks only \
           metal. Completely non-toxic and safe for all other \
           materials.~",
       before [;
        Squeeze,PutOn:
           if (second == 0)
               "[To apply the glue to an object, use ~squeeze glue onto \
               thing~.]";
           if (second == Catharine)
               "~Keep that stuff off me!~ says Catharine. ~I know it's \
               supposed to be non-toxic, but you never know with these \
               things, and I'd rather be safe than sorry.~";
           if (second hasnt is_disk && second ~= MousePartA or MousePartB
               or Gnomon)
               print_ret "You squeeze some glue onto ", (the) second, " \
                   and in seconds it evaporates. It looks like the \
                   description of the amazing properties of the glue was \
                   accurate.";
           if (second has is_disk && children(second) > 0)
               print_ret "There would be little point in doing that, \
                   because ", (the) child(second), " is already on ",
                       (the) second, ".";
           give second sticky;
           print_ret "You squeeze some glue onto ", (the) second, " and \
               it spreads out to form a sticky film.";
       ],
       after [;
        Examine:
           if (self hasnt general) {
               give self general;
               "^[To apply the glue to an object, use ~squeeze glue onto \
               thing~.]";
           }
       ];

[ ShakeSub; "Nothing happens."; ];

Verb "shake"
   * noun -> Shake
   * "hands" "with" noun -> Shake;

Extend "squeeze"
   * noun "onto" noun -> Squeeze
   * noun "at" noun -> Squeeze
   * noun "on" noun -> Squeeze;

Verb "stick"
   * noun "to" noun -> Tie;


!--------------------------------------------------------------------------
! 2.2 NOUGHTS AND CROSSES
!--------------------------------------------------------------------------
! The noughts & crosses code uses a standard trick to simplify    1 8 3
! the programming, which is to use a magic square to find the     6 4 2
! rows of three (see diagram at right).                           5 0 7
!
! Given two board positions, i and j say, the position that makes up a row
! of three is given by looking up i and j in the magic square and
! subracting the values there from 12.  (The result must be checked for
! bounds and that it doesn't equal i or j).
!
! Catharine's strategy is the simplest unbeatable strategy I could devise,
! which is (1) play in the centre on the first move; (2) win if possible;
! (3) block an opponent's win if necessary; (4) play a random corner if
! possible; (5) play a random edge.
!
! It is left as an exercise for the reader to show that the simpler
! strategy which deletes steps (4) and (5) and substitutes "(5') play a
! random vacant square" can be beaten in 2/35 games.
!--------------------------------------------------------------------------

Array MagicSquare   -> 1 8 3 6 4 2 5 0 7;  ! see above
Array OXBoard       -> 0 0 0 0 0 0 0 0 0;  ! the entries in the board
Array OXCorners     -> 0 2 6 8;            ! the four corners
Array OXEdges       -> 1 3 5 7;            ! the four edges
Array OXDisplay -> " OX";                  ! characters to display

Object  OXQuestion "noughts and crosses"
with   name "noughts" "and" "crosses" "tic" "tac" "toe" "tic-tac-toe",
       next OXPaper,
       state 0,
       description "~I told you I was unbeatable at noughts and crosses,~
           says Catharine."
           "~It's such a simple game to master,~ says Catharine, ~that \
           you have no chance of beating me without cheating.~";

Object  OXPaper "piece of paper"
       ! general if seen instructions
with   name "paper" "piece" "of",
       puzzle_state 0,
       puzzle_name "Noughts and crosses",
       description [ i j k;
           font off;
           new_line;
           for (i = 2: i >= 0: i--) {
               for (j = 0: j < 3: j++) {
                   k = OXBoard->(i*3+j);
                   spaces 1;
                   if (k ~= 0)
                       print char (OXDisplay->k);
                   else {
                       if (help == 0)
                           spaces 1;
                       else
                           print i*3+j+1;
                   }
                   spaces 1;
                   if (j ~= 2) print "|";
               }
               new_line;
               if (i ~= 0) print "---+---+---^";
           }
           font on;
           if (self hasnt general) {
               give self general;
               "^[The squares are numbered from 1 to 9, with 1 at the \
               bottom left and 9 at the top right. To play, type ~play o \
               at 1~, or just ~play 1~ for short. To see the numbers, \
               type ~help on~; to turn them off, type ~help off~.]";
           }
           rtrue;
       ],
       before [ i;
        Take,Remove:
           "~But we're in the middle of a game!~ Catharine exclaims.";
        Invoke:
           move self to Toyshop;
           move OXQuestion to AskQuestions;
           <Reset self>;
           OXBoard->4 = 2;
           self.puzzle_state = 1;
           Catharine.state = 1;
           score ++;
           "^Catharine extracts a piece of paper from the chest, places \
           it on the floor, and sits by it. She draws a three by three \
           grid of squares, and carefully writes an ~X~ into the centre \
           square. ~Try playing me at noughts and crosses,~ she says to \
           you. ~But I warn you, I'm unbeatable!~";
        Reset:
           for (i = 0: i < 9: i++)
               OXBoard->i = 0;
       ];

! returns 1 if play didn't happen.
! returns 2 if play happened and the game continues.
! returns 3 if play happened and the player won.
! returns 4 if play happened and game over (a draw).

[ OXPlay
   what  ! the value being played (1 = O, 2 = X) (INPUT)
   where ! the board position to be played (INPUT)
   who   ! who is playing it (Catharine or player) (INPUT)
   i j;  ! loop counters

   if (who ~= player or Catharine ||
       where < 0 || where > 8 ||
       what ~= 1 or 2)
       "** Error: call to `OXPlay' with bad arguments **";
   if (OXBoard->where ~= 0) {
       if (who == Catharine)
           "** Error: Catharine tried to play in non-empty location **";
       "~You can't play there,~ Catharine gently admonishes you. ~That \
       square's already occupied.~";
   }
   OXBoard->where = what;
   for (i = 0: i < 9: i++) {
       if (i ~= where && OXBoard->i == what) {
           j = 12 - MagicSquare->where - MagicSquare->i;
           if (j >= 0 && j < 9) {
               j = MagicSquare->(8-j);
               if (j ~= where && j ~= i && OXBoard->j == what)
                   return 3;
           }
       }
   }
   for (i = 0: i < 9: i++)
       if (OXBoard->i == 0)
           return 2;
   return 4;
];

[ OXPlayPlayer
   what ! The value being played (1 = O, 2 = X) (INPUT)
   move ! The move decided upon for the player and for Catharine
   i j k l a;

   if (DBPaper in Toyshop)
       "Use a letter to indicate which edge to draw: ~play a~, for \
       example.";
   if (OXPaper notin Toyshop)
       "You're not playing a game at the moment.";
   if (special_number < 1 || special_number > 9)
       "Try using a number from one to nine.";
   switch (OXPlay(what, special_number - 1, player)) {
    1: rtrue;
    2: ; ! fall through to Catharine's move
    3: <Examine OXPaper>;
       remove OXPaper;
       OXPaper.puzzle_state = 3;
       Catharine.state = 0;
       score ++;
       "^~Well done,~ says Catharine. ~You appear to have won. That's \
       definitely three in a row you have there. And to think that I \
       considered myself unbeatable! It just goes to show that you can \
       always learn something new about a game.~ She tidies the paper \
       away.";
    4: <Examine OXPaper>;
       jump Drawn;
   }

   ! Catharine's strategy
   ! --------------------

   ! (1) Check that the centre has an X in it.
   if (OXBoard->4 ~= 2)
       "** Error: centre square not played in **";

   ! (2) See if there's a win for X; (3) then a blockable win for O
   for (i = 2: i >= 1: i--) {
       for (j = 0: j < 9: j++) {
           for (k = 0: k < 9: k++) {
               if (j ~= k && OXBoard->j == i && OXBoard->k == i) {
                   l = 12 - MagicSquare->j - MagicSquare->k;
                   if (l >= 0 && l < 9) {
                       l = MagicSquare->(8-l);
                       if (l ~= j && l ~= k && OXBoard->l == 0) {
                           move = l;
                           jump FoundOXMove;
                       }
                   }
               }
           }
       }
   }

   ! (4) Select a corner if possible, (5) otherwise an edge
   for (i = 0: i < 2: i++) {
       if (i == 0) a = OXCorners;
       else a = OXEdges;
       for (j = 0, k = 0: j < 4: j++)
           if (OXBoard->(a->j) == 0)
               k++;
       if (k > 0) {
           l = random(k);
           for (j = 0, k = 0: j < 4: j++) {
               if (OXBoard->(a->j) == 0) {
                   k++;
                   if (l == k) {
                       move = a->j;
                       jump FoundOXMove;
                   }
               }
           }
       }
   }

   ! No move was found (this shouldn't happen)
   "** Error: Catharine couldn't find a move **";

   .FoundOXMove;
   i = OXPlay(2, move, Catharine);
   if (i < 2) rtrue;
   print "Catharine plays square number ", (EnglishNumber) (move+1), ".^";
   <Examine OXPaper>;
   if (i == 2) "^~Your move,~ she says.";
   if (i == 3)
       print "^~I won!~ exclaims Catharine. ~You see, I'm unbeatable at \
           noughts and crosses! But ";
   if (i == 4) {
       .Drawn;
       print "^~It's a draw,~ says Catharine. ~You don't play so \
           badly. But you'll need to play better than that to beat me! ";
   }
   print "I'll give you another chance.~ She clears away the old piece \
       of paper, brings out a new one, and marks her X in the centre \
       square.^";
   OXPaper.puzzle_state = 2;
   <Reset OXPaper>;
   OXBoard->4 = 2;
   <Examine OXPaper>;
   "^~Your move,~ she says.";
];

[ XMoveSub; OXPlayPlayer(2); ];
[ OMoveSub; OXPlayPlayer(1); ];

Verb "play"
   * "at" number     -> OMove
   * number          -> OMove
   * "o" "at" number -> OMove
   * "x" "at" number -> XMove;


!--------------------------------------------------------------------------
! 2.3 THE GNOMON
!--------------------------------------------------------------------------
! The gnomon pastiches a puzzle in Brian Moriarty's game "Trinity",
! although Moriarty never used the word "chirality".
!--------------------------------------------------------------------------

Object  GnomonQuestion "gnomon and the sundial"
with   name "gnomon" "sundial",
       next Gnomon,
       state 0,
       description "~The word `chirality' means handedness,~ says \
           Catharine. ~It refers to the sense in which a spiral coils, \
           clockwise or anticlockwise.~"
           "~The only thing stopping you is the screw thread on the \
           gnomon,~ says Catharine. ~If it weren't for that, it would be \
           easy.~"
           "~Can you find a way to reflect the gnomon?~ asks \
           Catharine. ~Remember, a reflection in three dimensions is a \
           rotation in four dimensions.~ She giggles."
           "~If you can't reflect the screw thread,~ says Catharaine, \
           ~maybe you could find a way to file down the gnomon so it \
           fits?~";

Object  Sundial "sundial"
has    static supporter
       ! general if you've seen it
with   name "sundial" "sun" "dial",
       capacity 4,
       description [ i;
           if (self hasnt general)
               print "You're not quite sure why you never noticed the \
                   sundial before. ";
           print "It's a square pedestal of stone, with markings for the \
               hours. The gnomon ";
           if (Gnomon in self)
               print "casts a shadow over the flat surface";
           else
               print "is missing, and there is a small hole in the \
                   centre of the flat surface";
           if (children(self)>1 || (Gnomon notin self && children(self)>0)) {
               objectloop(i in self) give i workflag;
               give Gnomon ~workflag;
               print ". On the sundial";
               WriteListFrom(child(self), ISARE_BIT + ENGLISH_BIT +
                   WORKFLAG_BIT);
           }
           ".";
       ],
       before [;
        Receive,Tie:
           if (noun ~= Gnomon) rfalse;
           if (Gnomon hasnt general) {
               Gnomon.puzzle_state = 2;
               "You attempt to screw the gnomon into the sundial, but it \
               won't fit. After some examination, you realise that this \
               is because the screw threads of the gnomon and the \
               sundial have opposite chirality.";
           }
           if (Gnomon hasnt sticky)
               "The filed-down gnomon fits into the hole, but the fit \
               isn't good, and it wobbles. You take it out, dissatisfied \
               with your efforts.";
           give Gnomon static ~sticky;
           move Gnomon to self;
           move Shadow to Toyshop;
           Gnomon.puzzle_state = 3;
           score ++;
           "The filed-down gnomon fits into the hole and adheres.";
       ];

Object  Gnomon "gnomon"
       ! general if filed down
       ! static if glued to sundial
with   name "gnomon",
       puzzle_state 0,
       puzzle_name "The gnomon and the sundial",
       puzzle_pre CardboardBox,
       description [;
           print "It's the part of a sundial that casts the shadow. Made \
               of metal, there is a long screw on one end";
           if (self has general)
               print ". The screw thread has been filed off";
           ".";
       ],
       before [;
        Tie:
           if (second == Sundial) <<PutOn self Sundial>>;
        Invoke:
           move self to player;
           move GnomonQuestion to AskQuestions;
           move Sundial to Toyshop;
           self.puzzle_state = 1;
           score ++;
           "^~You might like to have a go at this problem,~ says \
           Catharine. She reaches into a pocket and extracts a strange \
           metal object. ~It's a gnomon,~ she says, handing it to \
           you. ~It's the missing part of that sundial over there. Can \
           you repair it?~";
       ];

Object  Shadow "shadow"
has    scenery
with   name "shade" "shadow",
       description "The gnomon casts a shadow across the sundial. The \
           tip of the shadow just reaches the marking for tea-time.",
       before [;
        Examine: ;
        Receive:
           if (noun ~= Mushroom || Mushroom has general)
               <<PutOn noun Sundial>>;
           give Mushroom general;
           move Mushroom to Sundial;
           move Rod to Sundial;
           "You place the mushroom on the sundial. As the tip of the \
           shadow falls across the mushroom, a white door appears in the \
           mushroom's stalk. An inch-high man in shorts and t-shirt \
           walks through the door and stares up at you, aghast, before \
           turning on his heels and exiting through the door, which \
           vanishes as soon as it closes. He left something behind, \
           though.";
        default: "You can't do that to a shadow.";
       ];

[ FileWithSub;
   if (second hasnt is_disk)
       print_ret (The) second, " won't make a good file.";
   if (noun has animate)
       "That seems rather a dangerous thing to attempt.";
   if (noun ~= Gnomon)
       "Futile.";
   if (Gnomon has general)
       "You've already filed down the gnomon.";
   give Gnomon general;
   "The disk from the Towers of Hanoi set isn't a particularly good \
   file, but you make do, and with a lot of effort you manage to file \
   down the gnomon's screw thread.";
];

Verb "file"
   * noun "with" held -> FileWith
   * "down" noun "with" held -> FileWith;

Extend "screw"
   * held "into" noun -> Insert;


!--------------------------------------------------------------------------
! 2.4 THE TOWERS OF HANOI
!--------------------------------------------------------------------------

Object  HanoiQuestion "towers of Hanoi"
with   name "hanoi" "towers" "of" "peg" "disk" "disc" "disks" "discs"
           "pegs",
       next HanoiBoard,
       state 0,
       description "~The usual solution is recusive,~ says Catharine. \
           ~For example, to move a stack of three disks from the left \
           peg to the right peg, first move a stack of two disks to the \
           middle peg, then move the big disk, then move the stack of \
           two again."
           "~I said the usual solution is recursive,~ says Catharine, \
           ~but you'll see if you take the trouble to work it out that \
           three disks take seven moves to transport. So you'll have to \
           be cleverer than that to move them in six."
           "~The usual recursive solution is in fact minimal,~ says \
           Catharine. ~So you'll have to cheat to do better than that.~"
           "~If you could move two disks at a time,~ says Catharine, ~it \
           would be easy, but the rules only allow you to pick up the \
           top disk from a stack.~";

Object  HanoiBoard "Towers of Hanoi board"
has    supporter
       ! general if explained the names of the components
with   name "tower" "towers" "of" "hanoi" "board" "wooden" "wood",
       number 0,     ! counts the number of moves made
       state 0,      ! counts number of times play tried to cheat
       prev_num 0,   ! previous value of number, so C can announce change
       puzzle_state 0,
       puzzle_name "The towers of Hanoi",
       puzzle_pre CardboardBox,
       description [ i j k l;
           ! We can provide a text-only description
           if (pretty == 0) {
               print "A wooden board, bearing three pegs.^";
               <Examine LeftPeg>;
               <Examine MiddlePeg>;
               <Examine RightPeg>;
               give self general;
               rtrue;
           }

           ! Alternatively, draw a picture
           new_line;
           font off;
           for (i = 0: i < 3: i++) {
               for (j = LeftPeg: j ~= 0: j = j.next) {
                   for (k = 0, l = j: l ~= 0 && k < 3 - i: k++) {
                       if (l ~= 0 && children(l) ~= 0)
                           l = child(l);
                       else l = 0;
                   }
                   if (l ~= 0) l = l.number;
                   spaces(6-l*2);
                   for (k = 0: k < l: k++) print "==";
                   print "|";
                   for (k = 0: k < l: k++) print "==";
                   spaces(8-l*2);
               }
               new_line;
           }
           print "------+--------------+--------------+------^";
           font on;
           if (self hasnt general) {
               give self general;
               "^[The disks are called ~small~, ~medium~ and ~big~; the \
               pegs are ~left~, ~middle~ and ~right~.]";
           }
           rtrue;
       ],
       capacity 3,
       before [;
        Take,Remove:
           "It's easier to play with the Towers of Hanoi when they're on \
           the floor.";
        Receive:
           if (noun has is_disk)
               "You should say which peg you want to put it on.";
           "There's only room for the three pegs.";
        Invoke:
           move self to Toyshop;
           move HanoiQuestion to AskQuestions;
           <Reset self>;
           self.puzzle_state = 1;
           score ++;
           "^Catharine rummages around in the chest and extracts a large \
           wooden board on which three pegs are set. Three metal disks, \
           each with a hole in the middle, are threaded onto the left \
           peg. ~This puzzle is called the Towers of Hanoi,~ she says. \
           ~The idea is to move the three disks from the left peg to the \
           right peg in only six moves, without putting a disk on top of \
           a smaller one, and only picking up the top disk from a \
           peg. Here, have a go.~";
        Reset:
           remove BigDisk;
           remove MediumDisk;
           remove SmallDisk;
           move BigDisk to LeftPeg;
           move MediumDisk to BigDisk;
           move SmallDisk to MediumDisk;
           self.state = 0;
           self.number = 0;
           self.prev_num = 0;
       ],
       each_turn [;
           if (BigDisk in RightPeg && MediumDisk in BigDisk &&
               SmallDisk in MediumDisk) {
               self.puzzle_state = 3;
               self.each_turn = NULL;
               score ++;
               "^~You did it!~ says Catharine excitedly. ~And there was \
               me thinking that it took two to the power of n moves, \
               less one, to move a stack of n disks from one peg to \
               another!~";
           }
           if (self.number >= 6) {
               <Reset self>;
               self.puzzle_state = 2;
               "^~You've had your six moves,~ says Catharine, ~and it \
               doesn't look as though you've managed to move the stack \
               successfully. It's back to the beginning for you.~ She \
               collects up the disks and puts them back on the left \
               peg.";
           }
           if (self.state >= 3) {
               self.state = 0;
               switch (random(3)) {
                1: "^~You don't seem to be getting the hang of this,~ \
                   she says. ~Perhaps you should try a different game.~";
                2: "^~The rules are really very simple,~ she says, ~One \
                   disk at a time, and never put a larger disk on a \
                   smaller one.~";
                3: "^~There's nothing to be gained from cheating like
                   that,~ she says.";
               }
           }
           if (self.number ~= self.prev_num) {
               self.prev_num = self.number;
               print "^~That's ", (EnglishNumber) self.number, " move";
               if (self.number > 1) print "s";
               ",~ says Catharine.";
           }
       ];

Class   PegClass
has    concealed static supporter is_peg
with   name "peg",
       capacity 1,
       description [ i;
           if (children(self) == 0) print_ret (The) self, " is empty.";
           print "On ", (the) self;
           if (children(child(self)) == 0) print " is ";
           else print " are ";
           for (i = self: children(i) ~= 0:) {
               i = child(i);
               print (a) i;
               if (children(i) ~= 0) {
                   if (children(child(i)) == 0)
                       print " and ";
                   else print ", ";
               }
           }
           ".";
       ],
       before [;
        Take,Remove,Push,Pull,Turn:
           print_ret (The) self, " is fixed to the board.";
        Receive:
           if (noun hasnt is_disk)
               "You can only put disks on the peg.";
           if (children(self) ~= 0)
               <<PutOn noun child(self)>>;
       ];

Object  LeftPeg "left peg" HanoiBoard
class  PegClass
with   name "left",
       next MiddlePeg;

Object  MiddlePeg "middle peg" HanoiBoard
class  PegClass
with   name "middle",
       next RightPeg;

Object  RightPeg "right peg" HanoiBoard
class  PegClass
with   name "right",
       next 0;

Global MovingStuckDisk = 0;

Class   DiskClass
has    supporter is_disk
with   name "disk" "disc" "metal",
       capacity 1,
       stuck_to 0,
       description [ i;
           for (i = self: i ~= 0 && i hasnt is_peg: i = parent(i));
           print (The) self, " is made of metal, with a very rough \
               surface: it is painful to run your finger across \
               it. There is a hole in the middle";
           if (i hasnt is_peg) ".";
           print ". It is on ", (the) i;
           if (parent(self) == i && children(self) == 0) ".";
           print ". It ";
           if (parent(self) ~= i) {
               print "rests on ", (the) parent(self);
               if (children(self) ~= 0) print " and ";
           }
           if (children(self) ~= 0)
               print "supports ", (the) child(self);
           ".";
       ],
       before [ i;
        Take,Remove:
           if (self.stuck_to ~= 0) {
               i = parent(self.stuck_to);
               if (i hasnt is_disk && i hasnt is_peg)
                   print_ret (The) self, " appears to be firmly stuck to ",
                       (the) self.stuck_to, ".";
           }
           if (HanoiBoard.puzzle_state < 3 && children(self) ~= 0 &&
             MovingStuckDisk == 0) {
               HanoiBoard.state = HanoiBoard.state + 1;
               "~You can only take the top disk from a peg,~ says \
               Catharine.";
           }
           if (parent(self) hasnt is_disk && parent(self) hasnt is_peg)
               rfalse;
           if (HanoiBoard.puzzle_state < 3) {
               for (i = BigDisk: i ~= 0: i = i.next)
                   if (parent(i) hasnt is_disk && parent(i) hasnt is_peg) {
                       HanoiBoard.state = HanoiBoard.state + 1;
                       "~You can only have one disk at a time,~ says \
                       Catharine.";
               }
           }
           if (self.stuck_to ~= 0) {
               i = parent(self.stuck_to);
               if (i has is_disk || i has is_peg) {
                   MovingStuckDisk = 1;
                   <(action) self.stuck_to second>;
                   ResetVagueWords(self.stuck_to);
                   MovingStuckDisk = 0;
                   rtrue;
               }
               "** Error: disk came unstuck **";
           }
        Receive:
           if (noun hasnt is_disk)
               print_ret "You can't put ",(the)noun," on ",(the)self,".";
           if (parent(self) hasnt is_peg && parent(self) hasnt is_disk)
               print_ret "It would be better to put ", (the) self, " on \
                   a peg first.";
           if (HanoiBoard.puzzle_state < 3 && noun.number > self.number) {
               HanoiBoard.state = HanoiBoard.state + 1;
               print_ret "~You could put ", (the) self, " on ", (the) noun,
                   ",~ says Catharine, ~but putting ", (the) noun, " on \
                   ", (the) self, " is not allowed.~";
           }
           if (children(self) > 0)
               <<PutOn noun child(self)>>;
       ],
       after [;
        Take,Remove:
           if (MovingStuckDisk == 1)
               print_ret "Taken. ", (The) self, " comes with it.";
        PutOn:
           if (second hasnt is_disk && second hasnt is_peg) rfalse;
           HanoiBoard.number = HanoiBoard.number + 1;
           if (noun has sticky || second has sticky) {
               give noun ~sticky;
               give second ~sticky;
               noun.stuck_to = second;
               print (The) noun, " adheres to ", (the) second, ".^";
           }
           if (pretty == 1)
               <<Examine HanoiBoard>>;
       ];

Object  BigDisk "big disk" LeftPeg
class  DiskClass
with   name "big" "large",
       number 3,
       next MediumDisk;

Object  MediumDisk "medium disk" BigDisk
class  DiskClass
with   name "medium",
       number 2,
       next SmallDisk;

Object  SmallDisk "small disk" MediumDisk
class  DiskClass
with   name "small",
       number 1,
       next 0;


!--------------------------------------------------------------------------
! 2.5 DOTS AND BOXES
!--------------------------------------------------------------------------
! Catharine's strategy is remarkably simple, and is adapted from the
! discussion in "Winning Ways" volume 2 (Conway, Berlekamp and Guy).  It
! is, (1) capture any available boxes; (2) if the position is in the
! dictionary, play the dictionary move; (3) if the play that is
! rotationally symmetrical to the player's last move is available, play
! that; (4) play randomly.  Only 2 essentially different dictionary
! positions are needed.
!
! Dots and boxes on a 2x2 board is a win for the first player, but I
! believe that this second-player strategy is `optimal' in the sense that
! it wins if the first player makes a single mistake.
!
! `DBBoxes' is really a 4x4 array; it contains the numbers of the four
! edges for each of the four boxes (to make it easy to check which boxes
! have been closed by a move).
!
! `DBInput' maps edge number to the word specifying that edge.  The
! slightly strange orders of lettering of the edges on the board is to
! prevent the sequence of plays `abcd' from being a win (I think that would
! be too obvious).
!
! `DBDictionary' contains 16 positions (really 2 positions in each of 8
! orientations - it seemed simpler to extend the dictionary than add code
! to do the rotations and reflections); the board is encoded by a 12-bit
! binary number and is followed by the correct move in that position.
!
! We need the global variable `NoBoxes' because the `DBPlay' function has
! to return two pieces of information: the game status, and the number of
! boxes captured.
!--------------------------------------------------------------------------

Global NoBoxes; ! number of boxes captured in the most recent move
Array DBBoard -> 0 0 0 0 0 0 0 0 0 0 0 0; ! the edges of the board
Array DBOwner -> 0 0 0 0;                 ! box owner (0=none, 1=P, 2=C)
Array DBDisplay --> " " "Z" "C";          ! components of display
Array DBBoxes -> 0 2 3 5 1 3 4 6 5 7 8 10 6 8 9 11;
Array DBInput --> n$a n$b n$g n$h n$i n$c n$d n$j n$k n$l n$e n$f;
Array DBDictionary -->
   $$111000000101  4    $$111000010111  4
   $$101010000101  1    $$101010010111  1
   $$101000010101 10    $$111010010101 10
   $$101000000111  7    $$111010000111  7
   $$110010010010  2    $$110010010111  2
   $$011010010010  0    $$011010010111  0
   $$010010010110 11    $$111010010110 11
   $$010010010011  9    $$111010010011  9;

Object  DBQuestion "dots and boxes"
with   name "dots" "and" "boxes",
       next DBPaper,
       state 0,
       description "~Did you never play dots and boxes when you were a \
           child?~ asks Catharine. ~Many children play it, but few \
           continue to have any interest in it when they grow up, which \
           I think is a shame, because it's full of interesting tricks: \
           struggles for control, sacrifices and lots of game theory.~";

Object  DBPaper "piece of paper"
       ! general if seen instructions
with   name "piece" "of" "paper",
       puzzle_state 0,
       puzzle_pre OXPaper CardboardBox,
       puzzle_name "Dots and boxes",
       description [ i j k;
           if (self hasnt general) {
               give self general;
               font off;
               print "^+ a + b +^g   h   i^+ c + d +^j   k   l^+ e + f +^";
               font on;
               "^[The edges are labelled with letters from A to L. To \
               fill in an edge, type for example ~play A~. Type ~help \
               on~ to see the letters; type ~help off~ to turn them \
               off.]";
           }
           new_line;
           font off;
           for (i = 0: i < 3: i++) {
               for (j = 0: j < 2: j++) {
                   k = DBBoard->(i*5+j);
                   print "+";
                   if (k == 1)
                       print "---";
                   else {
                       if (help == 1)
                           print " ", (address) DBInput-->(i*5+j), " ";
                       else
                           spaces 3;
                   }
               }
               print "+^";
               if (i ~= 2) {
                   for (j = 0: j < 3: j++) {
                       k = DBBoard->(i*5+j+2);
                       if (k == 1)
                           print "|";
                       else {
                           if (help == 1)
                               print (address) DBInput-->(i*5+j+2);
                           else
                               spaces 1;
                       }
                       if (j ~= 2) {
                           print " ", (string)
                               (DBDisplay-->(DBOwner->(i*2+j))), " ";
                       }
                   }
                   new_line;
               }
           }
           font on;
           rtrue;
       ],
       before [ i;
        Take,Remove:
           "~But we're in the middle of a game!~ Catharine exclaims.";
        Invoke:
           move self to Toyshop;
           move DBQuestion to AskQuestions;
           <Reset self>;
           self.puzzle_state = 1;
           Catharine.state = 1;
           score ++;
           "^Catharine extracts a piece of paper from the chest, sits \
           down by it, and draws nine dots in three rows of three. ~Do \
           you know how to play dots and boxes?~ she asks. ~We take \
           turns to draw an edge between two dots. Whoever draws the \
           last edge around one of the four boxes captures that box and \
           writes their initial in it - that's `Z' for you and `C' for \
           me. I'll let you start, but since it's an advantage to start, \
           if we get two boxes apiece then I win. And I warn you, I'm \
           unbeatable.~";
        Reset:
           for (i = 0: i < 12: i++)
               DBBoard->i = 0;
           for (i = 0: i < 4: i++)
               DBOwner->i = 0;
       ];

! returns 1 if play didn't happen
! returns 2 if move as normal
! returns 3 if game over
! returns 4 if complimenting move

[ DBPlay
   where ! edge to be drawn (0 to 11) (INPUT)
   who   ! who is drawing the edge (Catharine or player) (INPUT)
   i j;  ! loop counters

   if (who ~= player or Catharine ||
       where < 0 || where >= 12)
       "** Error: call to `DBPlay' with bad arguments **";
   if (DBBoard->where ~= 0) {
       if (who == Catharine)
           "** Error: Catharine tried to play in non-empty location **";
       "~You can't play there,~ Catharine gently admonishes you. ~That \
       edge has already been drawn.~";
   }
   DBBoard->where = 1;

   ! search for filled-in boxes
   for (NoBoxes = 0, i = 0: i < 4: i++) {
       if (DBOwner->i == 0) {
           for (j = 0: j < 4: j++)
               if (DBBoard->(DBBoxes->(i*4+j)) == 0)
                   jump NextBox;
           if (who == player)
               DBOwner->i = 1;
           else
               DBOwner->i = 2;
           NoBoxes ++;
           .NextBox;
       }
   }

   ! check for game end
   for (i = 0: i < 12: i++)
       if (DBBoard->i == 0) {
           if (NoBoxes > 0)
               return 4;
           return 2;
       }
   return 3;
];

[ DBMoveSub
   pmove ! move selected by the player
   cmove ! move selected by Catharine
   i j k ! loop counters
   sq    ! counts the number of boxes captured by Catharine in her moves
   c     ! counts number of boxes owned by Catharine at end of game
   end;  ! set to 1 iff game is over

   if (OXPaper in Toyshop)
       "The squares are numbered from 1 to 9, with 1 at the bottom left \
       and 9 at the top right. To play, type ~play o at 1~, or just \
       ~play 1~ for short.";
   if (DBPaper notin Toyshop)
       "You're not playing a game at the moment.";
   for (pmove = 0: pmove < 12: pmove++)
       if (special_word == DBInput-->pmove)
           jump DecodedMove;
   "Try using a letter from A to L.";

   .DecodedMove;
   give DBPaper general;
   switch (DBPlay(pmove, player)) {
    1: rtrue;
    2: ;
    3: end = 1; jump DisplayBoard;
    4: <Examine DBPaper>; "^~You get another move,~ says Catharine.";
   }

   ! Catharine's strategy
   ! --------------------

   ! (1) Complete any boxes
   do {
       k = 0; ! set to 1 if any boxes were captured
       for (i = 0: i < 4: i++) {
           for (c = 0, j = 0: j < 4: j++) {
               if (DBBoard->(DBBoxes->(i*4+j)) == 1)
                   c ++;
               else
                   cmove = DBBoxes->(i*4+j);
           }
           if (c == 3) {
               switch (DBPlay(cmove, Catharine)) {
                1: rtrue;
                2: "** Error: Catharine closed a box but got no \
                    complimenting move **";
                3: sq = sq + NoBoxes; cmove = -1; end = 1; jump DoneMove;
                4: sq = sq + NoBoxes; k = 1;
               }
           }
       }
   } until (k == 0); ! loop until no more boxes can be captured

   ! (2) See if the position is in the dictionary
   for (k = 0, i = 0: i < 12: i++)
       k = k * 2 + DBBoard->i;
   for (i = 0: i < 16: i++)
       if (k == DBDictionary-->(i*2)) {
           cmove = DBDictionary-->(i*2+1);
           jump FoundDBMove;
       }

   ! (3) See if the symmetry move is available
   if (DBBoard->(11 - pmove) == 0) {
       cmove = 11 - pmove;
       jump FoundDBMove;
   }

   ! (4) Play randomly
   for (j = 0, i = 0: i < 12: i++)
       if (DBBoard->i == 0)
           j++;
   if (j > 0) {
       k = random(j);
       for (j = 0, i = 0: i < 12: i++) {
           if (DBBoard->i == 0) {
               j ++;
               if (k == j) {
                   cmove = i;
                   jump FoundDBMove;
               }
           }
       }
   }

   ! No move was found (shouldn't happen)
   "** Error: unable to find move for Catharine **";

   ! Catharine can't end the game or capture a box here, because if either
   ! were possible, it would have happened at stage (1).
   .FoundDBMove;
   switch (DBPlay(cmove, Catharine)) {
    1: rtrue;
    2: ;
    3: "** Error: Catharine ended the game with symmetry/random move **";
    4: "** Error: Catharine closed a box with the symmetry/random move **";
   }

   .DoneMove;
   print "Catharine ";
   if (sq > 0) {
       print "closes ", (EnglishNumber) sq, " box";
       if (sq > 1) print "es";
       if (cmove >= 0) print " and ";
   }
   if (cmove >= 0)
       print "draws the line ~", (address) DBInput-->cmove, "~";
   print ".^";

   .DisplayBoard;
   <Examine DBPaper>;
   if (end == 1) {
       for (c = 0, i = 0: i < 4: i++)
           if (DBOwner->i == 2)
               c++;
       print "^~The game is over,~ says Catharine. ~You scored ",
           (EnglishNumber) (4-c), " box";
       if (c ~= 3) print "es";
       ! Catharine scores at least one box always, so no problem here:
       print " to my ", (EnglishNumber) c, ".";
       if (c >= 2) {
           <Reset DBPaper>;
           DBPaper.puzzle_state = 2;
           if (c == 2)
               print " Two boxes apiece, which means that, as second \
                   player,";
           " I won! I told you I was unbeatable! But I'll give you \
           another chance.~ She tidies away the finished game and brings \
           out a new piece of paper, and draws a grid of nine dots on \
           it. ~I'll let you start again.~";
       }
       remove DBPaper;
       DBPaper.puzzle_state = 3;
       Catharine.state = 0;
       score ++;
       " It seems that you won. Well, my. I was sure that Mister Conway \
       said that my symmetry strategy would never fail. Still, we live \
       and learn.~ She tidies away the finished game.";
   }
   "^~Your move,~ she says.";
];

Extend "play"
   * special -> DBMove;


!--------------------------------------------------------------------------
! 2.6 THE ROBOT MOUSE
!--------------------------------------------------------------------------
! This puzzle pastiches one in "Curses" by Graham Nelson.  My version is
! easier in some ways, such as "put mouse in hole" being recognised, and
! harder in others...
!--------------------------------------------------------------------------

Object  MouseQuestion "robot mouse"
with   name "robot" "mouse" "hole" "cat" "maze" "key" "mousehole",
       next RobotMouse,
       state 0,
       description "~I can offer you a few hints about how the cat \
           works,~ says Catharine. ~First, the cat only moves after \
           the mouse has moved successfully.~"
           "~The cat's strategy,~ says Catharine, ~is always to move so \
           as to minimise the distance between it and the mouse.~"
           "~If the cat's strategy doesn't provide a unique move,~ says \
           Catharine, ~then it moves to the intersection occupied by the \
           mouse at the end of the last turn, if possible. Otherwise, it \
           chooses randomly among the moves that bring it closest to the \
           mouse.~"
           "~If you think about what I've said,~ says Catharine, ~and \
           play with some counters on paper, you'll realise that it's \
           just a parity problem. The cat is always an even number of \
           moves away from the mouse at the end of its turn, and the cat \
           can always reduce this number if the mouse doesn't \
           retreat.~"
           "~Now, if the mouse didn't start an even number of moves away \
           from the cat,~ says Catharine, ~the game would be a very \
           different thing altogether.~";

Object  Maze "maze";
       ! general if mouse has the key

Global mouse_x;
Global mouse_y;
Global cat_x;
Global cat_y;
Global last_mouse_x;
Global last_mouse_y;
Global key_x = 1;
Global key_y = 1;

Object  Mousehole "mousehole"
has    concealed talkable container open static
with   name "hole" "mousehole",
       description "A little hole in the wainscot of the west wall, such \
           as might have been made by mice.",
       life [;
        Order:
           if (RobotMouse notin Maze)
               "You feel silly talking to the hole.";
           RunLife(RobotMouse,##Order);
           rtrue;
       ],
       before [;
        Examine: PrintOrRun(self,description); rtrue;
        Search: "The hole is dark inside.";
        Receive:
           switch(noun) {
            Baguette:
               print "You push the baguette into the mousehole";
               if (RobotMouse in Maze && mouse_x == 4 && mouse_y == 4) {
                   mouse_x = 3;
                   ". The loaf encounters an obstacle, which you push as \
                   far into the hole as you can.";
               }
               if (RobotMouse in Maze && cat_x == 4 && cat_y == 4)
                   ". The loaf encounters an obstacle, and there is an \
                   angry hiss. Alarmed, you retrieve your bread.";
               ", but you encounter no obstruction, and retrieve your \
               bread.";
            RobotMouse:
               <Reset RobotMouse>;
               "You put the robot mouse into the hole.";
            default: "Nothing is to be gained by this.";
           }
       ];

Object  RobotMouse "robot mouse"
has    talkable
       ! general if Catharine's explained about cat
with   name "robot" "mouse" "grey" "fur" "wheel" "wheels" "rubber" "gray",
       description "A little mouse clad in fake grey fur, with four \
           rubber wheels.",
       puzzle_state 0,
       puzzle_pre CardboardBox,
       puzzle_name "The robot mouse",
       life [ dx dy sx sy;
        Order:
           if (action ~= ##Enter or ##Go || noun notin compass)
               "The mouse emits a querulous, unhappy bleep.";
           if (noun ~= n_obj or s_obj or e_obj && noun ~= w_obj)
               "The mouse bleeps with annoyance.";
           if (parent(self) ~= Toyshop or Maze)
               "The mouse wheels spin, but can't get a good enough grip \
               to move.";
           if (self in Toyshop && noun ~= w_obj)
               "The mouse emits a low-pitched bleep.";
           if (self in Toyshop) {
               <Reset self>;
               "The mouse just squeezes through the hole, and disappears \
               from view. Unfortunately you can't very easily talk to it \
               now...";
           }
           if (mouse_x == 4 && mouse_y == 4 && noun == e_obj) {
               move self to Toyshop;
               print "The mouse runs back out of the hole, looking \
                   dustier but none the worse for its adventure";
               if (Maze has general) {
                   give Maze ~general;
                   move BrassKey to Toyshop;
                   RobotMouse.puzzle_state = 3;
                   score ++;
                   ", and then almost looks proud as a magnet cuts out \
                   and something metal drops off the underside of the \
                   mouse onto the floor. The lost key is found!"
               }
               ".";
           }
           if ((mouse_x == 1 && noun == w_obj) ||
               (mouse_x == 4 && noun == e_obj) ||
               (mouse_y == 1 && noun == n_obj) ||
               (mouse_y == 4 && noun == s_obj))
               "From somewhere inside the hole, you hear a disconsolate \
               bleep.";
           print "From somewhere inside the hole, you hear tiny wheels \
               spin";

           last_mouse_x = mouse_x;
           last_mouse_y = mouse_y;
           switch (noun) {
            n_obj: mouse_y = mouse_y - 1;
            s_obj: mouse_y = mouse_y + 1;
            e_obj: mouse_x = mouse_x + 1;
            w_obj: mouse_x = mouse_x - 1;
           }
           ! See if the mouse has picked up the key.
           if (mouse_x == key_x && mouse_y == key_y) {
               key_x = 0;
               key_y = 0;
               give Maze general;
               print ", followed by an excitable high-pitched beep and a \
                   dull clanging noise";
           }
           print ".^";
           if (mouse_x == cat_x && mouse_y == cat_y)
               jump MouseDead;

           ! Now the cat. First see if it can chase to the last posn
           ! occupied by the mouse. If it can do this, it can't catch the
           ! mouse (by parity), so no need to check that first.
           dx = last_mouse_x - cat_x;
           dy = last_mouse_y - cat_y;
           if (dx * dx + dy * dy == 1) {
               cat_x = last_mouse_x;
               cat_y = last_mouse_y;
               rtrue;
           }

           ! Now see if it can catch the mouse
           dx = mouse_x - cat_x;
           dy = mouse_y - cat_y;
           if (dx * dx + dy * dy == 1)
               jump MouseDead;

           ! Find out which way it needs to go.
           sx = Sign(dx);
           sy = Sign(dy);
           if (sx ~= 0 && sy ~= 0) {
               if (Abs(dx) > Abs(dy)) sy = 0;
               else {
                   if (Abs(dy) > Abs(dx)) sx = 0;
                   else {
                       switch (random(2)) {
                        1: sx = 0;
                        2: sy = 0;
                       }
                   }
               }
           }
           cat_x = cat_x + sx;
           cat_y = cat_y + sy;
           rtrue;

           .MouseDead;
           move self to Toyshop;
           if (Maze has general) {
               key_x = mouse_x;
               key_y = mouse_y;
               give Maze ~general;
           }
           print "^From somewhere inside the hole, you hear a sharp hiss, \
               and a long drawn-out high-pitched squeal, and then the \
               robot mouse comes flying out of the hole at high speed, \
               looking somewhat the worse for wear.^";
           if (self hasnt general) {
               give self general;
               if (self.puzzle_state == 1)
                   self.puzzle_state = 2;
               print "^~Ah, I should have explained some things,~ says \
                   Catharine. ~There's a maze inside the wainscot in the \
                   form of a four-by-four grid of intersections, and my \
                   key is at the northwest corner. There's a robot cat \
                   in the maze too. When the robot mouse starts at the \
                   southeast corner, the robot cat goes to the northwest \
                   corner. Every time you give an instruction to the \
                   mouse and it moves from one intersection to the next, \
                   the robot cat moves too. I hope this helps.~^";
           }
           rtrue;
       ],
       before [;
        Reset:
           move self to Maze;
           mouse_x = 4;
           mouse_y = 4;
           last_mouse_x = 4;
           last_mouse_y = 4;
           cat_x = 1;
           cat_y = 1;
        Invoke:
           move Mousehole to Toyshop;
           move MouseQuestion to AskQuestions;
           move self to Toyshop;
           self.puzzle_state = 1;
           score ++;
           "^~That's a pretty robot mouse you have there,~ says \
           Catharine. ~Looks like just what I need. You see, I dropped \
           my key through the floorboards from the attic, and it's \
           fallen into the wainscot somewhere. Perhaps if you could send \
           the mouse into that mousehole over there, then it could find \
           my key.~";
       ];

[ Sign x;
   if (x < 0) return -1;
   if (x > 0) return 1;
   return 0;
];

[ Abs x;
   if (x < 0) return -x;
   return x;
];

Object  BrassKey "small brass key"
with   name "small" "brass" "key",
       initial "Lying where the mouse dropped it is a small \
           brass-coloured key.",
       description "Brass is of course a non-magnetic copper-zinc alloy, \
           so presumably this key also contains iron.";


!--------------------------------------------------------------------------
! 2.7 TEA-TIME
!--------------------------------------------------------------------------
! The egg puzzle is a reference to Adventions' Unnkulia series, in which
! every game has a puzzle that involves cooking or otherwise manipulating
! an egg, usually for the benefit of Duhdist monks.  The inclusion of food
! is a useful excuse to have a mushroom (another Trinity reference) and a
! baguette with which to solve the robot mouse puzzle.
!--------------------------------------------------------------------------

Object  EggQuestion "egg"
with   name "egg" "boiled" "fried" "raw" "hamper",
       article "an",
       next Hamper,
       state 0,
       description "~This isn't really a puzzle,~ says Catharine. ~It's \
           just tea-time. If you want to make a puzzle out of it, try \
           eating the egg.~"
           "Catharine says, ~If you haven't played Infocom's wonderful \
           game `Trinity' by Brian Moriarty, you're going to have \
           problems with this puzzle.~"
           "~In the game `Trinity',~ says Catharine, ~part of the action \
           takes place on the surface of a giant sundial covered with \
           mushrooms, each mushroom representing a mushroom cloud from \
           the explosion of an atomic bomb.~"
           "~The shadow from the gnomon was important too,~ says \
           Catharine. ~When it touched a mushroom, magical things would \
           happen.~"
           "Catharine says, ~You need to have played Graham Nelson's \
           marvellous game `Curses' too, or else you'll struggle.~"
           "~In the game `Curses',~ says Catharine, ~There are a number \
           of magic wands that can be charged by striking and discharged \
           by pointing them at things.~";

Object  Hamper "hamper"
has    container open
with   name "hamper" "picnic" "basket" "wicker" "wickerwork" "wicker-work",
       capacity 5,
       puzzle_state 0,
       puzzle_name "The egg",
       puzzle_pre Gnomon,
       description "A wicker picnic hamper.",
       before [;
        Invoke:
           move self to Toyshop;
           self.puzzle_state = 1;
           move EggQuestion to AskQuestions;
           score ++;
           "^Catharine looks at the shadow on the sundial. ~It looks \
           like it's tea-time,~ she says. ~Perhaps you would care to \
           join me in some food?~ She opens the chest, picks up a wicker \
           hamper, and closes the chest again.";
       ];

Nearby  Baguette "baguette"
has    edible
with   name "bread" "stick" "french" "baguette" "loaf" "breadstick",
       description "A long, thin loaf of French bread.";

Nearby  RawEgg "egg"
       ! general if you know it's raw
with   name "egg" "shell" "eggshell",
       article "an",
       before [;
        Eat,Attack,Cut:
           if (self has general)
               "You've never really been fond of raw egg.";
           "You're not certain if the egg is raw or cooked, and it would \
           be nice to find out before you broke it.";
        Shake,Turn:
           give self general;
           Hamper.puzzle_state = 2;
           "The egg wobbles distinctly in your hand; it's clearly raw.";
       ],
       after [;
        Drop: "You put the egg down gently.";
       ];

Nearby  Mushroom "mushroom"
has    edible
with   name "mushroom" "fungus";

Object  Rod "featureless mahogany rod"
       ! general if struck
with   name "featureless" "mahogany" "rod",
       description "A featureless mahogany rod, whose purpose is \
           oblique. It is about the size of a matchstick.",
       before [;
        PointAt:
           if (self hasnt general) rfalse;
           give self ~general;
           print "A tiny gout of fire leaps out from the tip of the rod, ";
           switch (second) {
            Baguette: "lightly toasting the baguette.";
            RawEgg:
               move CookedEgg to parent(RawEgg);
               StartTimer(CookedEgg,3);
               remove RawEgg;
               print "neatly cooking the egg";
               if (CookedEgg in player) {
                   move CookedEgg to Toyshop;
                   print ", which is now too hot to hold, and you drop \
                       it";
               }
               ".";
            Catharine: "but Catharine leaps backwards to avoid it. ~Mind \
               where you point that thing!~ she says.";
            Mushroom: "gently frying the mushroom.";
            selfobj: "making you uncomfortably hot.";
            default: print_ret "but ", (the) second, " seems unaffected.";
           }
        Strike:
           if (self has general) {
               remove self;
               "The already-charged rod explodes! Your hand is somewhat \
               singed.";
           }
           give self general;
           "The rod charges with etherial power, drawn up from the earth \
           through ley lines...";
       ];

Object  CookedEgg "egg"
has    edible general
       ! general if too hot
with   name "egg" "shell" "eggshell",
       article "an",
       before [;
        Take,Remove,Touch:
           if (self has general) "The egg is still too hot too to hold.";
        Shake,Turn: "The egg doesn't wobble at all: it's clearly \
           hard-boiled.";
       ],
       after [;
        Eat: Hamper.puzzle_state = 3; score++;
       ],
       time_left 0,
       time_out [;
           give self ~general;
       ];

[ PointAtSub; "Nothing happens."; ];

Verb "point"
   * held "at" noun -> PointAt;

[ StrikeSub; <<Attack noun>>; ];

Verb "strike"
   * noun -> Strike;

Verb "spin"
   * noun -> Turn;


!--------------------------------------------------------------------------
! 2.8 DODGEMS
!--------------------------------------------------------------------------
! This implementation of "Dodgems" uses a brute-force strategy: it encodes
! the result for every position (the table is copied from "Winning Ways",
! page 686).  The table takes up about half a kilobyte, so this approach
! doesn't seem too wasteful.
!
! The array `DodgemsPositions' is a 45 by 45 array of 2-bit outcomes:
!
!     0 = not possible or win for player
!     1 = win for first player
!     2 = win for second player
!     3 = win for Catharine
!
! Each row is padded out to 48 so that it will fit in 6 words.  Catharine's
! position is looked up in the columns, the players position in the rows.
!
! The `CLookup' and `PLookup' arrays translate from the position of one
! player's two pieces (encoded as a number from 0 to 99) to rows and
! columns, respectively, in the `DodgemsPositions' table.
!
! Catharine's strategy when she can't win is to choose the move that
! maximises the chance that the player will make a fatal blunder on his or
! her next move.  I'm not altogether happy with the way this works: for one
! thing, it makes her strategy too deterministic, and it misses flaws that
! are beyond this one-move horizon.  An alternative would be to move
! randomly, but with a bias towards the move selected by this strategy.
!
! If you compile with `Constant DEBUG' at the top of the source, and turn
! tracing on with `trace 1', then some information about Catharine's
! strategy is printed.
!--------------------------------------------------------------------------

Array DodgemsPositions --> [;
   $0000 $0000 $0000 $0000 $0000 $0500;
   $0000 $0000 $0000 $0000 $0000 $0540;
   $0000 $0000 $0000 $0000 $0000 $0540;
   $0000 $0000 $0000 $0000 $0051 $0f00;
   $0000 $0000 $0000 $0000 $0051 $0f00;
   $0000 $0000 $0000 $0000 $0051 $5fc0;
   $0000 $0000 $0001 $0010 $1644 $ccc0;
   $0000 $0000 $0000 $0410 $414d $5fc0;
   $0000 $0000 $1041 $0800 $0055 $5fc0;
   $0000 $0000 $0000 $4510 $00d8 $0c00;
   $0000 $0000 $0000 $4104 $00c7 $0f00;
   $0000 $0000 $0000 $4145 $00f7 $0f00;
   $0000 $0000 $0014 $4100 $3cd0 $ccc0;
   $0000 $0000 $0000 $4104 $10c3 $ffc0;
   $0000 $0000 $0000 $4545 $14f3 $ffc0;
   $0000 $0014 $0014 $4110 $3dd4 $ccc0;
   $0000 $0010 $0000 $4134 $d3cf $ffc0;
   $0000 $0000 $0000 $4155 $55ff $ffc0;
   $0001 $4114 $07d7 $4407 $fd9c $33c0;
   $0407 $c504 $5145 $1cf3 $cf3f $ffc0;
   $0c13 $4d00 $f3cf $3555 $55ff $ffc0;
   $0001 $0044 $0740 $100f $c074 $0300;
   $0000 $0000 $4100 $14d3 $403f $0f00;
   $0041 $0040 $d340 $37df $40ff $0f00;
   $0001 $0040 $075d $100f $3c70 $33c0;
   $0000 $0000 $4104 $10c3 $0c33 $ffc0;
   $0041 $0040 $d34d $37cf $3cf3 $ffc0;
   $0001 $147c $071c $100f $ff7c $33c0;
   $0000 $040c $4104 $10f3 $cf3f $ffc0;
   $0041 $0c54 $c30c $33ff $ffff $ffc0;
   $0000 $1411 $003d $d710 $33cc $ccc0;
   $0000 $1115 $0033 $c750 $3ffc $ccc0;
   $0010 $1231 $1451 $cf3c $f3cf $ffc0;
   $0044 $559c $003f $5400 $3ffc $00c0;
   $0000 $1504 $503f $1cf0 $0f3c $ccc0;
   $00d1 $0554 $d00f $3ff0 $3ffc $ccc0;
   $1410 $3d30 $0fff $440c $f3cc $33c0;
   $0400 $1f01 $d75d $0430 $c30f $ffc0;
   $04d0 $0d30 $f3cf $1f3c $f3cf $ffc0;
   $0014 $537c $0cf3 $4c0f $fffc $33c0;
   $0000 $c10d $34d3 $ccf3 $cf3f $ffc0;
   $0030 $c3fc $30c3 $0fff $ffff $ffc0;
   $1405 $7f0c $0fff $1c03 $cf3c $33c0;
   $04f3 $4ffc $03cf $3c0f $fffc $33c0;
   $0c03 $cf0c $f3cf $3cf3 $cf3f $ffc0;
];

Array CLookup ->
   99 26 38 44 25 37 43 24 36 42 26 99 11 23  2  8 20  1  5 17
   38 11 99 35 10 14 32  9 13 29 44 23 35 99 22 34 41 21 33 40
   25  2 10 22 99  7 19  0  4 16 37  8 14 34  7 99 31  6 12 28
   43 20 32 41 19 31 99 18 30 39 24  1  9 21  0  6 18 99  3 15
   36  5 13 33  4 12 30  3 99 27 42 17 29 40 16 28 39 15 27 99;

Array PLookup ->
   99  2  1  0  8  7  6 20 19 18  2 99  5  4 17 16 15 29 28 27
    1  5 99  3 14 13 12 26 25 24  0  4  3 99 11 10  9 23 22 21
    8 17 14 11 99 32 31 41 40 39  7 16 13 10 32 99 30 38 37 36
    6 15 12  9 31 30 99 35 34 33 20 29 26 23 41 38 35 99 44 43
   19 28 25 22 40 37 34 44 99 42 18 27 24 21 39 36 33 43 42 99;

Array Dodgems -> 12;                 ! three deep stack of board posns
Array DodgemsDisplay -> "12AB";      ! characters to print
Array Directions --> -1 -3 1 3;      ! directions to move pieces
Array DirectionNames --> "left" "up" "right" "down";
Array Mask --> $c000 $3000 $0c00 $0300 $00c0 $0030 $000c $0003;
Array Shift --> $4000 $1000 $0400 $0100 $0040 $0010 $0004 $0001;
Array MoveScore -> 6;

! Find the value of the position in the `DodgemsCopy' array.
[ Value
   a  ! row address, encoded from Catharine's pieces
   b  ! column address, encoded from player's pieces
   c; ! byte from table containing result

   a = CLookup->((Dodgems->0) * 10 + (Dodgems->1));
   b = PLookup->((Dodgems->2) * 10 + (Dodgems->3));
   c = DodgemsPositions-->(b * 6 + (a / 8));
   return ((c & (Mask-->(a % 8))) / (Shift-->(a % 8)));
];

! Manipulate the stack of positions
[ PushPosition i; for (i = 11: i > 3: i--) Dodgems->i = Dodgems->(i - 4); ];
[ PopPosition i; for (i = 0: i < 8: i++) Dodgems->i = Dodgems->(i + 4); ];

Object  DodgemsQuestion "game of dodgems"
with   name "game" "dodgems" "dodgem",
       next DodgemsPaper,
       state 0,
       description "~Dodgems was invented by mathematician Colin Vout,~ \
           says Catharine. ~It's one of the more interesting games that \
           can be played on a noughts and crosses board.~"
           "~I can't offer you much in the way of strategy,~ says \
           Catharine, ~except that it isn't always wise to move a piece \
           off the board when it can be blocking your opponent's pieces \
           instead.~"
           "~A final hint,~ says Catharine. ~The northeast corner of the \
           board is a good square to aim for.~";

Object  DodgemsPaper "piece of paper"
       ! general if seen instructions
with   name "piece" "of" "paper" "pieces",
       puzzle_state 0,
       puzzle_name "Dodgems",
       puzzle_pre DBPaper Gnomon,
       description [ i j k l;
           new_line;
           font off;
           for (i = 0: i < 3: i++) {
               for (j = 0: j < 3: j++) {
                   k = i * 3 + j + 1;
                   spaces 1;
                   for (l = 0: l < 4: l++) {
                       if (k == Dodgems->l) {
                           print char DodgemsDisplay->l;
                           jump FoundPiece;
                       }
                   }
                   spaces 1;
                   .FoundPiece;
                   spaces 1;
                   if (j ~= 2) print "|";
               }
               new_line;
               if (i ~= 2) print "---+---+---^";
           }
           font on;
           if (self hasnt general) {
               give self general;
               "^[To move piece A to the left, type ~move a left~. The \
               allowable directions are ~left~, ~right~ and ~up~.]";
           }
       ],
       before [;
        Take,Remove:
           "~But we're in the middle of a game!~ Catharine exclaims.";
        Invoke:
           move self to Toyshop;
           move DodgemsQuestion to AskQuestions;
           <Reset self>;
           self.puzzle_state = 1;
           Catharine.state = 1;
           score ++;
           "^Catharine open the chest and extracts another piece of \
           paper and four small pieces. The paper has a three-by-three \
           grid of squares on it, like a noughts and crosses \
           game. Catharine places two pieces at the left and two pieces \
           at the bottom.^^~This is a game called Dodgems,~ she \
           says. ~You have the pieces at the bottom, and I have the \
           pieces at the left. You can move your pieces up, left or \
           right, and I can move mine up, down, or right. Your aim is to \
           get both your pieces off the top of the board before I get \
           mine off the right hand side.~";
        Reset:
           Dodgems->0 = 1;
           Dodgems->1 = 4;
           Dodgems->2 = 8;
           Dodgems->3 = 9;
       ];

! returns 1 if play was illegal
! returns 2 if play happened and game continues
! returns 3 if play happened and player won

[ DodgemsPlay
   what   ! the piece being moved (0 to 3) (INPUT)
   dir    ! direction of piece (0 to 3) (INPUT)
   who    ! who's moving the piece (Catharine, player or 0) (INPUT)
   new    ! new position of piece
   i;     ! loop counter

   ! Basic sanity check on input
   if (who ~= player or Catharine or 0 ||
       what < 0 || what > 3 ||
       dir < 0 || dir > 3 ||
       (who == player or 0 && dir == 3) ||
       (who == Catharine && dir == 0))
       "** Error: call to DodgemsPlay with bad arguments **";

   ! Illegal moves:
   switch (who) {
    Catharine:
       if ((dir == 3 && (Dodgems->what) > 6) ||
           (dir == 1 && (Dodgems->what) < 4) || Dodgems->what == 0)
           rtrue;
    0:
       if ((dir == 0 && (Dodgems->what) % 3 == 1) ||
           (dir == 2 && (Dodgems->what) % 3 == 0) || Dodgems->what == 0)
           rtrue;
    player:
       if (dir == 0 && (Dodgems->what) % 3 == 1)
           "~You can't move your pieces left off the edge of the board,~ \
           says Catharine.";
       if (dir == 2 && (Dodgems->what) % 3 == 0)
           "~You can't move your pieces right off the edge of the \
           board,~ says Catharine.";
       if (Dodgems->what == 0)
           "~You can't move a piece any more once it's left the board,~ \
           says Catharine.";
   }

   ! Check for pieces moving off board (legally, that is)
   new = Dodgems->what + Directions-->dir;
   if (dir == 2 && new % 3 == 1) new = 0;
   if (dir == 1 && new < 1) new = 0;

   ! Check for a piece running into another piece
   if (new ~= 0) {
       for (i = 0: i < 4: i++) {
           if (Dodgems->i == new) {
               if (who == Catharine or 0) rtrue;
               "~You're not allowed to move a piece onto another piece,~ \
               says Catharine. ~There's no capturing in dodgems.~";
           }
       }
   }
   Dodgems->what = new;

   ! Check for a win
   if ((Dodgems->0 == 0 && Dodgems->1 == 0) ||
       (Dodgems->2 == 0 && Dodgems->3 == 0))
       return 3;
   return 2;
];

[ DodgemsPlayer
   what     ! which piece (2 or 3) (INPUT)
   dir      ! which direction to move it in (INPUT)
   a        ! count of available moves for Catharine
   b        ! count of available moves which win for her
   c        ! another count of moves which win
   move     ! the move selected
   s        ! number of opportunities for player to make a mistake
   max      ! best chance for player to make a mistake
   n        ! number of moves with the maximum score
   r        ! result of playing a move
   i j k l; ! loop counters

   switch (DodgemsPlay(what,dir,player)) {
    1: rtrue;
    2: ;
    3: .DodgemsPlayerWins;
       DodgemsPaper.puzzle_state = 3;
       Catharine.state = 0;
       score ++;
       remove DodgemsPaper;
       "^~You win!~ says Catharine. ~A splendid performance.~ She clears \
       away the paper and the pieces.";
   }

   ! Count available moves and available winnning moves; work out scores
   ! for moves that don't win.
   .DodgemStart;
   for (i = 0: i < 6: i++)
       MoveScore->i = 0;
   for (n = 0, max = 0, a = 0, b = 0, i = 0: i < 2: i++) {
       for (j = 1: j < 4: j++) {
           if (parser_trace >= 1)
               print "Catharine considering moving ", i+1, " ",
                   (string)DirectionNames-->j,"wards.^";
           PushPosition();
           r = DodgemsPlay(i, j, Catharine);
           if (r > 1) {
               a++;
               if (r == 3 || Value() >= 2) {
                   ! The move was winning; put 99 in the MoveScore array.
                   if (parser_trace >= 1)
                       print "  Move is winning^";
                   b++;
                   MoveScore->(i*3+j-1) = 99;
               }
               else {
                   ! The move was not winning, so count up how many of the
                   ! followups are losing.
                   if (parser_trace >= 1)
                       print "  Move is losing, considering responses:^";
                   for (s = 1, k = 2: k < 4: k++) {
                       for (l = 0: l < 3: l++) {
                           PushPosition();
                           r = DodgemsPlay(k, l, 0);
                           if (r == 2 && Value() == 1 or 3) {
                               s ++;
                               if (parser_trace >= 1)
                                   print "    losing response: ", s, ": ",
                                       char DodgemsDisplay->k, " ",
                                       (string) DirectionNames-->l,
                                       "wards^";
                           }
                           PopPosition();
                       }
                   }
                   MoveScore->(i*3+j-1) = s;
                   if (s == max) n ++;
                   if (s > max) {
                       n = 1;
                       max = s;
                   }
               }
           }
           PopPosition();
       }
   }

   if (parser_trace >= 1) {
       print "Result: MoveScores looks like: ";
       for (i = 0: i < 6: i++)
           print MoveScore->i, ", ";
       new_line;
   }

   ! It's possible that Catharine has no moves available
   if (a == 0) {
       print "~I'm stuck,~ says Catharine. ~Which means that you \
           lose.~^";
       r = 3;
       jump DodgemsDisplayBoard;
   }

   ! If there were any winning moves, pick one at random.  Otherwise,
   ! choose randomly among the moves that maximised the player's chance of
   ! making a mistake
   if (b > 0) {
       move = random(b);
       max = 99;
   } else {
       if (n >= 1)
           move = random(n);
       else
           move = -1; ! should never happen
   }
   for (c = 0, i = 0: i < 2: i++) {
       for (j = 1: j < 4: j++) {
           if (MoveScore->(i*3+j-1) == max)
               c ++;
           if (c == move) {
               if (parser_trace >= 1)
                   print "[Picked i = ", i, "; j = ", j, "]^";
               r = DodgemsPlay(i, j, Catharine);
               jump DodgemsDoneMove;
           }
       }
   }

   ! This should never happen:
   "** Error: unable to find a move for Catharine **";

   .DodgemsDoneMove;
   print "Catharine moves piece ", i + 1, " ", (string) DirectionNames-->j,
       "wards";
   if (Dodgems->i == 0)
       print " off the board";
   print ".^";

   .DodgemsDisplayBoard;
   <Examine DodgemsPaper>;
   if (r == 3) {
       <Reset DodgemsPaper>;
       if (DodgemsPaper.puzzle_state == 1)
           DodgemsPaper.puzzle_state = 2;
       print "^~I win!~ she says. ~However, I am feeling generous today \
           and I shall give you another chance.~ She rearranges the \
           pieces to their starting positions.^";
       <Examine DodgemsPaper>;
   }

   ! Check that player has a move
   for (i = 2: i < 4: i++) {
       for (j = 0: j < 3: j++) {
           PushPosition();
           r = DodgemsPlay(i,j,0);
           PopPosition();
           if (r > 1) jump DodgemEnd;
       }
   }

   ! If player has no move, then they win (this shouldn't happen with
   ! Catharine's strategy, but better safe than sorry).
   print "^~Oops. I appear to have boxed you in,~ she says.^";
   jump DodgemsPlayerWins;

   .DodgemEnd;
   "^~Your move,~ she says.";
];

[ DALeftSub; DodgemsPlayer(2,0); ];
[ DBLeftSub; DodgemsPlayer(3,0); ];
[ DARightSub; DodgemsPlayer(2,2); ];
[ DBRightSub; DodgemsPlayer(3,2); ];
[ DAUpSub; DodgemsPlayer(2,1); ];
[ DBUpSub; DodgemsPlayer(3,1); ];
[ DDownSub;
   "~You're not allowed to move your pieces downwards,~ says Catharine.";
];

Extend "move" first
   * "a" "left" -> DALeft
   * "b" "left" -> DBLeft
   * "a" "right" -> DARight
   * "b" "right" -> DBRight
   * "a" "up" -> DAUp
   * "b" "up" -> DBUp
   * "a" "down" -> DDown
   * "b" "down" -> DDown;


!--------------------------------------------------------------------------
! 2.9 THE INFERNAL MACHINE
!--------------------------------------------------------------------------
! The idea for this puzzle is shamelessly stolen from Raymond Smullyan's
! "Monte Carlo Lock" puzzle in his book "The Lady or the Tiger?"  That book
! has a machine which operated on strings of digits by the following rules:
!
!     2X2 -> X
!     if X -> Y then 3X -> 2Y
!     if X -> Y then 4X -> YY
!     if X -> Y then 5X -> the reverse of Y
!
! The puzzle was to find a string of digits that produced itself.  Here, we
! go for a more complicated system, based on Smullyan's.  People who have
! read the Smullyan book will find it an interesting, but not difficult,
! challenge.  People who haven't will have to resort to asking Catharine
! for advice.  The rules are:
!
!     1X -> X + 1
!     if X -> Y then 2X -> Y with first character chopped off
!     if X -> Y then 3X -> 1Y
!     if X -> Y then 4X -> YY
!     if X -> Y then 5X -> the reverse of Y
!
! I believe that the shortest self-replicating string is 543251543251, but
! I would be glad to be proved wrong.  (I decided that the simpler machine
! in which 1X generates X was too easy, since 4141 was self-replicating).
!--------------------------------------------------------------------------

Constant MAX_OUTPUT 200;             ! max number characters in output
Global input_n = 0;                  ! number of characters in input.
Array input_string -> 20;            ! stores strings of digits from 0 to 9.
Global output_n = 0;                 ! number of characters in output
Array output_string -> MAX_OUTPUT;   ! what comes out of the machine.

Object  ChestQuestion "oak chest"
with   name "oak" "oaken" "panel" "screen" "keypad" "chest" "code"
           "combination" "lock",
       article "an",
       next PuzzleChest,
       state 0,
       description "~The first rule,~ says Catharine, ~is that the \
           number 1X - by which I mean the number consisting of 1 \
           followed by the string of digits denoted by the letter X, not \
           1 multiplied by X - generates the number X plus 1. For \
           example, the number 12345 generates the number 2346.~"
           "~The second rule,~ says Catharine, ~is that if the number X \
           generates the number Y, then the number 2X generates the \
           number Y, but with its first digit removed. For example, \
           since we know that 123 generates 24, then 2123 generates 4.~"
           "~The third rule,~ says Catharine, ~is that if X generates Y, \
           then 3X generates 1Y, for example, since 123 generates 24, \
           then 3123 generates 124.~"
           "~The fourth rule,~ says Catharine, ~is that if X generates \
           Y, then 4X generates YY, that is, Y repeated. For example, \
           since 123 generates 24, then 4123 generates 2424.~"
           "~The fifth and final rule,~ says Catharine, ~is that if X \
           generates Y, the 5X generates the reversal of Y. For example, \
           since 1234 generates 235, then 51234 generates 532.~";

Object  PuzzleChest "oak chest"
has    static openable lockable locked
       ! general if read instructions
with   name "chest" "screen" "keypad" "oak" "oaken",
       article "an",
       puzzle_name "The infernal machine",
       puzzle_state 0,
       puzzle_pre PuzzleChest, ! make sure it doesn't happen too soon
       state 0, ! 0 = in input mode
                ! 1 = displaying output
       capacity 100,
       name "oak" "chest" "oaken",
       article "an",
       describe [;
           if (self has open) print "^An oak chest stands open";
           else print "^There's a closed oak chest";
           print_ret " in the ", (DirectionName) ToyChest.state, " corner.";
       ],
       description [;
           print "There's a little screen and keypad on the side of the \
               chest. ";
           <Display self>;
           if (self hasnt general) {
               give self general;
               "^[Type for example ~press 1~ to press the button marked \
               ~1~. Type ~press send~ to press the ~send~ button. Use \
               for example ~type 123~ to type a sequence of digits and \
               then press ~send~.]";
           }
           rtrue;
       ],
       before [ i;
        Display:
           print "The screen reads ~";
           switch (self.state) {
            0: print "Input: ";
               if (input_n == 0) print "none";
               for (i = 0: i < input_n: i++)
                   print input_string->i;
            1: print "Output: ";
               if (output_n == 0) print "none";
               for (i = 0: i < output_n: i++)
                   print output_string->i;
           }
           print "~.^";
        Enter:
           if (self hasnt open) rfalse;
           if (score >= 18)
               score = 20;
           deadflag = 2;
           "You climb into the chest, which turns out to be full of \
           drapery: large engulfing folds of cloth. You struggle to be \
           free of them, and pulling the last one aside, you find \
           yourself walking down a Victorian arcade. Looking around, you \
           see no sign of the toyshop, but clutched in your hands, you \
           find a wrapped parcel with a tag saying ~To Isabelle, on her \
           birthday.~ You are unsure of how you acquired it, but you are \
           somehow sure that Isabelle will enjoy it...";
        Lock, Unlock:
           if (second ~= BrassKey) rfalse;
           "Nothing happens.";
        Invoke:
           remove ToyChest;
           move self to Toyshop;
           move ChestQuestion to AskQuestions;
           self.puzzle_state = 1;
           score ++;
           "You turn the brass key in the lock, and with a click, a \
           panel in the side of the chest slides away, revealing a \
           little LCD screen and a keypad with six keys: ~1~ to ~5~ and \
           ~send~.^^~Aha!~ says Catharine, ~You've found the combination \
           lock to the chest. You probably won't get very far with it \
           unless I give you some hints, so here goes.^^~Some \
           combinations are said to generate other combinations \
           according to certain rules. When you enter a combination, the \
           chest either beeps, or displays the generated combination on \
           the screen. If a combination generates itself, then the chest \
           opens.~";
       ];

! returns 1 if unsuccessful
! returns 0 if successful

[ InputNumberSub
   nodisp;   ! 1 if the screen shouldn't be redisplayed (INPUT)

   if (PuzzleChest notin Toyshop)
       "You can't see any such thing.";
   if (special_number < 1 || special_number > 5)
       "The keys are ~1~ to ~5~ and ~send~.";
   if (input_n >= 20)
       "The chest beeps, but your digit doesn't appear on \
       screen. Perhaps you've reached the limit on the size of input.";
   input_string->input_n = special_number;
   input_n ++;
   PuzzleChest.state = 0;
   if (nodisp == 0)
       <Display PuzzleChest>;
   rfalse;
];

[ InputSendSub i;
   if (PuzzleChest notin Toyshop)
       "You can't see any such thing.";
   PuzzleChest.state = 1;
   if (PuzzleChest.puzzle_state == 1)
       PuzzleChest.puzzle_state = 2;
   switch(Decode(input_string,input_n)) {
    0: output_n = 0;
       input_n = 0;
       print "The chest beeps at you. ";
       <Display PuzzleChest>;
       rtrue;
    1: <Display PuzzleChest>;
       i = Compare();
       if (i == 0) {
           if (PuzzleChest hasnt open)
               print "^There's a series of clicks from somewhere inside \
                   the chest, and the lid pops open.^";
           if (PuzzleChest.puzzle_state < 3) {
               PuzzleChest.puzzle_state = 3;
               score ++;
           }
           give PuzzleChest open ~locked enterable container;
       }
       input_n = 0;
       rtrue;
   }
];

! Returns -1 if input less than output
! Returns 0 if input equals output
! Returns 1 if input greater than output
[ Compare i;
   if (output_n < input_n) return 1;
   if (input_n < output_n) return -1;
   for (i = 0: i < input_n: i++) {
       if (output_string->i < input_string->i) return 1;
       if (input_string->i < output_string->i) return -1;
   }
   return 0;
];

! Decodes from the array p to the output_string
! returns 1 if decoding was successful.
! returns 0 if it failed.
[ Decode
   p    ! pointer to a string to decode (INPUT)
   n    ! number of characters in the string (INPUT)
   i j; ! loop counters & miscellaneous

   if (n < 2) rfalse;
   switch (p->0) {
    1: ! copy the remainder of the input string, and add 1 to final digit
       ! (we know there will be no carry, since the maximum digit is 5).
       for (i = 1: i < n - 1: i++)
           output_string->(i-1) = p->i;
       output_string->(n-2) = p->(n-1) + 1;
       output_n = n - 1;
       rtrue;

    2: ! decode the remainder, chop the first character
       if (Decode(p+1,n-1) == 0) rfalse;
       if (output_n <= 0) rfalse;
       for (i = 1: i < output_n: i++)
           output_string->(i-1) = output_string->i;
       output_n --;
       rtrue;

    3: ! decode the remainder; put a 1 in front
       if (Decode(p+1,n-1) == 0) rfalse;
       if (output_n >= MAX_OUTPUT) rfalse;
       for (i = output_n: i > 0: i--)
           output_string->i = output_string->(i - 1);
       output_string->0 = 1;
       output_n ++;
       rtrue;

    4: ! decode the remainder, duplicate it
       if (Decode(p+1,n-1) == 0) rfalse;
       if (output_n > MAX_OUTPUT / 2) rfalse;
       for (i = 0: i < output_n: i++)
           output_string->(i+output_n) = output_string->i;
       output_n = output_n * 2;
       rtrue;

    5: ! decode the remainder, reverse it
       if (Decode(p+1,n-1) == 0) rfalse;
       for (i = 0: i < output_n / 2: i++) {
           j = output_string->i;
           output_string->i = output_string->(output_n-i-1);
           output_string->(output_n-i-1) = j;
       }
       rtrue;

    default: rfalse;
   }
];

[ TypeInputSub
   loc     ! location of word in parse table
   point   ! location of word in text buffer
   length  ! length of the word
   l       ! letter in word
   i;      ! loop counter

   if (PuzzleChest notin Toyshop)
       "You can't see anything to type on.";
   if (consult_words > 1)
       "Use for example ~type 123~ to type a string of digits and press \
       ~send~.";

   ! Check the input consists only of numbers from 1 to 5.
   loc = consult_from * 4 + 1;
   point = buffer+(parse->loc);
   length = parse->(loc-1);

   for (i = 0: i < length: i++) {
       l = point->i;
       if (l < '1' || l > '5')
           "The only numbers on the keypad are ~1~ to ~5~.";
   }

   for (i = 0: i < length: i++) {
       special_number = point->i - '0';
       if (InputNumberSub(1) == 1)
           jump PressSend;
   }

   .PressSend;
   InputSendSub();
];

Extend "press"
   * number -> InputNumber
   * "send" -> InputSend;

Verb "type"
   * ConTopic -> TypeInput;

End;