J’accuse

Tags: huge, game, humour, puzzle

You are the world-famous detective Mathilde Camus. Zophie the cat has gone missing, and you must sift through the clues. Suspects either always tell lies or always tell the truth. Will you find Zophie the cat in time and accuse the guilty party?

In this game, you take a taxi to different locations around the city. At each location is a suspect and an item. You can ask suspects about other suspects and items, compare their answers with your own exploration notes, and determine if they are lying or telling the truth. Some will know who has catnapped Zophie (or where she is, or what item is found at the location of the kidnapper), but you must determine if you can believe them. You have five minutes to find the criminal but will lose if you make three wrong accusations. This game is inspired by Homestar Runner’s “Where’s an Egg?” game.

To fully understand this program, you should pay close attention to the clues dictionary, which is set up on lines 51 to 109. You can uncomment lines 151 to 154 to display it on the screen. This dictionary has strings from the SUSPECTS list for the keys and “clue dictionaries” for the values. Each of these clue dictionaries contains strings from SUSPECTS and ITEMS. The original suspect will answer with these strings when asked about another suspect or item. For example, if clues[‘DUKE HAUTDOG’][‘CANDLESTICK’] is set to ‘DUCK POND’, then when the player asks Duke Hautdog about the Candlestick, they’ll say it is at the Duck Pond. The suspects, items, locations, and culprit get shuffled each time the game is played.

The code for this program revolves around this data structure, so understanding it is necessary to unlocking your understanding of the rest of the program.

jaccuse.py
  1"""J'ACCUSE!, by Al Sweigart al@inventwithpython.com
  2A mystery game of intrigue and a missing cat.
  3This code is available at https://nostarch.com/big-book-small-python-programming
  4Tags: extra-large, game, humor, puzzle"""
  5
  6# Play the original Flash game at:
  7# https://homestarrunner.com/videlectrix/wheresanegg.html
  8# More info at: http://www.hrwiki.org/wiki/Where's_an_Egg%3F
  9
 10import time, random, sys
 11
 12# Set up the constants:
 13SUSPECTS = ['DUKE HAUTDOG', 'MAXIMUM POWERS', 'BILL MONOPOLIS', 'SENATOR SCHMEAR', 'MRS. FEATHERTOSS', 'DR. JEAN SPLICER', 'RAFFLES THE CLOWN', 'ESPRESSA TOFFEEPOT', 'CECIL EDGAR VANDERTON']
 14ITEMS = ['FLASHLIGHT', 'CANDLESTICK', 'RAINBOW FLAG', 'HAMSTER WHEEL', 'ANIME VHS TAPE', 'JAR OF PICKLES', 'ONE COWBOY BOOT', 'CLEAN UNDERPANTS', '5 DOLLAR GIFT CARD']
 15PLACES = ['ZOO', 'OLD BARN', 'DUCK POND', 'CITY HALL', 'HIPSTER CAFE', 'BOWLING ALLEY', 'VIDEO GAME MUSEUM', 'UNIVERSITY LIBRARY', 'ALBINO ALLIGATOR PIT']
 16TIME_TO_SOLVE = 300  # 300 seconds (5 minutes) to solve the game.
 17
 18# First letters and longest length of places are needed for menu display:
 19PLACE_FIRST_LETTERS = {}
 20LONGEST_PLACE_NAME_LENGTH = 0
 21for place in PLACES:
 22    PLACE_FIRST_LETTERS[place[0]] = place
 23    if len(place) > LONGEST_PLACE_NAME_LENGTH:
 24        LONGEST_PLACE_NAME_LENGTH = len(place)
 25
 26# Basic sanity checks of the constants:
 27assert len(SUSPECTS) == 9
 28assert len(ITEMS) == 9
 29assert len(PLACES) == 9
 30# First letters must be unique:
 31assert len(PLACE_FIRST_LETTERS.keys()) == len(PLACES)
 32
 33
 34knownSuspectsAndItems = []
 35# visitedPlaces: Keys=places, values=strings of the suspect & item there.
 36visitedPlaces = {}
 37currentLocation = 'TAXI'  # Start the game at the taxi.
 38accusedSuspects = []  # Accused suspects won't offer clues.
 39liars = random.sample(SUSPECTS, random.randint(3, 4))
 40accusationsLeft = 3  # You can accuse up to 3 people.
 41culprit = random.choice(SUSPECTS)
 42
 43# Common indexes link these; e.g. SUSPECTS[0] and ITEMS[0] are at PLACES[0].
 44random.shuffle(SUSPECTS)
 45random.shuffle(ITEMS)
 46random.shuffle(PLACES)
 47
 48# Create data structures for clues the truth-tellers give about each
 49# item and suspect.
 50# clues: Keys=suspects being asked for a clue, value="clue dictionary".
 51clues = {}
 52for i, interviewee in enumerate(SUSPECTS):
 53    if interviewee in liars:
 54        continue  # Skip the liars for now.
 55
 56    # This "clue dictionary" has keys=items & suspects,
 57    # value=the clue given.
 58    clues[interviewee] = {}
 59    clues[interviewee]['debug_liar'] = False  # Useful for debugging.
 60    for item in ITEMS:  # Select clue about each item.
 61        if random.randint(0, 1) == 0:  # Tells where the item is:
 62            clues[interviewee][item] = PLACES[ITEMS.index(item)]
 63        else:  # Tells who has the item:
 64            clues[interviewee][item] = SUSPECTS[ITEMS.index(item)]
 65    for suspect in SUSPECTS:  # Select clue about each suspect.
 66        if random.randint(0, 1) == 0:  # Tells where the suspect is:
 67            clues[interviewee][suspect] = PLACES[SUSPECTS.index(suspect)]
 68        else:  # Tells what item the suspect has:
 69            clues[interviewee][suspect] = ITEMS[SUSPECTS.index(suspect)]
 70
 71# Create data structures for clues the liars give about each item
 72# and suspect:
 73for i, interviewee in enumerate(SUSPECTS):
 74    if interviewee not in liars:
 75        continue  # We've already handled the truth-tellers.
 76
 77    # This "clue dictionary" has keys=items & suspects,
 78    # value=the clue given:
 79    clues[interviewee] = {}
 80    clues[interviewee]['debug_liar'] = True  # Useful for debugging.
 81
 82    # This interviewee is a liar and gives wrong clues:
 83    for item in ITEMS:
 84        if random.randint(0, 1) == 0:
 85            while True:  # Select a random (wrong) place clue.
 86                # Lies about where the item is.
 87                clues[interviewee][item] = random.choice(PLACES)
 88                if clues[interviewee][item] != PLACES[ITEMS.index(item)]:
 89                    # Break out of the loop when wrong clue is selected.
 90                    break
 91        else:
 92            while True:  # Select a random (wrong) suspect clue.
 93                clues[interviewee][item] = random.choice(SUSPECTS)
 94                if clues[interviewee][item] != SUSPECTS[ITEMS.index(item)]:
 95                    # Break out of the loop when wrong clue is selected.
 96                    break
 97    for suspect in SUSPECTS:
 98        if random.randint(0, 1) == 0:
 99            while True:  # Select a random (wrong) place clue.
100                clues[interviewee][suspect] = random.choice(PLACES)
101                if clues[interviewee][suspect] != PLACES[ITEMS.index(item)]:
102                    # Break out of the loop when wrong clue is selected.
103                    break
104        else:
105            while True:  # Select a random (wrong) item clue.
106                clues[interviewee][suspect] = random.choice(ITEMS)
107                if clues[interviewee][suspect] != ITEMS[SUSPECTS.index(suspect)]:
108                    # Break out of the loop when wrong clue is selected.
109                    break
110
111# Create the data structures for clues given when asked about Zophie:
112zophieClues = {}
113for interviewee in random.sample(SUSPECTS, random.randint(3, 4)):
114    kindOfClue = random.randint(1, 3)
115    if kindOfClue == 1:
116        if interviewee not in liars:
117            # They tell you who has Zophie.
118            zophieClues[interviewee] = culprit
119        elif interviewee in liars:
120            while True:
121                # Select a (wrong) suspect clue.
122                zophieClues[interviewee] = random.choice(SUSPECTS)
123                if zophieClues[interviewee] != culprit:
124                    # Break out of the loop when wrong clue is selected.
125                    break
126
127    elif kindOfClue == 2:
128        if interviewee not in liars:
129            # They tell you where Zophie is.
130            zophieClues[interviewee] = PLACES[SUSPECTS.index(culprit)]
131        elif interviewee in liars:
132            while True:
133                # Select a (wrong) place clue.
134                zophieClues[interviewee] = random.choice(PLACES)
135                if zophieClues[interviewee] != PLACES[SUSPECTS.index(culprit)]:
136                    # Break out of the loop when wrong clue is selected.
137                    break
138    elif kindOfClue == 3:
139        if interviewee not in liars:
140            # They tell you what item Zophie is near.
141            zophieClues[interviewee] = ITEMS[SUSPECTS.index(culprit)]
142        elif interviewee in liars:
143            while True:
144                # Select a (wrong) item clue.
145                zophieClues[interviewee] = random.choice(ITEMS)
146                if zophieClues[interviewee] != ITEMS[SUSPECTS.index(culprit)]:
147                    # Break out of the loop when wrong clue is selected.
148                    break
149
150# EXPERIMENT: Uncomment this code to view the clue data structures:
151#import pprint
152#pprint.pprint(clues)
153#pprint.pprint(zophieClues)
154#print('culprit =', culprit)
155
156# START OF THE GAME
157print("""J'ACCUSE! (a mystery game)")
158By Al Sweigart al@inventwithpython.com
159Inspired by Homestar Runner\'s "Where\'s an Egg?" game
160
161You are the world-famous detective, Mathilde Camus.
162ZOPHIE THE CAT has gone missing, and you must sift through the clues.
163Suspects either always tell lies, or always tell the truth. Ask them
164about other people, places, and items to see if the details they give are
165truthful and consistent with your observations. Then you will know if
166their clue about ZOPHIE THE CAT is true or not. Will you find ZOPHIE THE
167CAT in time and accuse the guilty party?
168""")
169input('Press Enter to begin...')
170
171
172startTime = time.time()
173endTime = startTime + TIME_TO_SOLVE
174
175while True:  # Main game loop.
176    if time.time() > endTime or accusationsLeft == 0:
177        # Handle "game over" condition:
178        if time.time() > endTime:
179            print('You have run out of time!')
180        elif accusationsLeft == 0:
181            print('You have accused too many innocent people!')
182        culpritIndex = SUSPECTS.index(culprit)
183        print('It was {} at the {} with the {} who catnapped her!'.format(culprit, PLACES[culpritIndex], ITEMS[culpritIndex]))
184        print('Better luck next time, Detective.')
185        sys.exit()
186
187    print()
188    minutesLeft = int(endTime - time.time()) // 60
189    secondsLeft = int(endTime - time.time()) % 60
190    print('Time left: {} min, {} sec'.format(minutesLeft, secondsLeft))
191
192    if currentLocation == 'TAXI':
193        print('  You are in your TAXI. Where do you want to go?')
194        for place in sorted(PLACES):
195            placeInfo = ''
196            if place in visitedPlaces:
197                placeInfo = visitedPlaces[place]
198            nameLabel = '(' + place[0] + ')' + place[1:]
199            spacing = " " * (LONGEST_PLACE_NAME_LENGTH - len(place))
200            print('{} {}{}'.format(nameLabel, spacing, placeInfo))
201        print('(Q)UIT GAME')
202        while True:  # Keep asking until a valid response is given.
203            response = input('> ').upper()
204            if response == '':
205                continue  # Ask again.
206            if response == 'Q':
207                print('Thanks for playing!')
208                sys.exit()
209            if response in PLACE_FIRST_LETTERS.keys():
210                break
211        currentLocation = PLACE_FIRST_LETTERS[response]
212        continue  # Go back to the start of the main game loop.
213
214    # At a place; player can ask for clues.
215    print('  You are at the {}.'.format(currentLocation))
216    currentLocationIndex = PLACES.index(currentLocation)
217    thePersonHere = SUSPECTS[currentLocationIndex]
218    theItemHere = ITEMS[currentLocationIndex]
219    print('  {} with the {} is here.'.format(thePersonHere, theItemHere))
220
221    # Add the suspect and item at this place to our list of known
222    # suspects and items:
223    if thePersonHere not in knownSuspectsAndItems:
224        knownSuspectsAndItems.append(thePersonHere)
225    if ITEMS[currentLocationIndex] not in knownSuspectsAndItems:
226        knownSuspectsAndItems.append(ITEMS[currentLocationIndex])
227    if currentLocation not in visitedPlaces.keys():
228        visitedPlaces[currentLocation] = '({}, {})'.format(thePersonHere.lower(), theItemHere.lower())
229
230    # If the player has accused this person wrongly before, they
231    # won't give clues:
232    if thePersonHere in accusedSuspects:
233        print('They are offended that you accused them,')
234        print('and will not help with your investigation.')
235        print('You go back to your TAXI.')
236        print()
237        input('Press Enter to continue...')
238        currentLocation = 'TAXI'
239        continue  # Go back to the start of the main game loop.
240
241    # Display menu of known suspects & items to ask about:
242    print()
243    print('(J) "J\'ACCUSE!" ({} accusations left)'.format(accusationsLeft))
244    print('(Z) Ask if they know where ZOPHIE THE CAT is.')
245    print('(T) Go back to the TAXI.')
246    for i, suspectOrItem in enumerate(knownSuspectsAndItems):
247        print('({}) Ask about {}'.format(i + 1, suspectOrItem))
248
249    while True:  # Keep asking until a valid response is given.
250        response = input('> ').upper()
251        if response in 'JZT' or (response.isdecimal() and 0 < int(response) <= len(knownSuspectsAndItems)):
252            break
253
254    if response == 'J':  # Player accuses this suspect.
255        accusationsLeft -= 1  # Use up an accusation.
256        if thePersonHere == culprit:
257            # You've accused the correct suspect.
258            print('You\'ve cracked the case, Detective!')
259            print('It was {} who had catnapped ZOPHIE THE CAT.'.format(culprit))
260            minutesTaken = int(time.time() - startTime) // 60
261            secondsTaken = int(time.time() - startTime) % 60
262            print('Good job! You solved it in {} min, {} sec.'.format(minutesTaken, secondsTaken))
263            sys.exit()
264        else:
265            # You've accused the wrong suspect.
266            accusedSuspects.append(thePersonHere)
267            print('You have accused the wrong person, Detective!')
268            print('They will not help you with anymore clues.')
269            print('You go back to your TAXI.')
270            currentLocation = 'TAXI'
271
272    elif response == 'Z':  # Player asks about Zophie.
273        if thePersonHere not in zophieClues:
274            print('"I don\'t know anything about ZOPHIE THE CAT."')
275        elif thePersonHere in zophieClues:
276            print('  They give you this clue: "{}"'.format(zophieClues[thePersonHere]))
277            # Add non-place clues to the list of known things:
278            if zophieClues[thePersonHere] not in knownSuspectsAndItems and zophieClues[thePersonHere] not in PLACES:
279                knownSuspectsAndItems.append(zophieClues[thePersonHere])
280
281    elif response == 'T':  # Player goes back to the taxi.
282        currentLocation = 'TAXI'
283        continue  # Go back to the start of the main game loop.
284
285    else:  # Player asks about a suspect or item.
286        thingBeingAskedAbout = knownSuspectsAndItems[int(response) - 1]
287        if thingBeingAskedAbout in (thePersonHere, theItemHere):
288            print('  They give you this clue: "No comment."')
289        else:
290            print('  They give you this clue: "{}"'.format(clues[thePersonHere][thingBeingAskedAbout]))
291            # Add non-place clues to the list of known things:
292            if clues[thePersonHere][thingBeingAskedAbout] not in knownSuspectsAndItems and clues[thePersonHere][thingBeingAskedAbout] not in PLACES:
293                knownSuspectsAndItems.append(clues[thePersonHere][thingBeingAskedAbout])
294
295    input('Press Enter to continue...')

https://inventwithpython.com/bigbookpython/project38.html