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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy