Skip to content

Commit 7c4749d

Browse files
authored
Merge pull request larymak#111 from jerrychen1990/feature/ab_guess
add BullsAndCows
2 parents 92a582e + 3c46357 commit 7c4749d

File tree

4 files changed

+239
-1
lines changed

4 files changed

+239
-1
lines changed

BullsAndCows/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Bulls and Cows with AI
2+
AB Guess is a game to guess 4 digits with bulls and cows.
3+
the rule is [here](https://en.wikipedia.org/wiki/Bulls_and_Cows).
4+
5+
I build an AI program with MonteC Carlo tree search. I test the program 100 times, it takes an average of 4.52 steps to guess the number.
6+
7+
You can run
8+
```python
9+
./game.py --game_num=5
10+
```
11+
to compete with AI. Players with less step will win (I can't beat my AI😂). Good luck and have fun!

BullsAndCows/game.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#! /usr/bin/env python3
2+
# -*- coding utf-8 -*-
3+
"""
4+
-------------------------------------------------
5+
File Name: game.py
6+
Author : chenhao
7+
time: 2021/11/4 20:22
8+
Description :
9+
-------------------------------------------------
10+
"""
11+
import collections
12+
import logging
13+
import abc
14+
import math
15+
import random
16+
import time
17+
import fire
18+
from itertools import permutations
19+
from typing import List
20+
21+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s][%(filename)s:%(lineno)d]:%(message)s",
22+
datefmt='%Y-%m-%d %H:%M:%S')
23+
24+
logger = logging.getLogger(__name__)
25+
26+
NUMBER_COUNT = 4
27+
ALL_NUMBER = list(range(10))
28+
29+
30+
class IPlayer:
31+
def __init__(self, name):
32+
self.name = name
33+
34+
@abc.abstractmethod
35+
def guess(self) -> List[int]:
36+
pass
37+
38+
def refresh(self):
39+
pass
40+
41+
def notify(self, guess: List[int], judge_rs: dict):
42+
pass
43+
44+
def __str__(self):
45+
return self.name
46+
47+
def __repr__(self):
48+
return self.name
49+
50+
51+
class RandomPlayer(IPlayer):
52+
53+
def guess(self) -> List[int]:
54+
return random.sample(ALL_NUMBER, NUMBER_COUNT)
55+
56+
57+
class Human(IPlayer):
58+
def guess(self) -> List[int]:
59+
while True:
60+
try:
61+
logger.info("input your guess")
62+
guess = input()
63+
guess = [int(e) for e in guess]
64+
if len(guess) != NUMBER_COUNT:
65+
raise Exception()
66+
return guess
67+
except Exception as e:
68+
logger.error(f"invalid input:{guess}, please input again!")
69+
return guess
70+
71+
72+
class Node:
73+
def __init__(self, d):
74+
self.n = 0
75+
self.v = 0
76+
self.d = d
77+
if d < NUMBER_COUNT:
78+
self.children: List[Node] = [Node(d + 1) for _ in range(10)]
79+
else:
80+
self.children = None
81+
82+
def get_val(self, p, c=1.0):
83+
v = self.n / p
84+
d = math.log(1 / (self.v + 1))
85+
return v + c * d
86+
87+
def get_next(self, his):
88+
cands = [(idx, e, e.get_val(self.n)) for idx, e in enumerate(self.children) if e.n and idx not in his]
89+
# logger.info(cands)
90+
item = max(cands, key=lambda x: x[2])
91+
return item
92+
93+
def clear(self):
94+
self.n = 0
95+
if self.children:
96+
for c in self.children:
97+
c.clear()
98+
99+
def __repr__(self):
100+
return f"Node(n={self.n},v={self.v},d={self.d})"
101+
102+
def __str__(self):
103+
return self.__repr__()
104+
105+
106+
def update_tree(root, cand: List[int]):
107+
n = root
108+
for idx in cand:
109+
n.n += 1
110+
n = n.children[idx]
111+
n.n += 1
112+
113+
114+
class TreePlayer(IPlayer):
115+
116+
def __init__(self, name, wait=0):
117+
super().__init__(name=name)
118+
self.root = Node(d=0)
119+
self.cands = list(permutations(ALL_NUMBER, NUMBER_COUNT))
120+
self.wait = wait
121+
for cand in self.cands:
122+
update_tree(self.root, cand)
123+
124+
def refresh(self):
125+
self.root = Node(d=0)
126+
self.cands = list(permutations(ALL_NUMBER, NUMBER_COUNT))
127+
for cand in self.cands:
128+
update_tree(self.root, cand)
129+
130+
def guess(self) -> List[int]:
131+
n = self.root
132+
rs = []
133+
for _ in range(NUMBER_COUNT):
134+
idx, n, v = n.get_next(his=rs)
135+
n.v += 1
136+
rs.append(idx)
137+
time.sleep(self.wait)
138+
return rs
139+
140+
def notify(self, guess: List[int], judge_rs: dict):
141+
tmp = len(self.cands)
142+
self.cands = [e for e in self.cands if judge_rs2str(judge_rs) == judge_rs2str(judge(e, guess))]
143+
logger.info(f"cut cands from {tmp} to {len(self.cands)} after cuts")
144+
self.root.clear()
145+
for cand in self.cands:
146+
update_tree(self.root, cand)
147+
148+
149+
def judge(ans: List[int], gs: List[int]) -> dict:
150+
assert len(ans) == len(gs) == NUMBER_COUNT
151+
a_list = [e for e in zip(ans, gs) if e[0] == e[1]]
152+
a = len(a_list)
153+
b = len(set(ans) & set(gs))
154+
b -= a
155+
return dict(a=a, b=b)
156+
157+
158+
def judge_rs2str(j_rs):
159+
a = j_rs["a"]
160+
b = j_rs["b"]
161+
return f"{a}A{b}B"
162+
163+
164+
def run_game(player, rnd=10, answer=None):
165+
if not answer:
166+
answer = random.sample(ALL_NUMBER, NUMBER_COUNT)
167+
player.refresh()
168+
for idx in range(rnd):
169+
logger.info(f"round:{idx + 1}")
170+
guess = player.guess()
171+
judge_rs = judge(answer, guess)
172+
logger.info(f"{player} guess:{guess}, judge result:{judge_rs2str(judge_rs)}")
173+
if guess == answer:
174+
break
175+
player.notify(guess, judge_rs)
176+
logger.info(f"answer is :{answer}")
177+
if guess == answer:
178+
logger.info(f"{player} win in {idx + 1} rounds!")
179+
return idx
180+
else:
181+
logger.info(f"{player} failed!")
182+
return None
183+
184+
185+
def compete(players, game_num, rnd=10, base_score=10):
186+
answers = [random.sample(ALL_NUMBER, NUMBER_COUNT) for _ in range(game_num)]
187+
score_board = collections.defaultdict(int)
188+
for g in range(game_num):
189+
logger.info(f"game:{g + 1}")
190+
for p in players:
191+
logger.info(f"player {p} try")
192+
s = run_game(player=p, rnd=rnd, answer=answers[g])
193+
s = base_score - s if s is not None else 0
194+
score_board[p] += s
195+
logger.info("press any key to select next player")
196+
_ = input()
197+
logger.info(f"current score board:{dict(score_board)}")
198+
logger.info("press any key to next game")
199+
_ = input()
200+
201+
return score_board
202+
203+
204+
def compete_with_ai(game_num=3):
205+
human = Human("Human")
206+
ai = TreePlayer("AI", wait=2)
207+
players = [human, ai]
208+
logger.info(f"Human Vs AI with {game_num} games")
209+
score_board = compete(players=players, game_num=game_num)
210+
logger.info("final score board:{}")
211+
logger.info(score_board)
212+
213+
214+
def test_avg_step(test_num=100):
215+
ai = TreePlayer("AI", wait=0)
216+
steps = []
217+
for _ in range(test_num):
218+
steps.append(run_game(ai, rnd=10))
219+
avg = sum(steps) / len(steps)
220+
logger.info(f"{ai} avg cost{avg:.3f} steps with {test_num} tests")
221+
222+
223+
if __name__ == '__main__':
224+
fire.Fire(compete_with_ai)

BullsAndCows/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fire==0.3.0
2+

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,5 @@ The contribution guidelines are as per the guide [HERE](https://github.com/larym
8989
| 46 | [Image Divider](https://github.com/larymak/Python-project-Scripts/tree/main/ImageDivider) | [Rajarshi Banerjee](https://github.com/GSAUC3) |)
9090
| 47 | [Morse Code Converter](https://github.com/HarshitRV/Python-project-Scripts/tree/main/Morse-Code-Converter) | [HarshitRV](https://github.com/HarshitRV) |)
9191
| 48 | [CLI Photo Watermark](https://github.com/odinmay/Python-project-Scripts/tree/main/CLI-Photo-Watermark) | [Odin May](https://github.com/odinmay)
92-
| 49 | [Pomodoro App](https://github.com/HarshitRV/Python-project-Scripts/tree/main/Pomodoro-App) | [HarshitRV](https://github.com/HarshitRV)
92+
| 49 | [Pomodoro App](https://github.com/HarshitRV/Python-project-Scripts/tree/main/Pomodoro-App) | [HarshitRV](https://github.com/HarshitRV)
93+
| 49 | [BullsAndCows](https://github.com/HarshitRV/Python-project-Scripts/tree/main/BullsAndCows) | [JerryChen](https://github.com/jerrychen1990)

0 commit comments

Comments
 (0)