From fa2672ad4502a10cc406d99755c3147f24c20636 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Tue, 15 Aug 2017 17:09:24 -0700 Subject: [PATCH] Add files via upload --- Ghost.ipynb | 1151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1151 insertions(+) create mode 100644 Ghost.ipynb diff --git a/Ghost.ipynb b/Ghost.ipynb new file mode 100644 index 0000000..b038ea4 --- /dev/null +++ b/Ghost.ipynb @@ -0,0 +1,1151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ghost\n", + "\n", + "[*Ghost*](https://en.wikipedia.org/wiki/Ghost_(game)) is a word game in which (quoting Wikipedia):\n", + "\n", + "> *Ghost is a written or spoken word game in which players take turns adding letters to a growing word fragment, trying not to be the one to complete a valid word. Each fragment must be the beginning of an actual word, and usually some minimum is set on the length of a word that counts, such as three or four letters. The player who completes a word loses.*\n", + "\n", + "I'd like to create a program to allow any two players (human or computer) to play the game, and I'd like to figure out who wins if both players play optimally. The concepts I will need to define, and my implementation choices, are as follows:\n", + "\n", + "- **Words**: I will read a standard online word list, `enable1`, and make a set of all the words of sufficient length.\n", + "- **Fragment**: a fragment is a `str` of letters, such as `'gho'`.\n", + "- **Beginning**: each word has a set of valid beginnings: for `ghost` it is `{'', g, gh, gho, ghos, ghost}`. \"Prefix\" is a synonym of \"beginning\".\n", + "- **Vocabulary**: `Vocabulary(words)` is an object with: a set of all the `words`, and a set of all the valid `fragments` (beginnings) of the words.\n", + "- **Player**: The first player will be called player `0`; the second player `1`. \n", + "- **Play**: A play is a new fragment formed by adding one letter to the end of the existing fragment.\n", + "- **Legal Play**: A play that is a valid prefix of some word. `enable1.legal_plays('gho') = {'ghos, 'ghou'}`.\n", + "- **Strategy**: A strategy is a function with signature `strategy(vocab, fragment) -> play`.\n", + "- **Game**: `play_game(vocab, *strategies)` plays a game between two (or more) player strategies.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vocabulary: Words, Fragments, Legal Plays, and `enable1`\n", + "\n", + "`Vocabulary(text)` takes a collection of words as input, stores the words as a set, and also stores all the legal fragments of those words (that is, the beginnings). `legal_plays(fragments)` gives a set of all plays that can be formed by adding a letter to create a legal word fragment (where 'fragment' includes complete words). I also define the function `words` to split any string into component words." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class Vocabulary:\n", + " \"Holds a set of legal words and set of legal prefix fragments for those words; provides legal_plays(fragment).\"\n", + " def __init__(self, words, minlength=3):\n", + " self.words = {word for word in words if len(word) >= minlength}\n", + " self.fragments = {word[:i] for word in self.words for i in range(len(word) + 1)}\n", + " \n", + " def legal_plays(self, fragment): return {fragment + L for L in alphabet} & self.fragments \n", + " \n", + "alphabet = 'abcdefghijklmnopqrstuvwxyz'\n", + " \n", + "words = str.split # Function to split a str into words" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is a small example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "({'game', 'ghost', 'ghoul'},\n", + " {'', 'g', 'ga', 'gam', 'game', 'gh', 'gho', 'ghos', 'ghost', 'ghou', 'ghoul'})" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v = Vocabulary(words('game ghost ghoul'))\n", + "\n", + "v.words, v.fragments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And here is a large vocabulary, from a standard online Scrabble™ word list known as `enable1`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "! [ -e enable1.txt ] || curl -O http://norvig.com/ngrams/enable1.txt" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "enable1 = Vocabulary(words(open('enable1.txt').read()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here I explore `enable1`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(172724, 387878, 'ethylenediaminetetraacetates')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(enable1.words), len(enable1.fragments), max(enable1.words, key=len)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ghos', 'ghou'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "enable1.legal_plays('gho')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ewe'}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "enable1.legal_plays('ew')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'tha', 'the', 'thi', 'tho', 'thr', 'thu', 'thw', 'thy'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "enable1.legal_plays('th')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Players and Winners\n", + "\n", + "The first player is `0` and the second player is `1`. These names are convenient because:\n", + "- During the course of the game, the player whose turn it is to play next is always the length of the current fragment mod 2.\n", + "- When the game ends, the winning player is the length of the current fragment mod 2.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "to_play = winner = lambda fragment: len(fragment) % 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Who Wins?\n", + "\n", + "Who wins a game if both players are rational? Given the current fragment, the player whose turn it is will win if either:\n", + "- The fragment is a word (meaning the other player formed the word, and lost).\n", + "- The fragment is not a legal fragment (meaning the other player made something that is not the beginning of an actual word, and lost).\n", + "- At least one of the legal plays puts the opponent in a position from which they *cannot* win.\n", + "\n", + "The function `win(vocab, fragment)` implements this idea, returning `True` if the current player can force a win." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def win(vocab, fragment=''):\n", + " \"Does the player whose turn it is have a forced win?\"\n", + " return (fragment in vocab.words or \n", + " fragment not in vocab.fragments or\n", + " any(not win(vocab, play) \n", + " for play in vocab.legal_plays(fragment)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test `win` to gain some confidence that we got it right:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "win(Vocabulary(words('cat camel'))) # All words have odd number of letters; first player can never win" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "win(Vocabulary(words('cat camel goat'))) # First player plays 'g', leading to a win with 'goat'" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "win(Vocabulary(words('cat camel goat gar'))) # Second player can avoid 'goat' with 'ga'." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "win(Vocabulary(words('cat camel goat gar gannet'))) # First player counters 'ga' with 'gan' to win." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# TL;DR: The Answer\n", + "\n", + "Can the first player win with the `enable1` vocabulary?" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "win(enable1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**No.** The game is a win for the second player, not the first.\n", + "This agrees with [xkcd](https://xkcd.com/)'s Randall Monroe, who [says](https://blog.xkcd.com/2007/12/31/ghost/) *\"I hear if you use the Scrabble wordlist, it’s always a win for the second player.\"*\n", + "\n", + "But ... Wikipedia says that the minimum word length can be \"three or four letter.\" In `enable1` we included three-letter words, which is a disadvantage for the first player, who has to avoid all those three letter possibilities. What if we eliminate the three-letter words?" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "enable1_4 = Vocabulary(enable1.words, 4)\n", + "\n", + "win(enable1_4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Yes.** The first player can win in this case. So here's a good meta-strategy: Say \"Hey, let's play a game of Ghost. We can use the `enable1` word list. Would you like the limit to be 3 or 4 letters?\" Then if your opponent says three (or four) you can say \"OK, since you decided that, I'll decide to go second (or first).\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Playing the Game: Strategies\n", + "\n", + "We define a *strategy* as a function that is given a vocabulary and a fragment as arguments, and returns a legal play. Below we define `rational` (a strategy that wins whenever it is possible to do so) and `ask` (a strategy factory that returns a strategy that, when called, will ask the named person to input a fragment)." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def rational(vocab, fragment): \n", + " \"Select a play that makes opponent lose (if there is one), otherwise any play.\"\n", + " for play in vocab.legal_plays(fragment):\n", + " if not win(vocab, play):\n", + " return play\n", + " return play # Could return any play here\n", + "\n", + "def ask(name='Player'):\n", + " \"Return a strategy that asks for the next letter.\"\n", + " return (lambda _, fragment: \n", + " fragment + input(\"{}'s letter to add to '{}'? \".format(name, fragment)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is a function to play a game:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from itertools import cycle\n", + "\n", + "def play(vocab, *strategies):\n", + " \"Return (winner, final_fragment) for a game of Ghost between these strategies.\"\n", + " fragment = ''\n", + " for strategy in cycle(strategies):\n", + " play = strategy(vocab, fragment)\n", + " if play not in vocab.legal_plays(fragment):\n", + " return (winner(fragment + '?'), play) # Player loses for making an illegal play\n", + " elif play in vocab.words:\n", + " return (winner(play), play) # Player loses for making a word\n", + " else:\n", + " fragment = play # Keep playing" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 'ply')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play(enable1, rational, rational) " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 'huddle')" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play(enable1_4, rational, rational)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player's letter to add to ''? d\n", + "Player's letter to add to 'dr'? o\n", + "Player's letter to add to 'droi'? d\n" + ] + }, + { + "data": { + "text/plain": [ + "(1, 'droid')" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play(enable1, ask(), rational)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Minimizing Possible Outcomes\n", + "\n", + "Now we know how to play perfectly, if we have a computer handy to execute the strategy.\n", + "But can we summarize the strategy into a form that is small enough that a human can memorize it? I will define the function `outcomes(vocab, fragment, player)` to return a set of words that are the possible outcomes of a game, where the opponent can use any strategy whatsoever, but `player` uses a strategy that is:\n", + "\n", + "- *Rational*: plays towards a forced win whenever there is one.\n", + "- *Exploitive*: otherwise tries to give the opponent an opportunity to make a mistake that can be exploited.\n", + "- *Minimizing*: within the above constraints, returns the smallest possible set of words." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def outcomes(vocab, fragment, player):\n", + " \"The smallest set of outcomes, if player tries to win, and also tries to make the set of outcomes small.\"\n", + " if fragment in vocab.words:\n", + " return {fragment}\n", + " else:\n", + " cases = [outcomes(vocab, play, player) for play in vocab.legal_plays(fragment)]\n", + " if to_play(fragment) == player: # Player picks the top priority case\n", + " return min(cases, key=lambda words: priority(words, player))\n", + " else: # Oher player could pick anything\n", + " return set.union(*cases)\n", + " \n", + "def priority(words, player):\n", + " \"\"\"Return (lossiness, number_of_words, total_number_of_letters),\n", + " where lossiness is 0 if no losses, 1 if mixed losses/wins, 2 if all losses.\n", + " The idea is to find the list of outcome words that minimizes this triple.\"\"\"\n", + " lossiness = (0 if all(winner(word) == player for word in words) else\n", + " 1 if any(winner(word) == player for word in words) else\n", + " 2) \n", + " return (lossiness, len(words), sum(map(len, words)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Minimizing Outcomes for Player 0\n", + "\n", + "Let's see what minimal set of words player 0 can force the game into (with both vocabularies):" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'qaid', 'qiviut', 'qoph', 'qursh', 'qurush', 'qwerty'}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "outcomes(enable1, '', 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Interesting!** There are only 6 words; it wouldn't be hard for a human to memorize these. Then, when you are playing as player 0, pick `'q'` first, and then try to steer the game to one of the 5 words with an even number of letters. Unfortunately, one word, `'qursh'` (a monetary unit of Saudi Arabia), has an odd number of letters, which means that if the opponent replies to `'q'` with `'qu'` and to `'qur'` with `'qurs'`, then player 0 will lose. But if the opponent makes any other responses, player 0 will win." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'nays', 'nene', 'ngultrum', 'nirvanic', 'nolo', 'null', 'nyctalopia'}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "outcomes(enable1_4, '', 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Neat!** Only 7 words, and the first player can always win by forcing the opponent to one of these words.\n", + "\n", + "## Minimizing Outcomes for Player 1\n", + "\n", + "Since player 0 can pick any letter, the minimal `outcomes` set for player 1 must be at least 26 words. Let's see how much bigger it turns out to be. \n", + "\n", + "With `enable1` we already know that player 1 can force a win, so all the words in the `outcomes` set will have odd length:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'aah',\n", + " 'aal',\n", + " 'aargh',\n", + " 'aas',\n", + " 'bwana',\n", + " 'cwm',\n", + " 'drave',\n", + " 'dreck',\n", + " 'drink',\n", + " 'droit',\n", + " 'drunk',\n", + " 'dry',\n", + " 'ewe',\n", + " 'fjeld',\n", + " 'fjord',\n", + " 'gjetost',\n", + " 'hmm',\n", + " 'ihram',\n", + " 'jnana',\n", + " 'kwashiorkor',\n", + " 'llano',\n", + " 'mho',\n", + " 'nth',\n", + " 'oquassa',\n", + " 'praam',\n", + " 'prequel',\n", + " 'prill',\n", + " 'pro',\n", + " 'prurigo',\n", + " 'pry',\n", + " 'qua',\n", + " 'quell',\n", + " 'quiff',\n", + " 'quomodo',\n", + " 'qursh',\n", + " 'rhamnus',\n", + " 'rheum',\n", + " 'rhizoid',\n", + " 'rho',\n", + " 'rhumb',\n", + " 'rhyolitic',\n", + " 'squoosh',\n", + " 'tchotchke',\n", + " 'uhlan',\n", + " 'vroom',\n", + " 'wrack',\n", + " 'wrest',\n", + " 'wrist',\n", + " 'wrong',\n", + " 'wrung',\n", + " 'wry',\n", + " 'xanthic',\n", + " 'xanthin',\n", + " 'ycleped',\n", + " 'zucchetto'}" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "outcomes(enable1, '', 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(55, [])" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(_), [w for w in _ if winner(w) == 0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This says that player 1 can force the game towards one of these 55 words (none of which are losses). Memorize this list and you will never lose as player 1.\n", + "\n", + "How about with the other vocabulary?" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'aquiculture',\n", + " 'aquifer',\n", + " 'aquilegia',\n", + " 'aquiver',\n", + " 'bwana',\n", + " 'cnidarian',\n", + " 'drave',\n", + " 'dreck',\n", + " 'drink',\n", + " 'droit',\n", + " 'drunk',\n", + " 'drywall',\n", + " 'eschatologies',\n", + " 'eschatology',\n", + " 'escheat',\n", + " 'eserine',\n", + " 'eskar',\n", + " 'esophagus',\n", + " 'esplanade',\n", + " 'esquire',\n", + " 'esquiring',\n", + " 'essay',\n", + " 'estuarial',\n", + " 'estuary',\n", + " 'esurience',\n", + " 'fjeld',\n", + " 'fjord',\n", + " 'gjetost',\n", + " 'hyaenic',\n", + " 'hybris',\n", + " 'hydatid',\n", + " 'hyena',\n", + " 'hyenine',\n", + " 'hyenoid',\n", + " 'hygeist',\n", + " 'hying',\n", + " 'hylozoism',\n", + " 'hylozoist',\n", + " 'hymen',\n", + " 'hyoid',\n", + " 'hypha',\n", + " 'hyphen',\n", + " 'hyraces',\n", + " 'hyrax',\n", + " 'hyson',\n", + " 'hyte',\n", + " 'ihram',\n", + " 'jnana',\n", + " 'kwashiorkor',\n", + " 'llano',\n", + " 'mbira',\n", + " 'ngultrum',\n", + " 'ngwee',\n", + " 'oquassa',\n", + " 'plaza',\n", + " 'plethoric',\n", + " 'plica',\n", + " 'plonk',\n", + " 'pluck',\n", + " 'plyer',\n", + " 'quack',\n", + " 'quell',\n", + " 'quiff',\n", + " 'quomodo',\n", + " 'qursh',\n", + " 'rhamnus',\n", + " 'rheum',\n", + " 'rhizoid',\n", + " 'rhomb',\n", + " 'rhumb',\n", + " 'rhyolitic',\n", + " 'squoosh',\n", + " 'tchotchke',\n", + " 'uhlan',\n", + " 'vroom',\n", + " 'wrack',\n", + " 'wrest',\n", + " 'wrist',\n", + " 'wrong',\n", + " 'wrung',\n", + " 'wryly',\n", + " 'xanthic',\n", + " 'xanthin',\n", + " 'ycleped',\n", + " 'zucchetto'}" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "outcomes(enable1_4, '', 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(85, ['hyphen', 'ngultrum', 'hyte', 'hybris'])" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(_), [w for w in _ if winner(w) == 0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case there are 85 words, four of which are losses for player 1. But the other 81 words are wins, so with this strategy you'd have a good chance against an imperfect opponent." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SuperGhost\n", + "\n", + "In the variant *SuperGhost*, players can add a letter to either the beginning or the end of a fragment, as long as this forms a fragment that is part of some word. I was thinking of SuperGhost when I made the design decision to encapsulate `legal_plays` as a method of `Vocabulary`, rather than as a separate function. Because I did that, I should be able to use all the existing code if I just make a new class, `SuperVocabulary`, that finds *all* fragments (i.e. infixes) rather than just the beginning fragments (i.e. prefixes)." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "class SuperVocabulary(Vocabulary):\n", + " \"Holds a set of legal words and set of legal infix fragments of those words; provides legal_plays(fragment).\" \n", + " def __init__(self, words, minlength=3):\n", + " self.words = {word for word in words if len(word) >= minlength}\n", + " self.fragments = {word[i:j] for word in self.words \n", + " for i in range(len(word)) \n", + " for j in range(i, len(word) + 1)}\n", + " \n", + " def legal_plays(self, fragment):\n", + " \"All plays (adding a letter to fragment) that form a valid infix.\"\n", + " return {play for L in alphabet for play in (fragment + L, L + fragment)} & self.fragments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One more thing: I'll change `ask` to ask for a fragment, not a letter:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def ask(name='Player'):\n", + " \"Return a strategy that asks for a fragment.\"\n", + " return (lambda _, fragment: input(\"{}'s fragment, given '{}'? \".format(name, fragment)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now I will create `SuperVocabulary` objects for 3- and 4-letter versions of `enable1`, and check out how many fragments there are in each variant:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[387878, 387844, 1076434, 1076431]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "enable1s = SuperVocabulary(enable1.words)\n", + "enable1_4s = SuperVocabulary(enable1.words, 4)\n", + "\n", + "[len(v.fragments) for v in [enable1, enable1_4, enable1s, enable1_4s]]" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Can the first player win in SuperGhost with 3-letter words?\n", + "\n", + "win(enable1s)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# How about with a 4-letter limit?\n", + "\n", + "win(enable1_4s)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first player can win with or without three-letter words. And unless the first player is perfect, the rational strategy can do pretty well as seond player as well. Here is a sample game:" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Peter's fragment, given ''? z\n", + "Peter's fragment, given 'zq'? zqu\n", + "Peter's fragment, given 'ezqu'? ezqui\n", + "Peter's fragment, given 'mezqui'? mezquit\n" + ] + }, + { + "data": { + "text/plain": [ + "(1, 'mezquit')" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play(enable1s, ask('Peter'), rational)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I would like to give a concise summary of the strategy for SuperGhost, but my existing `outcomes` function won't do it. That's because it is not enough to know that a particular word results in a win; we have to know in what order the letters of the word are added. I'll leave it as an exercise to find a good way to summarize SuperGhost strategies." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "Here's a summary of what we have learned. (*Note:* the bold **qursh** means it is a losing word):\n", + "\n", + "| Game \t| Shortest \t| Winner \t| First Player Outcomes | Second Player Outcomes\n", + "|----\t|---\t |---\t |--- |---\n", + "| Ghost | 3 \t | Second \t| qaid qiviut qoph **qursh** qurush qwerty | 55 words\n", + "| Ghost | 4 \t | First \t| naan nene ngultrum nirvanic nolo null nyctalopia | 85 words\n", + "| SuperGhost | 3\t| First \t| ? | ? |\n", + "| SuperGhost | 4 \t| First \t| ? | ? |\n", + "\n", + "# Further Work\n", + "\n", + "Here are some additional ideas to play with:\n", + "\n", + "- **Exploitation:** What are some good strategies when you are not guaranteed to win, to exploit an imperfect human opponent? Can you steer the game so that you win if the opponent is unfamiliar with some obscure word(s)? You might need a file of [word frequencies](http://norvig.com/ngrams/count_1w.txt).\n", + "- **Security:** A strategy function could *cheat*, and modify `vocab.words`, inserting or deleting some crucial words to ensure victory. Can you harden `play` (and/or change `Vocabulary`) to protect against that?\n", + "- **Saving Space:** Currently `Vocabulary` saves words and fragments that could never be reached in a game. For example, because `'the'` is a word that ends the game, we could never reach `'them'` or `'theme'` or `'thermoluminescences'`. Can you eliminate these redundant words/fragments?\n", + "- **Multi-player:** `play(enable1, ask('A'), ask('B'), ask('C'))` will play a three-player game. But `rational` (along with `win` and `winner`) would no longer work, since they assume there are exactly two players. Can you alter them to allow *n* players?\n", + "- **SuperGhost Summary:** Can you summarize a SuperGhost strategy in a way that a human can memorize?\n", + "- **Xghost:** In *Xghost*, a letter can be added anywhere, so from the fragment `'ab'` you could play `'arb'`.\n", + "- **Spook:** In *Spook*, letters can be rearranged before adding one, so from the fragment `'ab'` you could play `'bxa'`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}