!-------------------------------------------------------------------------- ! 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 , 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; <>; ]; [ NoQuestionSub; <>; ]; [ 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; <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: <>; ], 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; <>; ], 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.^"; <>; ], 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; ; 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: ; 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: ; 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), ".^"; ; 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; ; OXBoard->4 = 2; ; "^~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) <>; 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) <>; 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.^"; ; ; ; 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; ; 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) { ; 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) <>; ]; 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) <>; ], 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) <>; ]; 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; ; 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: ; "^~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; ; 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) { ; 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: ; "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) { ; "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; <>; ]; 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; ; 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; ; if (r == 3) { ; 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.^"; ; } ! 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. "; ; 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) ; 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. "; ; rtrue; 1: ; 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;