First bit of the A-level miscellany
[cas-master-teacher-training.git] / hangman / 01-hangman-setter.ipynb
1 {
2 "metadata": {
3 "name": "",
4 "signature": "sha256:374900202f4f7c3f157762c0d012b597cc502efce4de220013e3cc9cd8dfc896"
5 },
6 "nbformat": 3,
7 "nbformat_minor": 0,
8 "worksheets": [
9 {
10 "cells": [
11 {
12 "cell_type": "markdown",
13 "metadata": {},
14 "source": [
15 "# Hangman 1: set a puzzle\n",
16 "\n",
17 "A fairly traditional hangman game. The computer chooses a word, the (human) player has to guess it without making too many wrong guesses. You'll need to find a list of words to chose from and develop a simple UI for the game (e.g. text only: display the target word with underscores and letters, lives left, and maybe incorrect guesses). \n",
18 "\n",
19 "## Data structures\n",
20 "\n",
21 "* What do we need to track?\n",
22 "* What operations do we need to perform on it?\n",
23 "* How to store it?\n",
24 "\n",
25 "## Creating a game\n",
26 "* 'List' of words to choose from\n",
27 " * Pick one at random\n",
28 "\n",
29 "## Game state\n",
30 "\n",
31 "<table>\n",
32 "<tr valign=\"top\">\n",
33 "<th>Data</th>\n",
34 "<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>\n",
35 "<th>Operations</th>\n",
36 "</tr>\n",
37 "<tr valign=\"top\">\n",
38 "<td>\n",
39 "\n",
40 "* Target word\n",
41 "* Discovered letters\n",
42 " * In order in the word\n",
43 "* Lives left\n",
44 "* Wrong letters?\n",
45 "\n",
46 "</td>\n",
47 "<td></td>\n",
48 "<td>\n",
49 "\n",
50 "* Get a guess\n",
51 "* Update discovered letters\n",
52 "* Update lives\n",
53 "* Show discovered word\n",
54 "* Detect game end, report\n",
55 "* Detect game win or loss, report\n",
56 "\n",
57 "</td>\n",
58 "</tr>\n",
59 "</table>"
60 ]
61 },
62 {
63 "cell_type": "code",
64 "collapsed": false,
65 "input": [
66 "import re\n",
67 "import random"
68 ],
69 "language": "python",
70 "metadata": {},
71 "outputs": [],
72 "prompt_number": 3
73 },
74 {
75 "cell_type": "markdown",
76 "metadata": {},
77 "source": [
78 "## Get the words\n",
79 "Read the words the game setter can choose from. The list contains all sorts of hangman-illegal words, such as proper nouns and abbreviations. We'll remove them by taking the pragmatic approach that if a word contains a non-lowercase letter (capital letters, full stops, apostrophes, etc.), it's illegal. "
80 ]
81 },
82 {
83 "cell_type": "code",
84 "collapsed": false,
85 "input": [
86 "# Just use a regex to filter the words. There are other ways to do this, as you know.\n",
87 "WORDS = [w.strip() for w in open('/usr/share/dict/british-english').readlines() \n",
88 " if re.match(r'^[a-z]*$', w.strip())]"
89 ],
90 "language": "python",
91 "metadata": {},
92 "outputs": [],
93 "prompt_number": 4
94 },
95 {
96 "cell_type": "markdown",
97 "metadata": {},
98 "source": [
99 "A few quick looks at the list of words, to make sure it's sensible."
100 ]
101 },
102 {
103 "cell_type": "code",
104 "collapsed": false,
105 "input": [
106 "len(WORDS)"
107 ],
108 "language": "python",
109 "metadata": {},
110 "outputs": [
111 {
112 "metadata": {},
113 "output_type": "pyout",
114 "prompt_number": 5,
115 "text": [
116 "62856"
117 ]
118 }
119 ],
120 "prompt_number": 5
121 },
122 {
123 "cell_type": "code",
124 "collapsed": false,
125 "input": [
126 "WORDS[30000:30010]"
127 ],
128 "language": "python",
129 "metadata": {},
130 "outputs": [
131 {
132 "metadata": {},
133 "output_type": "pyout",
134 "prompt_number": 6,
135 "text": [
136 "['jotted',\n",
137 " 'jotting',\n",
138 " 'jottings',\n",
139 " 'joule',\n",
140 " 'joules',\n",
141 " 'jounce',\n",
142 " 'jounced',\n",
143 " 'jounces',\n",
144 " 'jouncing',\n",
145 " 'journal']"
146 ]
147 }
148 ],
149 "prompt_number": 6
150 },
151 {
152 "cell_type": "markdown",
153 "metadata": {},
154 "source": [
155 "## Constants and game states"
156 ]
157 },
158 {
159 "cell_type": "markdown",
160 "metadata": {},
161 "source": [
162 "## The target word"
163 ]
164 },
165 {
166 "cell_type": "code",
167 "collapsed": false,
168 "input": [
169 "target = random.choice(WORDS)\n",
170 "target"
171 ],
172 "language": "python",
173 "metadata": {},
174 "outputs": [
175 {
176 "metadata": {},
177 "output_type": "pyout",
178 "prompt_number": 7,
179 "text": [
180 "'rocketing'"
181 ]
182 }
183 ],
184 "prompt_number": 7
185 },
186 {
187 "cell_type": "code",
188 "collapsed": false,
189 "input": [
190 "STARTING_LIVES = 10\n",
191 "lives = 0"
192 ],
193 "language": "python",
194 "metadata": {},
195 "outputs": [],
196 "prompt_number": 8
197 },
198 {
199 "cell_type": "code",
200 "collapsed": false,
201 "input": [
202 "wrong_letters = []"
203 ],
204 "language": "python",
205 "metadata": {},
206 "outputs": [],
207 "prompt_number": 9
208 },
209 {
210 "cell_type": "markdown",
211 "metadata": {},
212 "source": [
213 "We'll represent the partially-discovered word as a list of characters. Each character is either the letter in the target (if it's been discovered) or an underscore if it's not.\n",
214 "\n",
215 "Use a `list` as it's a mutable data structure. That means we can change individual elements of a list, which we couldn't if we represented it as a `string`.\n",
216 "\n",
217 "We can also use `discovered` to check of a game win. If `discovered` contains an underscore, the player hasn't won the game."
218 ]
219 },
220 {
221 "cell_type": "code",
222 "collapsed": false,
223 "input": [
224 "discovered = list('_' * len(target))\n",
225 "discovered"
226 ],
227 "language": "python",
228 "metadata": {},
229 "outputs": [
230 {
231 "metadata": {},
232 "output_type": "pyout",
233 "prompt_number": 10,
234 "text": [
235 "['_', '_', '_', '_', '_', '_', '_', '_', '_']"
236 ]
237 }
238 ],
239 "prompt_number": 10
240 },
241 {
242 "cell_type": "markdown",
243 "metadata": {},
244 "source": [
245 "We can use `' '.join(discovered)` to display it nicely."
246 ]
247 },
248 {
249 "cell_type": "code",
250 "collapsed": false,
251 "input": [
252 "' '.join(discovered)"
253 ],
254 "language": "python",
255 "metadata": {},
256 "outputs": [
257 {
258 "metadata": {},
259 "output_type": "pyout",
260 "prompt_number": 11,
261 "text": [
262 "'_ _ _ _ _ _ _ _ _'"
263 ]
264 }
265 ],
266 "prompt_number": 11
267 },
268 {
269 "cell_type": "markdown",
270 "metadata": {},
271 "source": [
272 "## Read a letter\n",
273 "Get rid of fluff and make sure we only get one letter."
274 ]
275 },
276 {
277 "cell_type": "code",
278 "collapsed": false,
279 "input": [
280 "letter = input('Enter letter: ').strip().lower()[0]\n",
281 "letter"
282 ],
283 "language": "python",
284 "metadata": {},
285 "outputs": [
286 {
287 "name": "stdout",
288 "output_type": "stream",
289 "stream": "stdout",
290 "text": [
291 "Enter letter: r\n"
292 ]
293 },
294 {
295 "metadata": {},
296 "output_type": "pyout",
297 "prompt_number": 9,
298 "text": [
299 "'r'"
300 ]
301 }
302 ],
303 "prompt_number": 9
304 },
305 {
306 "cell_type": "markdown",
307 "metadata": {},
308 "source": [
309 "# Finding a letter in a word\n",
310 "One operation we want to to is to update `discovered` when we get a `letter`. The trick is that we want to update every occurrence of the `letter` in `discovered`. \n",
311 "\n",
312 "We'll do it in two phases. In the first phase, we'll find all the occurences of the `letter` in the `target` and return a list of those locations. In the second phase, we'll update all those positions in `discovered` with the `letter`. \n",
313 "\n",
314 "There's probaby a clever way of doing this in one pass, but it's complicated (or at least not obvious), so let's just do it the easy way."
315 ]
316 },
317 {
318 "cell_type": "code",
319 "collapsed": false,
320 "input": [
321 "# Note that find returns either the first occurrence of a substring, or -1 if it doesn't exist.\n",
322 "def find_all_explicit(string, letter):\n",
323 " locations = []\n",
324 " starting=0\n",
325 " location = string.find(letter)\n",
326 " while location > -1:\n",
327 " locations += [location]\n",
328 " starting = location + 1\n",
329 " location = string.find(letter, starting)\n",
330 " return locations"
331 ],
332 "language": "python",
333 "metadata": {},
334 "outputs": [],
335 "prompt_number": 25
336 },
337 {
338 "cell_type": "code",
339 "collapsed": false,
340 "input": [
341 "# A simpler way using a list comprehension and the built-in enumerate()\n",
342 "def find_all(string, letter):\n",
343 " return [p for p, l in enumerate(string) if l == letter]"
344 ],
345 "language": "python",
346 "metadata": {},
347 "outputs": [],
348 "prompt_number": 12
349 },
350 {
351 "cell_type": "code",
352 "collapsed": false,
353 "input": [
354 "find_all('happy', 'p')"
355 ],
356 "language": "python",
357 "metadata": {},
358 "outputs": [
359 {
360 "metadata": {},
361 "output_type": "pyout",
362 "prompt_number": 13,
363 "text": [
364 "[2, 3]"
365 ]
366 }
367 ],
368 "prompt_number": 13
369 },
370 {
371 "cell_type": "markdown",
372 "metadata": {},
373 "source": [
374 "## Updating the discovered word\n",
375 "Now we can update `discovered`. We'll look through the `locations` list and update that element of `discovered`."
376 ]
377 },
378 {
379 "cell_type": "code",
380 "collapsed": false,
381 "input": [
382 "guessed_letter = 'e'\n",
383 "locations = find_all(target, guessed_letter)\n",
384 "for location in locations:\n",
385 " discovered[location] = guessed_letter\n",
386 "discovered"
387 ],
388 "language": "python",
389 "metadata": {},
390 "outputs": [
391 {
392 "metadata": {},
393 "output_type": "pyout",
394 "prompt_number": 15,
395 "text": [
396 "['_', '_', '_', '_', 'e', '_', '_', '_', '_']"
397 ]
398 }
399 ],
400 "prompt_number": 15
401 },
402 {
403 "cell_type": "markdown",
404 "metadata": {},
405 "source": [
406 "## Putting it all together\n",
407 "We've not got quite a few bits and pieces of how the game should work. Now it's time to start putting them together into the game.\n",
408 "\n",
409 "```\n",
410 "to play a game:\n",
411 " initialise lives, target, etc.\n",
412 " finished? = False\n",
413 " handle a turn\n",
414 " while not finished?:\n",
415 " if guessed all letters:\n",
416 " report success\n",
417 " finished? = True\n",
418 " elif run out of lives:\n",
419 " report failure\n",
420 " finished? = True\n",
421 " else:\n",
422 " handle a turn\n",
423 "```\n",
424 "\n",
425 "```\n",
426 "to handle a turn:\n",
427 " print the challenge\n",
428 " get the letter\n",
429 " if letter in target:\n",
430 " update discovered\n",
431 " else:\n",
432 " update lives, wrong_letters\n",
433 "```\n",
434 "\n",
435 "```\n",
436 "to update discovered:\n",
437 " find all the locations of letter in target\n",
438 " replace the appropriate underscores in discovered\n",
439 "```\n",
440 "\n",
441 "We pretty much know how to do all these bits, so let's just start at the bottom and off we go!\n",
442 "\n",
443 "###Syntax note\n",
444 "Note the use of Python's `global` statement, to tell Python that we're updating the global variables in each procedure. We'll fix that later."
445 ]
446 },
447 {
448 "cell_type": "code",
449 "collapsed": false,
450 "input": [
451 "def updated_discovered_word(discovered, guessed_letter):\n",
452 " locations = find_all(target, guessed_letter)\n",
453 " for location in locations:\n",
454 " discovered[location] = guessed_letter\n",
455 " return discovered"
456 ],
457 "language": "python",
458 "metadata": {},
459 "outputs": [],
460 "prompt_number": 16
461 },
462 {
463 "cell_type": "code",
464 "collapsed": false,
465 "input": [
466 "def initialise():\n",
467 " global lives, target, discovered, wrong_letters\n",
468 " lives = STARTING_LIVES\n",
469 " target = random.choice(WORDS)\n",
470 " discovered = list('_' * len(target))\n",
471 " wrong_letters = []"
472 ],
473 "language": "python",
474 "metadata": {},
475 "outputs": [],
476 "prompt_number": 17
477 },
478 {
479 "cell_type": "code",
480 "collapsed": false,
481 "input": [
482 "def do_turn():\n",
483 " global discovered, lives, wrong_letters\n",
484 " print('Word:', ' '.join(discovered), ' : Lives =', lives, ', wrong guesses:', ' '.join(sorted(wrong_letters)))\n",
485 " guess = input('Enter letter: ').strip().lower()[0]\n",
486 " if guess in target:\n",
487 " updated_discovered_word(discovered, guess)\n",
488 " else:\n",
489 " lives -= 1\n",
490 " if guess not in wrong_letters:\n",
491 " wrong_letters += [guess]"
492 ],
493 "language": "python",
494 "metadata": {},
495 "outputs": [],
496 "prompt_number": 18
497 },
498 {
499 "cell_type": "code",
500 "collapsed": false,
501 "input": [
502 "def play_game():\n",
503 " global discovered, lives\n",
504 " initialise()\n",
505 " game_finished = False\n",
506 " do_turn()\n",
507 " while not game_finished:\n",
508 " if '_' not in discovered:\n",
509 " print('You won! The word was', target)\n",
510 " game_finished = True\n",
511 " elif lives <= 0:\n",
512 " print('You lost. The word was', target)\n",
513 " game_finished = True\n",
514 " else:\n",
515 " do_turn()"
516 ],
517 "language": "python",
518 "metadata": {},
519 "outputs": [],
520 "prompt_number": 19
521 },
522 {
523 "cell_type": "markdown",
524 "metadata": {},
525 "source": [
526 "## Playing the game\n",
527 "That seemed straightforward. Let's see if it works!"
528 ]
529 },
530 {
531 "cell_type": "code",
532 "collapsed": false,
533 "input": [
534 "play_game()"
535 ],
536 "language": "python",
537 "metadata": {},
538 "outputs": [
539 {
540 "output_type": "stream",
541 "stream": "stdout",
542 "text": [
543 "Word: _ _ _ _ _ _ _ : Lives = 10 , wrong guesses: \n"
544 ]
545 },
546 {
547 "name": "stdout",
548 "output_type": "stream",
549 "stream": "stdout",
550 "text": [
551 "Enter letter: e\n"
552 ]
553 },
554 {
555 "output_type": "stream",
556 "stream": "stdout",
557 "text": [
558 "Word: _ _ _ _ _ _ e : Lives = 10 , wrong guesses: \n"
559 ]
560 },
561 {
562 "name": "stdout",
563 "output_type": "stream",
564 "stream": "stdout",
565 "text": [
566 "Enter letter: a\n"
567 ]
568 },
569 {
570 "output_type": "stream",
571 "stream": "stdout",
572 "text": [
573 "Word: _ _ _ _ _ _ e : Lives = 9 , wrong guesses: a\n"
574 ]
575 },
576 {
577 "name": "stdout",
578 "output_type": "stream",
579 "stream": "stdout",
580 "text": [
581 "Enter letter: i\n"
582 ]
583 },
584 {
585 "output_type": "stream",
586 "stream": "stdout",
587 "text": [
588 "Word: _ _ _ _ i _ e : Lives = 9 , wrong guesses: a\n"
589 ]
590 },
591 {
592 "name": "stdout",
593 "output_type": "stream",
594 "stream": "stdout",
595 "text": [
596 "Enter letter: o\n"
597 ]
598 },
599 {
600 "output_type": "stream",
601 "stream": "stdout",
602 "text": [
603 "Word: _ o _ _ i _ e : Lives = 9 , wrong guesses: a\n"
604 ]
605 },
606 {
607 "name": "stdout",
608 "output_type": "stream",
609 "stream": "stdout",
610 "text": [
611 "Enter letter: n\n"
612 ]
613 },
614 {
615 "output_type": "stream",
616 "stream": "stdout",
617 "text": [
618 "Word: _ o n _ i _ e : Lives = 9 , wrong guesses: a\n"
619 ]
620 },
621 {
622 "name": "stdout",
623 "output_type": "stream",
624 "stream": "stdout",
625 "text": [
626 "Enter letter: v\n"
627 ]
628 },
629 {
630 "output_type": "stream",
631 "stream": "stdout",
632 "text": [
633 "Word: _ o n _ i _ e : Lives = 8 , wrong guesses: a v\n"
634 ]
635 },
636 {
637 "name": "stdout",
638 "output_type": "stream",
639 "stream": "stdout",
640 "text": [
641 "Enter letter: t\n"
642 ]
643 },
644 {
645 "output_type": "stream",
646 "stream": "stdout",
647 "text": [
648 "Word: _ o n _ i _ e : Lives = 7 , wrong guesses: a t v\n"
649 ]
650 },
651 {
652 "name": "stdout",
653 "output_type": "stream",
654 "stream": "stdout",
655 "text": [
656 "Enter letter: s\n"
657 ]
658 },
659 {
660 "output_type": "stream",
661 "stream": "stdout",
662 "text": [
663 "Word: _ o n _ i _ e : Lives = 6 , wrong guesses: a s t v\n"
664 ]
665 },
666 {
667 "name": "stdout",
668 "output_type": "stream",
669 "stream": "stdout",
670 "text": [
671 "Enter letter: h\n"
672 ]
673 },
674 {
675 "output_type": "stream",
676 "stream": "stdout",
677 "text": [
678 "Word: _ o n _ i _ e : Lives = 5 , wrong guesses: a h s t v\n"
679 ]
680 },
681 {
682 "name": "stdout",
683 "output_type": "stream",
684 "stream": "stdout",
685 "text": [
686 "Enter letter: f\n"
687 ]
688 },
689 {
690 "output_type": "stream",
691 "stream": "stdout",
692 "text": [
693 "Word: _ o n f i _ e : Lives = 5 , wrong guesses: a h s t v\n"
694 ]
695 },
696 {
697 "name": "stdout",
698 "output_type": "stream",
699 "stream": "stdout",
700 "text": [
701 "Enter letter: b\n"
702 ]
703 },
704 {
705 "output_type": "stream",
706 "stream": "stdout",
707 "text": [
708 "Word: b o n f i _ e : Lives = 5 , wrong guesses: a h s t v\n"
709 ]
710 },
711 {
712 "name": "stdout",
713 "output_type": "stream",
714 "stream": "stdout",
715 "text": [
716 "Enter letter: r\n"
717 ]
718 },
719 {
720 "output_type": "stream",
721 "stream": "stdout",
722 "text": [
723 "You won! The word was bonfire\n"
724 ]
725 }
726 ],
727 "prompt_number": 22
728 },
729 {
730 "cell_type": "code",
731 "collapsed": false,
732 "input": [],
733 "language": "python",
734 "metadata": {},
735 "outputs": [],
736 "prompt_number": 17
737 }
738 ],
739 "metadata": {}
740 }
741 ]
742 }