Skip to content

add BullsAndCows #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions BullsAndCows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Bulls and Cows with AI
AB Guess is a game to guess 4 digits with bulls and cows.
the rule is [here](https://en.wikipedia.org/wiki/Bulls_and_Cows).

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.

You can run
```python
./game.py --game_num=5
```
to compete with AI. Players with less step will win (I can't beat my AI😂). Good luck and have fun!
224 changes: 224 additions & 0 deletions BullsAndCows/game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#! /usr/bin/env python3
# -*- coding utf-8 -*-
"""
-------------------------------------------------
File Name: game.py
Author : chenhao
time: 2021/11/4 20:22
Description :
-------------------------------------------------
"""
import collections
import logging
import abc
import math
import random
import time
import fire
from itertools import permutations
from typing import List

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s][%(filename)s:%(lineno)d]:%(message)s",
datefmt='%Y-%m-%d %H:%M:%S')

logger = logging.getLogger(__name__)

NUMBER_COUNT = 4
ALL_NUMBER = list(range(10))


class IPlayer:
def __init__(self, name):
self.name = name

@abc.abstractmethod
def guess(self) -> List[int]:
pass

def refresh(self):
pass

def notify(self, guess: List[int], judge_rs: dict):
pass

def __str__(self):
return self.name

def __repr__(self):
return self.name


class RandomPlayer(IPlayer):

def guess(self) -> List[int]:
return random.sample(ALL_NUMBER, NUMBER_COUNT)


class Human(IPlayer):
def guess(self) -> List[int]:
while True:
try:
logger.info("input your guess")
guess = input()
guess = [int(e) for e in guess]
if len(guess) != NUMBER_COUNT:
raise Exception()
return guess
except Exception as e:
logger.error(f"invalid input:{guess}, please input again!")
return guess


class Node:
def __init__(self, d):
self.n = 0
self.v = 0
self.d = d
if d < NUMBER_COUNT:
self.children: List[Node] = [Node(d + 1) for _ in range(10)]
else:
self.children = None

def get_val(self, p, c=1.0):
v = self.n / p
d = math.log(1 / (self.v + 1))
return v + c * d

def get_next(self, his):
cands = [(idx, e, e.get_val(self.n)) for idx, e in enumerate(self.children) if e.n and idx not in his]
# logger.info(cands)
item = max(cands, key=lambda x: x[2])
return item

def clear(self):
self.n = 0
if self.children:
for c in self.children:
c.clear()

def __repr__(self):
return f"Node(n={self.n},v={self.v},d={self.d})"

def __str__(self):
return self.__repr__()


def update_tree(root, cand: List[int]):
n = root
for idx in cand:
n.n += 1
n = n.children[idx]
n.n += 1


class TreePlayer(IPlayer):

def __init__(self, name, wait=0):
super().__init__(name=name)
self.root = Node(d=0)
self.cands = list(permutations(ALL_NUMBER, NUMBER_COUNT))
self.wait = wait
for cand in self.cands:
update_tree(self.root, cand)

def refresh(self):
self.root = Node(d=0)
self.cands = list(permutations(ALL_NUMBER, NUMBER_COUNT))
for cand in self.cands:
update_tree(self.root, cand)

def guess(self) -> List[int]:
n = self.root
rs = []
for _ in range(NUMBER_COUNT):
idx, n, v = n.get_next(his=rs)
n.v += 1
rs.append(idx)
time.sleep(self.wait)
return rs

def notify(self, guess: List[int], judge_rs: dict):
tmp = len(self.cands)
self.cands = [e for e in self.cands if judge_rs2str(judge_rs) == judge_rs2str(judge(e, guess))]
logger.info(f"cut cands from {tmp} to {len(self.cands)} after cuts")
self.root.clear()
for cand in self.cands:
update_tree(self.root, cand)


def judge(ans: List[int], gs: List[int]) -> dict:
assert len(ans) == len(gs) == NUMBER_COUNT
a_list = [e for e in zip(ans, gs) if e[0] == e[1]]
a = len(a_list)
b = len(set(ans) & set(gs))
b -= a
return dict(a=a, b=b)


def judge_rs2str(j_rs):
a = j_rs["a"]
b = j_rs["b"]
return f"{a}A{b}B"


def run_game(player, rnd=10, answer=None):
if not answer:
answer = random.sample(ALL_NUMBER, NUMBER_COUNT)
player.refresh()
for idx in range(rnd):
logger.info(f"round:{idx + 1}")
guess = player.guess()
judge_rs = judge(answer, guess)
logger.info(f"{player} guess:{guess}, judge result:{judge_rs2str(judge_rs)}")
if guess == answer:
break
player.notify(guess, judge_rs)
logger.info(f"answer is :{answer}")
if guess == answer:
logger.info(f"{player} win in {idx + 1} rounds!")
return idx
else:
logger.info(f"{player} failed!")
return None


def compete(players, game_num, rnd=10, base_score=10):
answers = [random.sample(ALL_NUMBER, NUMBER_COUNT) for _ in range(game_num)]
score_board = collections.defaultdict(int)
for g in range(game_num):
logger.info(f"game:{g + 1}")
for p in players:
logger.info(f"player {p} try")
s = run_game(player=p, rnd=rnd, answer=answers[g])
s = base_score - s if s is not None else 0
score_board[p] += s
logger.info("press any key to select next player")
_ = input()
logger.info(f"current score board:{dict(score_board)}")
logger.info("press any key to next game")
_ = input()

return score_board


def compete_with_ai(game_num=3):
human = Human("Human")
ai = TreePlayer("AI", wait=2)
players = [human, ai]
logger.info(f"Human Vs AI with {game_num} games")
score_board = compete(players=players, game_num=game_num)
logger.info("final score board:{}")
logger.info(score_board)


def test_avg_step(test_num=100):
ai = TreePlayer("AI", wait=0)
steps = []
for _ in range(test_num):
steps.append(run_game(ai, rnd=10))
avg = sum(steps) / len(steps)
logger.info(f"{ai} avg cost{avg:.3f} steps with {test_num} tests")


if __name__ == '__main__':
fire.Fire(compete_with_ai)
2 changes: 2 additions & 0 deletions BullsAndCows/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fire==0.3.0

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