Updated notebook versions
[cas-master-teacher-training.git] / hangman / 01-hangman-setter.ipynb
1 {
2 "cells": [
3 {
4 "cell_type": "markdown",
5 "metadata": {},
6 "source": [
7 "# Hangman 1: set a puzzle\n",
8 "\n",
9 "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",
10 "\n",
11 "## Data structures\n",
12 "\n",
13 "* What do we need to track?\n",
14 "* What operations do we need to perform on it?\n",
15 "* How to store it?\n",
16 "\n",
17 "## Creating a game\n",
18 "* 'List' of words to choose from\n",
19 " * Pick one at random\n",
20 "\n",
21 "## Game state\n",
22 "\n",
23 "### Data\n",
24 "* Target word\n",
25 "* Discovered letters\n",
26 " * In order in the word\n",
27 "* Lives left\n",
28 "* Wrong letters?\n",
29 "\n",
30 "### Operations\n",
31 "* Get a guess\n",
32 "* Update discovered letters\n",
33 "* Update lives\n",
34 "* Show discovered word\n",
35 "* Detect game end, report\n",
36 "* Detect game win or loss, report\n"
37 ]
38 },
39 {
40 "cell_type": "code",
41 "execution_count": 3,
42 "metadata": {
43 "collapsed": false
44 },
45 "outputs": [],
46 "source": [
47 "import re\n",
48 "import random"
49 ]
50 },
51 {
52 "cell_type": "markdown",
53 "metadata": {},
54 "source": [
55 "## Get the words\n",
56 "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. "
57 ]
58 },
59 {
60 "cell_type": "code",
61 "execution_count": 4,
62 "metadata": {
63 "collapsed": false
64 },
65 "outputs": [],
66 "source": [
67 "# Just use a regex to filter the words. There are other ways to do this, as you know.\n",
68 "WORDS = [w.strip() for w in open('/usr/share/dict/british-english').readlines() \n",
69 " if re.match(r'^[a-z]*$', w.strip())]"
70 ]
71 },
72 {
73 "cell_type": "markdown",
74 "metadata": {},
75 "source": [
76 "A few quick looks at the list of words, to make sure it's sensible."
77 ]
78 },
79 {
80 "cell_type": "code",
81 "execution_count": 5,
82 "metadata": {
83 "collapsed": false
84 },
85 "outputs": [
86 {
87 "data": {
88 "text/plain": [
89 "62856"
90 ]
91 },
92 "execution_count": 5,
93 "metadata": {},
94 "output_type": "execute_result"
95 }
96 ],
97 "source": [
98 "len(WORDS)"
99 ]
100 },
101 {
102 "cell_type": "code",
103 "execution_count": 6,
104 "metadata": {
105 "collapsed": false
106 },
107 "outputs": [
108 {
109 "data": {
110 "text/plain": [
111 "['jotted',\n",
112 " 'jotting',\n",
113 " 'jottings',\n",
114 " 'joule',\n",
115 " 'joules',\n",
116 " 'jounce',\n",
117 " 'jounced',\n",
118 " 'jounces',\n",
119 " 'jouncing',\n",
120 " 'journal']"
121 ]
122 },
123 "execution_count": 6,
124 "metadata": {},
125 "output_type": "execute_result"
126 }
127 ],
128 "source": [
129 "WORDS[30000:30010]"
130 ]
131 },
132 {
133 "cell_type": "markdown",
134 "metadata": {},
135 "source": [
136 "## Constants and game states"
137 ]
138 },
139 {
140 "cell_type": "markdown",
141 "metadata": {},
142 "source": [
143 "## The target word"
144 ]
145 },
146 {
147 "cell_type": "code",
148 "execution_count": 7,
149 "metadata": {
150 "collapsed": false
151 },
152 "outputs": [
153 {
154 "data": {
155 "text/plain": [
156 "'rocketing'"
157 ]
158 },
159 "execution_count": 7,
160 "metadata": {},
161 "output_type": "execute_result"
162 }
163 ],
164 "source": [
165 "target = random.choice(WORDS)\n",
166 "target"
167 ]
168 },
169 {
170 "cell_type": "code",
171 "execution_count": 8,
172 "metadata": {
173 "collapsed": false
174 },
175 "outputs": [],
176 "source": [
177 "STARTING_LIVES = 10\n",
178 "lives = 0"
179 ]
180 },
181 {
182 "cell_type": "code",
183 "execution_count": 9,
184 "metadata": {
185 "collapsed": false
186 },
187 "outputs": [],
188 "source": [
189 "wrong_letters = []"
190 ]
191 },
192 {
193 "cell_type": "markdown",
194 "metadata": {},
195 "source": [
196 "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",
197 "\n",
198 "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",
199 "\n",
200 "We can also use `discovered` to check for a game win. If `discovered` contains an underscore, the player hasn't won the game."
201 ]
202 },
203 {
204 "cell_type": "code",
205 "execution_count": 10,
206 "metadata": {
207 "collapsed": false
208 },
209 "outputs": [
210 {
211 "data": {
212 "text/plain": [
213 "['_', '_', '_', '_', '_', '_', '_', '_', '_']"
214 ]
215 },
216 "execution_count": 10,
217 "metadata": {},
218 "output_type": "execute_result"
219 }
220 ],
221 "source": [
222 "discovered = list('_' * len(target))\n",
223 "discovered"
224 ]
225 },
226 {
227 "cell_type": "markdown",
228 "metadata": {},
229 "source": [
230 "We can use `' '.join(discovered)` to display it nicely."
231 ]
232 },
233 {
234 "cell_type": "code",
235 "execution_count": 11,
236 "metadata": {
237 "collapsed": false
238 },
239 "outputs": [
240 {
241 "data": {
242 "text/plain": [
243 "'_ _ _ _ _ _ _ _ _'"
244 ]
245 },
246 "execution_count": 11,
247 "metadata": {},
248 "output_type": "execute_result"
249 }
250 ],
251 "source": [
252 "' '.join(discovered)"
253 ]
254 },
255 {
256 "cell_type": "markdown",
257 "metadata": {},
258 "source": [
259 "## Read a letter\n",
260 "Get rid of fluff and make sure we only get one letter."
261 ]
262 },
263 {
264 "cell_type": "code",
265 "execution_count": 9,
266 "metadata": {
267 "collapsed": false
268 },
269 "outputs": [
270 {
271 "name": "stdout",
272 "output_type": "stream",
273 "text": [
274 "Enter letter: r\n"
275 ]
276 },
277 {
278 "data": {
279 "text/plain": [
280 "'r'"
281 ]
282 },
283 "execution_count": 9,
284 "metadata": {},
285 "output_type": "execute_result"
286 }
287 ],
288 "source": [
289 "letter = input('Enter letter: ').strip().lower()[0]\n",
290 "letter"
291 ]
292 },
293 {
294 "cell_type": "markdown",
295 "metadata": {},
296 "source": [
297 "# Finding a letter in a word\n",
298 "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",
299 "\n",
300 "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",
301 "\n",
302 "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."
303 ]
304 },
305 {
306 "cell_type": "code",
307 "execution_count": 25,
308 "metadata": {
309 "collapsed": false
310 },
311 "outputs": [],
312 "source": [
313 "# Note that find returns either the first occurrence of a substring, or -1 if it doesn't exist.\n",
314 "def find_all_explicit(string, letter):\n",
315 " locations = []\n",
316 " starting=0\n",
317 " location = string.find(letter)\n",
318 " while location > -1:\n",
319 " locations += [location]\n",
320 " starting = location + 1\n",
321 " location = string.find(letter, starting)\n",
322 " return locations"
323 ]
324 },
325 {
326 "cell_type": "code",
327 "execution_count": 12,
328 "metadata": {
329 "collapsed": false
330 },
331 "outputs": [],
332 "source": [
333 "# A simpler way using a list comprehension and the built-in enumerate()\n",
334 "def find_all(string, letter):\n",
335 " return [p for p, l in enumerate(string) if l == letter]"
336 ]
337 },
338 {
339 "cell_type": "code",
340 "execution_count": 13,
341 "metadata": {
342 "collapsed": false
343 },
344 "outputs": [
345 {
346 "data": {
347 "text/plain": [
348 "[2, 3]"
349 ]
350 },
351 "execution_count": 13,
352 "metadata": {},
353 "output_type": "execute_result"
354 }
355 ],
356 "source": [
357 "find_all('happy', 'p')"
358 ]
359 },
360 {
361 "cell_type": "markdown",
362 "metadata": {},
363 "source": [
364 "## Updating the discovered word\n",
365 "Now we can update `discovered`. We'll look through the `locations` list and update that element of `discovered`."
366 ]
367 },
368 {
369 "cell_type": "code",
370 "execution_count": 15,
371 "metadata": {
372 "collapsed": false
373 },
374 "outputs": [
375 {
376 "data": {
377 "text/plain": [
378 "['_', '_', '_', '_', 'e', '_', '_', '_', '_']"
379 ]
380 },
381 "execution_count": 15,
382 "metadata": {},
383 "output_type": "execute_result"
384 }
385 ],
386 "source": [
387 "guessed_letter = 'e'\n",
388 "locations = find_all(target, guessed_letter)\n",
389 "for location in locations:\n",
390 " discovered[location] = guessed_letter\n",
391 "discovered"
392 ]
393 },
394 {
395 "cell_type": "markdown",
396 "metadata": {},
397 "source": [
398 "## Putting it all together\n",
399 "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",
400 "\n",
401 "```\n",
402 "to play a game:\n",
403 " initialise lives, target, etc.\n",
404 " finished? = False\n",
405 " handle a turn\n",
406 " while not finished?:\n",
407 " if guessed all letters:\n",
408 " report success\n",
409 " finished? = True\n",
410 " elif run out of lives:\n",
411 " report failure\n",
412 " finished? = True\n",
413 " else:\n",
414 " handle a turn\n",
415 "```\n",
416 "\n",
417 "```\n",
418 "to handle a turn:\n",
419 " print the challenge\n",
420 " get the letter\n",
421 " if letter in target:\n",
422 " update discovered\n",
423 " else:\n",
424 " update lives, wrong_letters\n",
425 "```\n",
426 "\n",
427 "```\n",
428 "to update discovered:\n",
429 " find all the locations of letter in target\n",
430 " replace the appropriate underscores in discovered\n",
431 "```\n",
432 "\n",
433 "We pretty much know how to do all these bits, so let's just start at the bottom and off we go!\n",
434 "\n",
435 "###Syntax note\n",
436 "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."
437 ]
438 },
439 {
440 "cell_type": "code",
441 "execution_count": 16,
442 "metadata": {
443 "collapsed": false
444 },
445 "outputs": [],
446 "source": [
447 "def updated_discovered_word(discovered, guessed_letter):\n",
448 " locations = find_all(target, guessed_letter)\n",
449 " for location in locations:\n",
450 " discovered[location] = guessed_letter\n",
451 " return discovered"
452 ]
453 },
454 {
455 "cell_type": "code",
456 "execution_count": 17,
457 "metadata": {
458 "collapsed": false
459 },
460 "outputs": [],
461 "source": [
462 "def initialise():\n",
463 " global lives, target, discovered, wrong_letters\n",
464 " lives = STARTING_LIVES\n",
465 " target = random.choice(WORDS)\n",
466 " discovered = list('_' * len(target))\n",
467 " wrong_letters = []"
468 ]
469 },
470 {
471 "cell_type": "code",
472 "execution_count": 18,
473 "metadata": {
474 "collapsed": false
475 },
476 "outputs": [],
477 "source": [
478 "def do_turn():\n",
479 " global discovered, lives, wrong_letters\n",
480 " print('Word:', ' '.join(discovered), ' : Lives =', lives, ', wrong guesses:', ' '.join(sorted(wrong_letters)))\n",
481 " guess = input('Enter letter: ').strip().lower()[0]\n",
482 " if guess in target:\n",
483 " updated_discovered_word(discovered, guess)\n",
484 " else:\n",
485 " lives -= 1\n",
486 " if guess not in wrong_letters:\n",
487 " wrong_letters += [guess]"
488 ]
489 },
490 {
491 "cell_type": "code",
492 "execution_count": 19,
493 "metadata": {
494 "collapsed": false
495 },
496 "outputs": [],
497 "source": [
498 "def play_game():\n",
499 " global discovered, lives\n",
500 " initialise()\n",
501 " game_finished = False\n",
502 " do_turn()\n",
503 " while not game_finished:\n",
504 " if '_' not in discovered:\n",
505 " print('You won! The word was', target)\n",
506 " game_finished = True\n",
507 " elif lives <= 0:\n",
508 " print('You lost. The word was', target)\n",
509 " game_finished = True\n",
510 " else:\n",
511 " do_turn()"
512 ]
513 },
514 {
515 "cell_type": "markdown",
516 "metadata": {},
517 "source": [
518 "## Playing the game\n",
519 "That seemed straightforward. Let's see if it works!"
520 ]
521 },
522 {
523 "cell_type": "code",
524 "execution_count": 22,
525 "metadata": {
526 "collapsed": false
527 },
528 "outputs": [
529 {
530 "name": "stdout",
531 "output_type": "stream",
532 "text": [
533 "Word: _ _ _ _ _ _ _ : Lives = 10 , wrong guesses: \n",
534 "Enter letter: e\n",
535 "Word: _ _ _ _ _ _ e : Lives = 10 , wrong guesses: \n",
536 "Enter letter: a\n",
537 "Word: _ _ _ _ _ _ e : Lives = 9 , wrong guesses: a\n",
538 "Enter letter: i\n",
539 "Word: _ _ _ _ i _ e : Lives = 9 , wrong guesses: a\n",
540 "Enter letter: o\n",
541 "Word: _ o _ _ i _ e : Lives = 9 , wrong guesses: a\n",
542 "Enter letter: n\n",
543 "Word: _ o n _ i _ e : Lives = 9 , wrong guesses: a\n",
544 "Enter letter: v\n",
545 "Word: _ o n _ i _ e : Lives = 8 , wrong guesses: a v\n",
546 "Enter letter: t\n",
547 "Word: _ o n _ i _ e : Lives = 7 , wrong guesses: a t v\n",
548 "Enter letter: s\n",
549 "Word: _ o n _ i _ e : Lives = 6 , wrong guesses: a s t v\n",
550 "Enter letter: h\n",
551 "Word: _ o n _ i _ e : Lives = 5 , wrong guesses: a h s t v\n",
552 "Enter letter: f\n",
553 "Word: _ o n f i _ e : Lives = 5 , wrong guesses: a h s t v\n",
554 "Enter letter: b\n",
555 "Word: b o n f i _ e : Lives = 5 , wrong guesses: a h s t v\n",
556 "Enter letter: r\n",
557 "You won! The word was bonfire\n"
558 ]
559 }
560 ],
561 "source": [
562 "play_game()"
563 ]
564 },
565 {
566 "cell_type": "code",
567 "execution_count": 17,
568 "metadata": {
569 "collapsed": false
570 },
571 "outputs": [],
572 "source": []
573 }
574 ],
575 "metadata": {
576 "kernelspec": {
577 "display_name": "Python 3",
578 "language": "python",
579 "name": "python3"
580 },
581 "language_info": {
582 "codemirror_mode": {
583 "name": "ipython",
584 "version": 3
585 },
586 "file_extension": ".py",
587 "mimetype": "text/x-python",
588 "name": "python",
589 "nbconvert_exporter": "python",
590 "pygments_lexer": "ipython3",
591 "version": "3.5.2+"
592 }
593 },
594 "nbformat": 4,
595 "nbformat_minor": 0
596 }