Add files via upload

This commit is contained in:
Peter Norvig 2017-08-16 19:46:31 -07:00 committed by GitHub
parent 9232bdb195
commit f335103e95

View File

@ -6,9 +6,9 @@
"source": [
"# Ghost\n",
"\n",
"[*Ghost*](https://en.wikipedia.org/wiki/Ghost_(game)) is a word game in which (quoting Wikipedia):\n",
"According to [Wikipedia](https://en.wikipedia.org/wiki/Ghost_(game)):\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",
"> **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",
@ -446,24 +446,24 @@
},
"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",
"import random\n",
"\n",
"def ask(name='Player'):\n",
"def rational(vocab, fragment): \n",
" \"Select a play that makes opponent not win (if there is one), otherwise a random play.\"\n",
" plays = list(vocab.legal_plays(fragment))\n",
" return next((play for play in plays if not win(vocab, play)),\n",
" random.choice(plays))\n",
"\n",
"def ask(name):\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)))"
" return (lambda _, fragment: input('Player {}, given \"{}\", plays? '.format(name, fragment)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here is a function to play a game:"
"Here is a function to play a game. You give it a vocabulary, two (or potentially more) strategies, and optionally a `verbose` keyword to say if you want a line printed for each play or not."
]
},
{
@ -476,11 +476,14 @@
"source": [
"from itertools import cycle\n",
"\n",
"def play(vocab, *strategies):\n",
"def play(vocab, *strategies, verbose=True):\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 verbose:\n",
" print('Player {}, given \"{}\", plays \"{}\".'\n",
" .format(strategies.index(strategy), fragment, play))\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",
@ -496,10 +499,23 @@
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Player 0, given \"\", plays \"s\".\n",
"Player 0, given \"s\", plays \"sq\".\n",
"Player 0, given \"sq\", plays \"squ\".\n",
"Player 0, given \"squ\", plays \"squo\".\n",
"Player 0, given \"squo\", plays \"squoo\".\n",
"Player 0, given \"squoo\", plays \"squoos\".\n",
"Player 0, given \"squoos\", plays \"squoosh\".\n"
]
},
{
"data": {
"text/plain": [
"(1, 'ply')"
"(1, 'squoosh')"
]
},
"execution_count": 19,
@ -518,10 +534,20 @@
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Player 0, given \"\", plays \"h\".\n",
"Player 0, given \"h\", plays \"ha\".\n",
"Player 0, given \"ha\", plays \"haa\".\n",
"Player 0, given \"haa\", plays \"haaf\".\n"
]
},
{
"data": {
"text/plain": [
"(0, 'huddle')"
"(0, 'haaf')"
]
},
"execution_count": 20,
@ -544,15 +570,23 @@
"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"
"Player 0, given \"\", plays? d\n",
"Player 0, given \"\", plays \"d\".\n",
"Player 1, given \"d\", plays \"dw\".\n",
"Player 0, given \"dw\", plays? dwa\n",
"Player 0, given \"dw\", plays \"dwa\".\n",
"Player 1, given \"dwa\", plays \"dwar\".\n",
"Player 0, given \"dwar\", plays? dwarv\n",
"Player 0, given \"dwar\", plays \"dwarv\".\n",
"Player 1, given \"dwarv\", plays \"dwarve\".\n",
"Player 0, given \"dwarve\", plays? dwarves\n",
"Player 0, given \"dwarve\", plays \"dwarves\".\n"
]
},
{
"data": {
"text/plain": [
"(1, 'droid')"
"(1, 'dwarves')"
]
},
"execution_count": 21,
@ -561,7 +595,7 @@
}
],
"source": [
"play(enable1, ask(), rational)"
"play(enable1, ask(0), rational)"
]
},
{
@ -655,7 +689,7 @@
{
"data": {
"text/plain": [
"{'nays', 'nene', 'ngultrum', 'nirvanic', 'nolo', 'null', 'nyctalopia'}"
"{'nazi', 'nene', 'ngultrum', 'nirvanic', 'nolo', 'null', 'nyctalopia'}"
]
},
"execution_count": 24,
@ -700,7 +734,7 @@
" 'dreck',\n",
" 'drink',\n",
" 'droit',\n",
" 'drunk',\n",
" 'druid',\n",
" 'dry',\n",
" 'ewe',\n",
" 'fjeld',\n",
@ -714,7 +748,7 @@
" 'mho',\n",
" 'nth',\n",
" 'oquassa',\n",
" 'praam',\n",
" 'prase',\n",
" 'prequel',\n",
" 'prill',\n",
" 'pro',\n",
@ -737,13 +771,13 @@
" 'vroom',\n",
" 'wrack',\n",
" 'wrest',\n",
" 'wrist',\n",
" 'wrick',\n",
" 'wrong',\n",
" 'wrung',\n",
" 'wry',\n",
" 'xanthic',\n",
" 'xanthin',\n",
" 'ycleped',\n",
" 'yperite',\n",
" 'zucchetto'}"
]
},
@ -807,7 +841,7 @@
" 'dreck',\n",
" 'drink',\n",
" 'droit',\n",
" 'drunk',\n",
" 'druid',\n",
" 'drywall',\n",
" 'eschatologies',\n",
" 'eschatology',\n",
@ -826,7 +860,7 @@
" 'fjord',\n",
" 'gjetost',\n",
" 'hyaenic',\n",
" 'hybris',\n",
" 'hybrid',\n",
" 'hydatid',\n",
" 'hyena',\n",
" 'hyenine',\n",
@ -857,7 +891,7 @@
" 'plonk',\n",
" 'pluck',\n",
" 'plyer',\n",
" 'quack',\n",
" 'quaff',\n",
" 'quell',\n",
" 'quiff',\n",
" 'quomodo',\n",
@ -874,13 +908,13 @@
" 'vroom',\n",
" 'wrack',\n",
" 'wrest',\n",
" 'wrist',\n",
" 'wrick',\n",
" 'wrong',\n",
" 'wrung',\n",
" 'wryly',\n",
" 'xanthic',\n",
" 'xanthin',\n",
" 'ycleped',\n",
" 'yperite',\n",
" 'zucchetto'}"
]
},
@ -903,7 +937,7 @@
{
"data": {
"text/plain": [
"(85, ['hyphen', 'ngultrum', 'hyte', 'hybris'])"
"(85, ['hyphen', 'ngultrum', 'hybrid', 'hyte'])"
]
},
"execution_count": 28,
@ -928,7 +962,7 @@
"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)."
"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. As Wikipedia says, given the fragment `era`, a player might play `bera` or `erad`. 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), and if I change `legal_plays` to add letters to both ends."
]
},
{
@ -981,27 +1015,14 @@
},
{
"cell_type": "code",
"execution_count": 35,
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[387878, 387844, 1076434, 1076431]"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"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]]"
"enable1_4s = SuperVocabulary(enable1.words, 4)"
]
},
{
@ -1014,7 +1035,19 @@
{
"data": {
"text/plain": [
"True"
"{'atresse',\n",
" 'ctresse',\n",
" 'etresse',\n",
" 'itresse',\n",
" 'ntresse',\n",
" 'otresse',\n",
" 'ptresse',\n",
" 'rtresse',\n",
" 'stresse',\n",
" 'tressed',\n",
" 'tressel',\n",
" 'tresses',\n",
" 'ttresse'}"
]
},
"execution_count": 32,
@ -1023,9 +1056,8 @@
}
],
"source": [
"# Can the first player win in SuperGhost with 3-letter words?\n",
"\n",
"win(enable1s)"
"# Example legal plays\n",
"enable1s.legal_plays('tresse')"
]
},
{
@ -1046,6 +1078,30 @@
"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": 34,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 34,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# How about with a 4-letter limit?\n",
"\n",
@ -1061,7 +1117,7 @@
},
{
"cell_type": "code",
"execution_count": 34,
"execution_count": 35,
"metadata": {
"collapsed": false
},
@ -1070,19 +1126,26 @@
"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"
"Peter's fragment, given ''? q\n",
"Player 0, given \"\", plays \"q\".\n",
"Player 1, given \"q\", plays \"mq\".\n",
"Peter's fragment, given 'mq'? mqu\n",
"Player 0, given \"mq\", plays \"mqu\".\n",
"Player 1, given \"mqu\", plays \"umqu\".\n",
"Peter's fragment, given 'umqu'? umqua\n",
"Player 0, given \"umqu\", plays \"umqua\".\n",
"Player 1, given \"umqua\", plays \"kumqua\".\n",
"Peter's fragment, given 'kumqua'? kumquat\n",
"Player 0, given \"kumqua\", plays \"kumquat\".\n"
]
},
{
"data": {
"text/plain": [
"(1, 'mezquit')"
"(1, 'kumquat')"
]
},
"execution_count": 34,
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
@ -1095,7 +1158,221 @@
"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."
"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.\n",
"\n",
"# SuperDuperGhost\n",
"\n",
"In the variant *SuperDuperGhost*, players have an option to reverse the fragment before adding a letter to the beginning or end. As Wikipedia says, given the fragment `era`, a player might play `bera, erad, nare,` or `aren`.\n",
"Wikipedia is not clear, but I interpret this as meaning that the fragment played must still be a fragment of a word (not a reversed fragment of a word). Again, all we need is a new subclass:"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"class SuperDuperVocabulary(SuperVocabulary):\n",
" \"Holds a set of legal words and a set of legal infix fragments of those words.\"\n",
" \n",
" def legal_plays(self, fragment):\n",
" \"All plays that form a valid infix; optionally reverse fragment first.\"\n",
" return {play for L in alphabet \n",
" for play in (fragment + L, fragment[::-1] + L,\n",
" L + fragment, L + fragment[::-1])} & self.fragments"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"enable1sd = SuperDuperVocabulary(enable1.words)\n",
"enable1_4sd = SuperDuperVocabulary(enable1.words, 4)"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{'atresse',\n",
" 'ctresse',\n",
" 'dessert',\n",
" 'esserts',\n",
" 'etresse',\n",
" 'itresse',\n",
" 'ntresse',\n",
" 'otresse',\n",
" 'ptresse',\n",
" 'rtresse',\n",
" 'stresse',\n",
" 'tressed',\n",
" 'tressel',\n",
" 'tresses',\n",
" 'ttresse'}"
]
},
"execution_count": 38,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Example legal plays\n",
"enable1sd.legal_plays('tresse')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we should check who wins. But when I tried `win(enable1sd)`, I didn't get an answer within the first minute; there are just too many paths to get to the same fragment, so we are repeating a lot of work. The standard response to this problem is a `lru_cache`, which brings the time down to 2 seconds."
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"from functools import lru_cache\n",
"\n",
"@lru_cache(None)\n",
"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": "code",
"execution_count": 40,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"win(enable1sd)"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"win(enable1_4sd)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first player can win with either vocabulary. Here's a sample game."
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Player 0, given \"\", plays \"b\".\n",
"Player 0, given \"b\", plays \"sb\".\n",
"Player 0, given \"sb\", plays \"msb\".\n",
"Player 0, given \"msb\", plays \"msbu\".\n",
"Player 0, given \"msbu\", plays \"emsbu\".\n",
"Player 0, given \"emsbu\", plays \"gemsbu\".\n",
"Player 0, given \"gemsbu\", plays \"gemsbuc\".\n",
"Player 0, given \"gemsbuc\", plays \"gemsbuck\".\n"
]
},
{
"data": {
"text/plain": [
"(0, 'gemsbuck')"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"play(enable1sd, rational, rational)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's see how many fragments each vocabulary takes:"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[387878, 387844, 1076434, 1076431, 1076434, 1076431]"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[len(v.fragments) for v in [enable1, enable1_4, enable1s, enable1_4s, enable1sd, enable1_4sd]]"
]
},
{
@ -1106,12 +1383,14 @@
"\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",
"| Variant \t| Shortest \t| Winner \t| First Player Outcomes | 2nd Outcomes | Fragments\n",
"|----\t|---:\t |---\t |--- |--- |---:\n",
"| Ghost | 3 \t | Second \t| qaid qiviut qoph **qursh** qurush qwerty | 55 words | 387,878\n",
"| Ghost | 4 \t | First \t| naan nene ngultrum nirvanic nolo null nyctalopia | 85 words | 387,844\n",
"| Super | 3\t| First \t| ? | ? | 1,076,434\n",
"| Super | 4 \t| First \t| ? | ? | 1,076,431\n",
"| SuperDuper | 3 | First| ? | ? | 1,076,434\n",
"| SuperDuper | 4 | First| ? | ? | 1,076,431\n",
"\n",
"# Further Work\n",
"\n",