Framework for experimenting with guessing strategies in Master-mind style games.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | import random
from itertools import izip, imap
from math import log
digits = 4
trials = 100
fmt = '%0' + str(digits) + 'd'
searchspace = tuple([tuple(map(int,fmt % i)) for i in range(0,10**digits)])
def compare(a, b, imap=imap, sum=sum, izip=izip, min=min):
count1 = [0] * 10
count2 = [0] * 10
strikes = 0
for dig1, dig2 in izip(a,b):
if dig1 == dig2:
strikes += 1
count1[dig1] += 1
count2[dig2] += 1
balls = sum(imap(min, count1, count2)) - strikes
return (strikes, balls)
def rungame(target, strategy, verbose=True, maxtries=15):
possibles = list(searchspace)
for i in xrange(maxtries):
g = strategy(i, possibles)
if verbose:
print "Out of %7d possibilities. I'll guess %r" % (len(possibles), g),
score = compare(g, target)
if verbose:
print ' ---> ', score
if score[0] == digits:
if verbose:
print "That's it. After %d tries, I won." % (i+1,)
break
possibles = [n for n in possibles if compare(g, n) == score]
return i+1
############################################### Strategy support
def info(seqn):
bits = 0
s = float(sum(seqn))
for i in seqn:
p = i / s
bits -= p * log(p, 2)
return bits
def utility(play, possibles):
b = {}
for poss in possibles:
score = compare(play, poss)
b[score] = b.get(score, 0) + 1
return info(b.values())
def hasdup(play, set=set, digits=digits):
return len(set(play)) != digits
def nodup(play, set=set, digits=digits):
return len(set(play)) == digits
################################################ Strategies
def s_allrand(i, possibles):
return random.choice(possibles)
def s_trynodup(i, possibles):
for j in xrange(20):
g = random.choice(possibles)
if nodup(g):
break
return g
def s_bestinfo(i, possibles):
if i == 0:
return s_trynodup(i, possibles)
plays = random.sample(possibles, min(20, len(possibles)))
_, play = max([(utility(play, possibles), play) for play in plays])
return play
def s_worstinfo(i, possibles):
if i == 0:
return s_trynodup(i, possibles)
plays = random.sample(possibles, min(20, len(possibles)))
_, play = min([(utility(play, possibles), play) for play in plays])
return play
def s_samplebest(i, possibles):
if i == 0:
return s_trynodup(i, possibles)
if len(possibles) > 150:
possibles = random.sample(possibles, 150)
plays = possibles[:20]
elif len(possibles) > 20:
plays = random.sample(possibles, 20)
else:
plays = possibles
_, play = max([(utility(play, possibles), play) for play in plays])
return play
## Evaluate Strategies
def average(seqn):
return sum(seqn) / float(len(seqn))
def counts(seqn):
limit = max(10, max(seqn)) + 1
tally = [0] * limit
for i in seqn:
tally[i] += 1
return tuple(tally[1:])
from time import clock
print '-' * 60
##for ss in searchspace, filter(hasdup, searchspace), filter(nodup, searchspace):
## print 'spacesize:', len(ss), ' = ', log(len(ss),2), 'bits'
##for play in [(1,2,3,4,5), (1,1,2,3,4), (1,1,1,2,3), (1,1,2,2,3), (1,1,1,2,2)]:
## print 'First play:', play, 'has info content', utility(play, searchspace)
## print 'First play:', play, 'has info content', utility(play, filter(hasdup, searchspace)), 'against dups'
## print 'First play:', play, 'has info content', utility(play, filter(nodup, searchspace)), 'against nodups'
## print
for strategy in (s_bestinfo, s_samplebest, s_worstinfo, s_allrand, s_trynodup, s_bestinfo):
start = clock()
data = [rungame(random.choice(searchspace), strategy, verbose=False) for i in xrange(trials)]
#print '[%d %d| median %.2f mean %.2f |%d %d] dig=%d n=%d %s' % (min(data), lq(data), median(data),average(data), uq(data), max(data), digits, len(data), strategy.__name__)
print 'mean=%.2f %r %s n=%d dig=%d' % (average(data), counts(data), strategy.__name__, len(data), digits)
print 'Time elapsed %.2f' % (clock() - start,)
""" Analysis of strategies with four digit targets
s_worstinfo [3 | median 7.00 mean 6.76 | 10] n=120
s_allrand [3 | median 6.00 mean 6.19 | 9] n=120
s_firstnodup [3 | median 6.00 mean 6.24 | 9] n=120
s_trynodup [3 | median 6.00 mean 5.99 | 9] n=120
s_bestinfo [2 | median 6.00 mean 5.75 | 10] n=120
s_worstinfo [3 | median 7.00 mean 6.86 | 10] n=200
s_allrand [4 | median 6.00 mean 6.52 | 10] n=200
s_firstnodup [1 | median 6.00 mean 6.19 | 10] n=200
s_firstdup_restnodup [3 | median 6.00 mean 6.13 | 10] n=200
s_trynodup [3 | median 6.00 mean 6.11 | 10] n=200
s_bestinfo [2 | median 6.00 mean 5.73 | 9] n=200
[3 6| median 7.00 mean 6.78 |8 11] dig=4 n=200 s_worstinfo 6.77
[3 5| median 6.00 mean 6.24 |7 10] dig=4 n=200 s_allrand 6.38
[2 6| median 6.00 mean 6.26 |7 9] dig=4 n=200 s_firstnodup 6.22
[2 5| median 6.00 mean 5.93 |7 9] dig=4 n=200 s_firstdup_restnodup 6.03
[4 5| median 6.00 mean 6.14 |7 9] dig=4 n=200 s_trynodup 6.12
[3 5| median 6.00 mean 5.87 |6 9] dig=4 n=200 s_bestinfo 5.74
[1 6| median 7.00 mean 6.93 |8 11] dig=4 n=500 s_worstinfo
[2 5| median 6.00 mean 6.11 |7 10] dig=4 n=500 s_allrand
[3 5| median 6.00 mean 6.12 |7 10] dig=4 n=500 s_firstnodup
[2 5| median 6.00 mean 6.07 |7 10] dig=4 n=500 s_firstdup_restnodup
[2 5| median 6.00 mean 6.03 |7 9] dig=4 n=500 s_trynodup
[2 5| median 6.00 mean 5.82 |6 9] dig=4 n=500 s_bestinfo
[2 5| median 6.00 mean 5.82 |6 9] dig=4 n=500 s_bestinfo_randfirst
spacesize: 100000 = 16.6096404744 bits
spacesize: 69760 = 16.0901124197 bits
spacesize: 30240 = 14.8841705191 bits
First play: (1, 2, 3, 4, 5) has info content 2.72731327592
First play: (1, 2, 3, 4, 5) has info content 2.54363363003 against dups
First play: (1, 2, 3, 4, 5) has info content 2.77115216576 against nodups
First play: (1, 1, 2, 3, 4) has info content 2.57287940403
First play: (1, 1, 2, 3, 4) has info content 2.51059435464 against dups
First play: (1, 1, 2, 3, 4) has info content 2.56508152257 against nodups
First play: (1, 1, 1, 2, 3) has info content 2.16683284403
First play: (1, 1, 1, 2, 3) has info content 2.11526590293 against dups
First play: (1, 1, 1, 2, 3) has info content 2.13291456398 against nodups
First play: (1, 1, 2, 2, 3) has info content 2.28656723529
First play: (1, 1, 2, 2, 3) has info content 2.28606417747 against dups
First play: (1, 1, 2, 2, 3) has info content 2.14424289741 against nodups
First play: (1, 1, 1, 2, 2) has info content 2.16683284403
First play: (1, 1, 1, 2, 2) has info content 2.11526590293 against dups
First play: (1, 1, 1, 2, 2) has info content 2.13291456398 against nodups
mean=6.22 (0, 0, 0, 4, 33, 130, 72, 9, 2, 0) s_samplebest n=250 dig=5
Time elapsed 1395.75
"""
|
The basic part of the framework takes only about 30 lines. They rest is an implementation of various search strategies and an engine to gather statistics on how well each strategy performs:
- Randomly pick any one of the remaining possibilities
- If possible, only make guesses that do not have duplicated digits.
- Make the guess that provides the most information (the one that most evenly divides-up the remaining possibilities).
To make it run fast, use psyco. To make it even faster, implement the compare function in C:
/* ------------- C version of the compare() function ----------------*/
#include "Python.h"
PyObject *
compare(PyObject *self, PyObject *args)
{
PyObject *t1, *t2, *elem;
int len, i, j, c1, c2;
int balls=0, strikes=0;
if (!PyArg_ParseTuple(args, "O!O!:compare", &PyTuple_Type, &t1, &PyTuple_Type, &t2))
return NULL;
len = PyTuple_Size(t1);
if (PyTuple_Size(t2) != len)
return NULL;
for (i=0 ; i<len ; i++) {
elem = PyTuple_GET_ITEM(t1, i);
for (c1=0, j=0 ; j<len ; j++) {
if (j<i && PyTuple_GET_ITEM(t1, j) == elem)
break;
if (PyTuple_GET_ITEM(t1, j) == elem)
c1 += 1;
}
for (c2=0, j=0 ; j<len ; j++) {
if (PyTuple_GET_ITEM(t2, j) == elem)
c2 += 1;
}
balls += (c1 < c2) ? c1 : c2;
if (PyTuple_GetItem(t2, i) == elem)
strikes += 1;
}
return Py_BuildValue("(ii)", strikes, balls-strikes);
}
static PyMethodDef mmfuncs[] = {
{"compare", (PyCFunction)compare, METH_VARARGS, "score two positions"},
{NULL}
};
void
initmm(void)
{
Py_InitModule3("mm", mmfuncs, "mastermind helper functions module");
}
Tags: algorithms