forked from vpavlenko/bulls-and-cows
-
Notifications
You must be signed in to change notification settings - Fork 0
/
solver.py
159 lines (123 loc) · 5.12 KB
/
solver.py
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
#!/usr/bin/env python3
import re
from math import log
from copy import copy
from random import shuffle, randrange, seed
seed(42)
NUM_DIGITS = 4
def is_allowed_number(number):
return len(str(number)) == NUM_DIGITS and len(set(str(number))) == NUM_DIGITS
ALL_NUMBERS = [number for number in range(1000, 10000) if is_allowed_number(number)]
POTENTIAL_ANSWERS = [(0, 1), (0, 2), (1, 1), (1, 0), (0, 0), (0, 3), (1, 2), (2, 0), (2, 1), (3, 0), (0, 4), (1, 3), (2, 2), (4, 0)]
# this order should accelerate the execution of question_entropy()
def count_bulls_and_cows(number, question):
number = str(number)
question = str(question)
bulls = 0
cows = 0
for i in range(NUM_DIGITS):
for j in range(NUM_DIGITS):
if number[i] == question[j]:
if i == j:
bulls += 1
else:
cows += 1
return (bulls, cows)
def number_is_consistent_with_qa(number, question, answer):
return count_bulls_and_cows(number, question) == answer
def count_possible_numbers(history, allowed_numbers):
count = 0
for number in allowed_numbers:
if all(number_is_consistent_with_qa(number, question, answer) for question, answer in history):
count += 1
return count
def get_unique_possible_number(history):
for number in ALL_NUMBERS:
if all(number_is_consistent_with_qa(number, question, answer) for question, answer in history):
return number
def question_entropy_by_history(history, allowed_numbers):
current_min_entropy = 10 ** 9
def question_entropy(question):
nonlocal current_min_entropy
result = 0
for answer in POTENTIAL_ANSWERS:
count = count_possible_numbers([(question, answer)], allowed_numbers)
if count > 0:
result += - log(1 / count) * count
if result > current_min_entropy:
return result
current_min_entropy = result
return result
return question_entropy
def get_best_question(step, history, current_allowed_numbers):
if step > 1:
for number in list(current_allowed_numbers):
if not number_is_consistent_with_qa(number, *history[-1]):
current_allowed_numbers.remove(number)
sample = list(current_allowed_numbers)
shuffle(sample)
sample = sample[:10 ** (step - 1)]
return min(sample, key=question_entropy_by_history(history, current_allowed_numbers))
class Game:
def __init__(self):
self.history = []
self.i = 0
self.current_allowed_numbers = set(ALL_NUMBERS)
def is_finished(self):
return count_possible_numbers(self.history, ALL_NUMBERS) <= 1
def get_question(self):
assert not self.is_finished()
self.i += 1
self.last_question = get_best_question(self.i, self.history, self.current_allowed_numbers)
return self.last_question
def put_answer(self, answer):
assert len(answer) == 2
self.history.append((self.last_question, answer))
def get_step(self):
if self.is_finished():
return self.i if self.history[-1][1][0] == 4 else self.i + 1
else:
return self.i
def is_correct(self):
return count_possible_numbers(self.history, ALL_NUMBERS) > 0
def guessed_number(self):
return get_unique_possible_number(self.history)
def interactive_game():
print("""This is 'Bulls and cows' solver
Author: Vitaly Pavlenko, http://vk.com/vitalypavlenko
Date: Nov 11 2012
Think of some number of four digits. All digits should be different.
For every question please answer two numbers: bulls and cows.
Example: if your secret number is 1234 and my question is 1453, you should answer '1 2'.
""")
game = Game()
while not game.is_finished():
question = game.get_question()
print('Question #{0}: {1}'.format(game.get_step(), question))
answer = tuple([int(i) for i in re.findall(r'[0-9]+', input())]) # should be a tuple of two numbers
if len(answer) != 2:
raise ValueError('your answer should contain exactly two numbers')
game.put_answer(answer)
if game.is_correct():
print("Your number is {0}. It took me {1} steps to guess it.".format(game.guessed_number(), game.get_step()))
else:
print("It seems that you've made a mistake somewhere.")
def test_all_numbers():
worst_number_of_steps = 0
for number in ALL_NUMBERS:
print('Testing number {0}'.format(number))
game = Game()
while not game.is_finished():
question = game.get_question()
print(' Question #{0}: {1}'.format(game.get_step(), question))
answer = count_bulls_and_cows(number, question)
print(' Answer: {0}'.format(answer))
game.put_answer(answer)
assert game.is_correct()
if game.get_step() > worst_number_of_steps:
worst_number_of_steps = game.get_step()
print(' Total number of steps:', game.get_step())
print(' The worst one was: ', worst_number_of_steps)
if __name__ == "__main__":
interactive_game()
# test_all_numbers()