From f45c5761c41e03bcebb10ff2984112f974e5e6b9 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Mon, 28 Aug 2017 22:48:09 -0700 Subject: [PATCH] Add files via upload --- Coin Flip.ipynb | 183 ++++++++++++++++++++++-------------------------- 1 file changed, 83 insertions(+), 100 deletions(-) diff --git a/Coin Flip.ipynb b/Coin Flip.ipynb index 936598f..dcc5fff 100644 --- a/Coin Flip.ipynb +++ b/Coin Flip.ipynb @@ -6,23 +6,19 @@ "source": [ "# The Devil and the Coin Flip Game\n", "\n", - ">You're playing a game with the devil, with your soul at stake. You're sitting at a circular table, and on the table, there are 4 coins, arranged in a diamond, at the 12, 3, 6, and 9 o'clock positions. We'll number those as positions 0, 1, 2, and 3, respectively. Your goal is to get all 4 coins showing heads. You are blindfolded the entire time, and do not know the initial or subsequent state of the coins.\n", + ">You're playing a game with the devil, with your soul at stake. You're sitting at a circular table which has 4 coins, arranged in a diamond, at the 12, 3, 6, and 9 o'clock positions. We'll number those as positions 0, 1, 2, and 3, respectively. You are blindfolded, and can never see the coins or the table.\n", "\n", - ">Your only way of interacting with the coins is to tell the devil the position number(s) of some coins you want flipped. We call this a \"move\" on your part. The devil will faithfully perform the requested flips, but will first sneakily rotate the table either 0, 1, 2, or 3 quarter-turns, so that the coins are in different positions. You keep\n", - "making moves until 4 heads come up.\n", + ">Your goal is to get all 4 coins showing heads, by telling the devil the position number(s) of some coins to flip. We call this a \"move\" on your part. The devil must faithfully perform the requested flips, but may first sneakily rotate the table any number of quarter-turns, so that the coins are in different positions. The game ends when 4 heads come up and you win; until then you keep making moves.\n", "\n", - "> Example: You tell the devil to flip positions 0 and 2 (the 12 o'clock and 6 o'clock positions). The devil could leave the table unrotated (or could rotate it a half-turn), and then flip the two coins that you specified. Or the devil could rotate the table a quarter turn in either direction, and then flip the coins that are now in the 12 o'clock and 6 o'clock locations, which are now the two other coins from the ones you specified. You don't know how much the table was rotated, so you won't know which coins were flipped (and of course you can't see the state of the table before, during, or after the rotating/flipping action).\n", + "> Example: You tell the devil to flip positions 0 and 2 (the 12 o'clock and 6 o'clock positions). The devil could leave the table unrotated (or could rotate it a half-turn), and then flip the two coins that you specified. Or the devil could rotate the table a quarter turn in either direction, and then flip the coins that are now in the 12 o'clock and 6 o'clock locations, which are the two other coins from the ones you specified. You won't know which of these actions the devil took.\n", "\n", "> What is a shortest sequence of moves that is guaranteed to win, no matter what the initial state of the coins, and no matter what rotations the devil applies?\n", "\n", "# Analysis\n", "\n", - "The hard part is that we are blindfolded. So we don't know the true state of the coins. We need to represent what we do know: the *set of possible states* of the coins. We call this a *belief state*. At the start of the game, each of the four coins could be either heads or tails, so that's 24 = 16 possibilities. However, some of these possibilities are just rotations of other possibilities, and since the devil is free to apply any rotation at any time, it makes more sense to collapse these possibilities together. For example, a set of four possibilities, `{'HHHT', 'HHTH', 'HTHH', 'THHH'}`, all correspond to having three heads on the table, and for purposes of the belief state, I will represent this as a single canonical possibility, `{'HHHT'}`. (I arbitrarily chose the\n", - "one that comes first in alphabetical order.)\n", + "The player, being blindfolded, does not know the true state of the coins. So the player should represent what is known: the *set of possible states* of the coins. We call this a *belief state*. At the start of the game, each of the four coins could be either heads or tails, so that's 24 = 16 possibilities in the belief state. \n", "\n", - "Once we have the notion of a belief state, we can then update the belief state with the player's move, which is a set of positions to flip, such as `{0, 2}`. The updated belief state consists of every coin sequence in the original belief state, rotated in every possible way, and then with the flips applied. \n", - "\n", - "Note that the game is described as a turn-taking game, but it is equivalent to a open-loop game where the player specifies a complete sequence of moves all at once, and the devil then follows the instructions. So to solve the game, I need to come up with a sequence of moves that ends up in a belief state consisting of just `{'HHHH'}`. I want it to be a shortest path, so a breadth-first search seems reasonable.\n", + "Each possible move, such as `{0, 2}`, which means \"flip the 12 o'clock and 6 o'clock positions,\" updates the belief state as follows: for every coin sequence in the current belief state, rotate it in every possible way, and then flip the appropriate coins. Collect all these results together to form the new belief state. To solve the game, I need to come up with a sequence of moves that ends in a belief state consisting of just `{'HHHH'}`. I want it to be a shortest possible sequence, so a breadth-first search seems right. The search space will be tiny, so compute time will be trivial; the only issue is specifying the domain correctly. (To increase the chance of getting it correct, I won't try to do anything fancy, such as noticing that some sequences are rotational variants of other sequences.)\n", "\n", "\n", "# Implementation Choices\n", @@ -30,12 +26,10 @@ "Here are the main concepts, and my implementation choices:\n", "\n", "- `Coins`: A *coin sequence* is represented as a `str` of four characters, such as `'HTTT'`. \n", - "- `all_coins`: Every possible coin sequence.\n", "- `Belief`: A *belief state* is represented as a `frozenset` of `Coins` (frozen so that it can be hashed in a `set`).\n", - "- `rotations`: The function `rotations(coins)` returns the set of all 4 rotations of coins.\n", - "- `initial_belief`: The set of possible (canonical) coin sequences at the start of the game.\n", - "- `move`: A *move* is a set of positions to flip, such as `{0, 2}`, which means to flip the 12 o'clock and 6 o'clock positions.\n", - "- `update`: The function `update(belief, move)` retuns an updated belief state, representing all the possible coin sequences that could result from a devil rotation followed by the specified flip(s).\n", + "- `initial_belief`: The set of all possible coin sequences.\n", + "- `rotations`: The function `rotations(coins)` returns the set of all 4 rotations of the coin sequence.\n", + "- `update`: The function `update(belief, move)` retuns an updated belief state, representing all the possible coin sequences that could result from any devil rotation followed by the specified flip(s). (But don't flip `'HHHH'`, because the game would have already ended.)\n", "- `flip`: The function `flip(coins, move)` flips the specified positions within the coin sequence." ] }, @@ -43,28 +37,24 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ "from collections import deque, Counter\n", - "from itertools import chain, product, combinations\n", + "from itertools import product, combinations\n", "import random\n", "\n", "Coins = ''.join # Function to make a 4-element Coin Sequence, such as 'HHHT'\n", "\n", - "all_coins = {Coins(x) for x in product('HT', repeat=4)}\n", + "Belief = frozenset\n", "\n", - "def Belief(coinseq):\n", - " \"The set of possible coin sequences (canonicalized).\"\n", - " return frozenset(min(rotations(coins)) for coins in coinseq)\n", + "initial_belief = Belief(map(Coins, product('HT', repeat=4)))\n", "\n", "def rotations(coins): return {coins[r:] + coins[:r] for r in range(4)}\n", "\n", - "initial_belief = Belief(all_coins)\n", - "\n", "def update(belief, move):\n", - " \"Update belief: consider all rotations, followed by flips of non-winners.\"\n", + " \"Update belief: consider all possible rotations, then flip.\"\n", " return Belief((flip(c, move) if c != 'HHHH' else c)\n", " for coins in belief\n", " for c in rotations(coins))\n", @@ -138,22 +128,22 @@ { "data": { "text/plain": [ - "{'HHHH',\n", - " 'HHHT',\n", - " 'HHTH',\n", - " 'HHTT',\n", - " 'HTHH',\n", - " 'HTHT',\n", - " 'HTTH',\n", - " 'HTTT',\n", - " 'THHH',\n", - " 'THHT',\n", - " 'THTH',\n", - " 'THTT',\n", - " 'TTHH',\n", - " 'TTHT',\n", - " 'TTTH',\n", - " 'TTTT'}" + "frozenset({'HHHH',\n", + " 'HHHT',\n", + " 'HHTH',\n", + " 'HHTT',\n", + " 'HTHH',\n", + " 'HTHT',\n", + " 'HTTH',\n", + " 'HTTT',\n", + " 'THHH',\n", + " 'THHT',\n", + " 'THTH',\n", + " 'THTT',\n", + " 'TTHH',\n", + " 'TTHT',\n", + " 'TTTH',\n", + " 'TTTT'})" ] }, "execution_count": 4, @@ -162,7 +152,7 @@ } ], "source": [ - "all_coins" + "initial_belief" ] }, { @@ -175,7 +165,21 @@ { "data": { "text/plain": [ - "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'})" + "frozenset({'HHHH',\n", + " 'HHHT',\n", + " 'HHTH',\n", + " 'HHTT',\n", + " 'HTHH',\n", + " 'HTHT',\n", + " 'HTTH',\n", + " 'HTTT',\n", + " 'THHH',\n", + " 'THHT',\n", + " 'THTH',\n", + " 'THTT',\n", + " 'TTHH',\n", + " 'TTHT',\n", + " 'TTTH'})" ] }, "execution_count": 5, @@ -183,35 +187,6 @@ "output_type": "execute_result" } ], - "source": [ - "initial_belief" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The above says that there are 16 possible coin sequences, but only 6 of them are distinct after rotations. We can name the 6: all heads, 3 heads, 2 adjacent heads, 2 opposite heads, 1 head, all tails." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT'})" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "update(initial_belief, {0, 1, 2, 3})" ] @@ -220,14 +195,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That says that if we flip all 4 coins, we eliminate the possibility of 4 tails, but the other 5 coin sequences are still possible." + "That says that if we flip all 4 coins, we eliminate the possibility of 4 tails, cutting the possibilities down from 16 to 15." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Everything looks good so far. One more thing: we need to find all subsets of the 4 coin positions:" + "Everything looks good so far. One more thing: we need to find all subsets of the 4 positions:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def powerset(sequence): \n", + " \"All subsets of a sequence.\"\n", + " return [set(c) \n", + " for r in range(len(sequence) + 1)\n", + " for c in combinations(sequence, r)]" ] }, { @@ -264,12 +254,6 @@ } ], "source": [ - "def powerset(sequence): \n", - " \"All subsets of a sequence.\"\n", - " # See https://docs.python.org/3.6/library/itertools.html#itertools-recipes\n", - " combos = (combinations(sequence, r) for r in range(len(sequence) + 1))\n", - " return [set(x) for x in chain(*combos)]\n", - "\n", "powerset(range(4))" ] }, @@ -364,14 +348,13 @@ }, "outputs": [], "source": [ - "def random_play(moves):\n", - " \"Play moves against a random devil; return the number of moves until win, or None.\"\n", - " coins = random.choice(list(all_coins))\n", + "def random_devil(moves):\n", + " \"A random devil responds to moves; return the number of moves until win, or None.\"\n", + " coins = random.choice(list(initial_belief))\n", " if coins == 'HHHH':\n", " return 0\n", " for (i, move) in enumerate(moves, 1):\n", - " coins = random.choice(list(rotations(coins)))\n", - " coins = flip(coins, move)\n", + " coins = flip(random.choice(list(rotations(coins))), move)\n", " if coins == 'HHHH': \n", " return i" ] @@ -393,22 +376,22 @@ { "data": { "text/plain": [ - "Counter({0: 993,\n", - " 1: 994,\n", - " 2: 969,\n", - " 3: 1004,\n", - " 4: 992,\n", - " 5: 992,\n", - " 6: 957,\n", - " 7: 1002,\n", - " 8: 1021,\n", - " 9: 1013,\n", - " 10: 1001,\n", - " 11: 1023,\n", - " 12: 1007,\n", - " 13: 1036,\n", - " 14: 1026,\n", - " 15: 970})" + "Counter({0: 969,\n", + " 1: 1034,\n", + " 2: 966,\n", + " 3: 1026,\n", + " 4: 975,\n", + " 5: 979,\n", + " 6: 987,\n", + " 7: 1047,\n", + " 8: 978,\n", + " 9: 979,\n", + " 10: 998,\n", + " 11: 1033,\n", + " 12: 1025,\n", + " 13: 973,\n", + " 14: 1038,\n", + " 15: 993})" ] }, "execution_count": 11, @@ -419,7 +402,7 @@ "source": [ "moves = search()\n", "\n", - "Counter(random_play(moves) for _ in range(16000))" + "Counter(random_devil(moves) for _ in range(16000))" ] }, {