如何解决“Mastermind”猜谜游戏?

时间:2009-07-26 21:43:36

标签: python algorithm

你如何创建一个算法来解决以下难题,“Mastermind”?

你的对手选择了六种不同的颜色(黄色,蓝色,绿色,红色,橙色,紫色)。你必须猜测他们选择了哪个,以及以什么顺序。在每次猜测之后,你的对手会告诉你你猜到的颜色中有多少(但没有哪种)是正确的颜色[“黑色”]和多少(但不是哪个)是正确的颜色但是在错误的地方[ “白色”]。当你猜对了时,游戏就结束了(4个黑人,0个白人)。

例如,如果你的对手选择了(蓝色,绿色,橙色,红色),你猜(黄色,蓝色,绿色,红色),你会得到一个“黑色”(红色)和两个白色(蓝色和绿色)。猜测会得到相同的分数(蓝色,橙色,红色,紫色)。

我对您选择的算法以及(可选)如何将其转换为代码(最好是Python)感兴趣。我对以下编码解决方案感兴趣:

  1. 清楚(易于理解)
  2. 简明
  3. 高效(快速猜测)
  4. 有效(解决难题的猜测次数最少)
  5. 灵活(可以轻松回答有关算法的问题,例如最糟糕的情况?)
  6. 一般(可以很容易地适应除Mastermind之外的其他类型的拼图)
  7. 我很满意一种非常有效但效率不高的算法(前提是它不仅实现得不好!);然而,一个非常有效和有效的算法实现不灵活且无法使用。

    我已经发布了我自己的(详细)Python解决方案,但这绝不是唯一或最好的方法,所以请发布更多!我不期待一篇文章;)

10 个答案:

答案 0 :(得分:40)

答案 1 :(得分:12)

答案 2 :(得分:7)

你见过Raymond Hettinger的attempt吗?它们肯定符合您的一些要求。

我想知道他的解决方案与你的解决方案相比如何。

答案 3 :(得分:4)

答案 4 :(得分:1)

我以为我会贡献90多行代码。我建立在@Jim Dennis'答案的基础上,大部分都取消了对称评分的暗示。我已经实现了Knuth在Mastermind wikipedia article中描述的minimax算法,但有一个例外:我限制了我的下一步移动到当前的可能解决方案列表,因为我发现在每个解决方案考虑所有可能的解决方案时性能严重恶化步。目前的方法给我留下了最糟糕的情况,对于任何组合都有6次猜测,每次发现都在一秒钟之内。

或许重要的是要注意我对隐藏序列没有任何限制,允许任意数量的重复。

from itertools import product, tee
from random import choice

COLORS = 'red ', 'green', 'blue', 'yellow', 'purple', 'pink'#, 'grey', 'white', 'black', 'orange', 'brown', 'mauve', '-gap-'
HOLES = 4

def random_solution():
    """Generate a random solution."""
    return tuple(choice(COLORS) for i in range(HOLES))

def all_solutions():
    """Generate all possible solutions."""
    for solution in product(*tee(COLORS, HOLES)):
        yield solution

def filter_matching_result(solution_space, guess, result):
    """Filter solutions for matches that produce a specific result for a guess."""
    for solution in solution_space:
        if score(guess, solution) == result:
            yield solution

def score(actual, guess):
    """Calculate score of guess against actual."""
    result = []
    #Black pin for every color at right position
    actual_list = list(actual)
    guess_list = list(guess)
    black_positions = [number for number, pair in enumerate(zip(actual_list, guess_list)) if pair[0] == pair[1]]
    for number in reversed(black_positions):
        del actual_list[number]
        del guess_list[number]
        result.append('black')
    #White pin for every color at wrong position
    for color in guess_list:
        if color in actual_list:
            #Remove the match so we can't score it again for duplicate colors
            actual_list.remove(color)
            result.append('white')
    #Return a tuple, which is suitable as a dictionary key
    return tuple(result)

def minimal_eliminated(solution_space, solution):
    """For solution calculate how many possibilities from S would be eliminated for each possible colored/white score.
    The score of the guess is the least of such values."""
    result_counter = {}
    for option in solution_space:
        result = score(solution, option)
        if result not in result_counter.keys():
            result_counter[result] = 1
        else:
            result_counter[result] += 1
    return len(solution_space) - max(result_counter.values())

def best_move(solution_space):
    """Determine the best move in the solution space, being the one that restricts the number of hits the most."""
    elim_for_solution = dict((minimal_eliminated(solution_space, solution), solution) for solution in solution_space)
    max_elimintated = max(elim_for_solution.keys())
    return elim_for_solution[max_elimintated]

def main(actual = None):
    """Solve a game of mastermind."""
    #Generate random 'hidden' sequence if actual is None
    if actual == None:
        actual = random_solution()

    #Start the game of by choosing n unique colors
    current_guess = COLORS[:HOLES]

    #Initialize solution space to all solutions
    solution_space = all_solutions()
    guesses = 1
    while True:
        #Calculate current score
        current_score = score(actual, current_guess)
        #print '\t'.join(current_guess), '\t->\t', '\t'.join(current_score)
        if current_score == tuple(['black'] * HOLES):
            print guesses, 'guesses for\t', '\t'.join(actual)
            return guesses

        #Restrict solution space to exactly those hits that have current_score against current_guess
        solution_space = tuple(filter_matching_result(solution_space, current_guess, current_score))

        #Pick the candidate that will limit the search space most
        current_guess = best_move(solution_space)
        guesses += 1

if __name__ == '__main__':
    print max(main(sol) for sol in all_solutions())

如果有人发现上述代码的任何可能的改进,我会对您的建议非常感兴趣。

答案 5 :(得分:0)

要找出“最差”的情况,而不是使用熵,我正在寻找具有最大元素数量的分区,然后选择最小值为此最大值的尝试=>当我不幸运时,这将给我最小的剩余可能性(在最坏的情况下会发生)。

这总是在5次尝试中解决标准情况,但它并不是真正需要5次尝试的完整证据,因为它可能发生在下一步中更大的设置可能性会给出比较小的更好的结果(因为更容易区分)。

虽然对于1680年的“标准游戏”,我有一个简单的正式证据: 对于第一步,给出具有最大数量的分区的最小值的尝试是0,0,1,1:256。播放0,0,1,2不是很好:276。 对于每个后续尝试,有14个结果(1个未放置,3个放置是不可能的),4个放置的分区为1.这意味着在最好的情况下(所有分区大小相同),我们将得到一个最大分区,即最小值(可能性的数量 - 1)/ 13(向上舍入,因为我们有整数,所以必然会有一些会更少,其他更多,所以最大值被四舍五入)。

如果我申请:

第一次比赛(0,0,1,1)后我剩下256分。

第二次尝试后:20 =(256-1)/ 13

第三次尝试后:2 =(20-1)/ 13

然后我别无选择,只能尝试剩下的两个中的一个进行第四次尝试。

如果我运气不好,则需要进行第五次尝试。

这证明我们至少需要5次尝试(但这并不足以)。

答案 6 :(得分:0)

这是我写的一个通用算法,它使用数字来表示不同的颜色。易于更改,但我发现数字比字符串更容易使用。

只要相应地给予信用,您就可以随意使用此算法的任何全部或部分内容。

请注意我只是一名12年级的计算机科学专业的学生,​​所以我愿意打赌,肯定会有更优化的解决方案。

无论如何,这是代码:

import random


def main():

    userAns = raw_input("Enter your tuple, and I will crack it in six moves or less: ")
    play(ans=eval("("+userAns+")"),guess=(0,0,0,0),previousGuess=[])

def play(ans=(6,1,3,5),guess=(0,0,0,0),previousGuess=[]):

    if(guess==(0,0,0,0)):
       guess = genGuess(guess,ans)
    else:
        checker = -1
        while(checker==-1):
            guess,checker = genLogicalGuess(guess,previousGuess,ans)

    print guess, ans


    if not(guess==ans):
        previousGuess.append(guess)

        base = check(ans,guess)

        play(ans=ans,guess=base,previousGuess=previousGuess)

    else:
        print "Found it!"





def genGuess(guess,ans):
    guess = []
    for i in range(0,len(ans),1):
        guess.append(random.randint(1,6))

    return tuple(guess)

def genLogicalGuess(guess,previousGuess,ans):
    newGuess = list(guess)
    count = 0

    #Generate guess

    for i in range(0,len(newGuess),1):
        if(newGuess[i]==-1):
            newGuess.insert(i,random.randint(1,6))
            newGuess.pop(i+1)


    for item in previousGuess:
        for i in range(0,len(newGuess),1):
            if((newGuess[i]==item[i]) and (newGuess[i]!=ans[i])):
                newGuess.insert(i,-1)
                newGuess.pop(i+1)
                count+=1

    if(count>0):
        return guess,-1
    else:
        guess = tuple(newGuess)
        return guess,0


def check(ans,guess):
    base = []
    for i in range(0,len(zip(ans,guess)),1):
        if not(zip(ans,guess)[i][0] == zip(ans,guess)[i][1]):
            base.append(-1)
        else:
            base.append(zip(ans,guess)[i][1])

    return tuple(base)

main()

答案 7 :(得分:0)

这是Mastermind(tm)纯Python解算器的链接:http://code.activestate.com/recipes/496907-mastermind-style-code-breaking/它有一个简单版本,可以尝试各种猜测策略,性能测量和可选的C加速器。

配方的核心是简短而甜蜜的:

import random
from itertools import izip, imap

digits = 4
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

def strategy_allrand(i, possibles):
    return random.choice(possibles)

if __name__ == '__main__':
    hidden_code = random.choice(searchspace)
    rungame(hidden_code, strategy_allrand)

以下是输出结果:

Out of   10000 possibilities.  I'll guess (6, 4, 0, 9)  --->  (1, 0)
Out of    1372 possibilities.  I'll guess (7, 4, 5, 8)  --->  (1, 1)
Out of     204 possibilities.  I'll guess (1, 4, 2, 7)  --->  (2, 1)
Out of      11 possibilities.  I'll guess (1, 4, 7, 1)  --->  (3, 0)
Out of       2 possibilities.  I'll guess (1, 4, 7, 4)  --->  (4, 0)
That's it.  After 5 tries, I won.

答案 8 :(得分:0)

我的朋友正在考虑相对简单的案例 - 8种颜色,没有重复,没有空白。

没有重复,不需要最大熵考虑,所有猜测都具有相同的熵,并且首先或随机猜测都可以正常工作。

以下是解决该变体的完整代码:

# SET UP
import random
import itertools
colors = ('red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'ultra')

# ONE FUNCTION REQUIRED
def EvaluateCode(guess, secret_code):
    key = []
    for i in range(0, 4):
        for j in range(0, 4):
            if guess[i] == secret_code[j]:
                key += ['black'] if i == j else ['white']    
    return key

# MAIN CODE
# choose secret code
secret_code = random.sample(colors, 4)
print ('(shh - secret code is: ', secret_code, ')\n', sep='')
# create the full list of permutations
full_code_list = list(itertools.permutations(colors, 4))
N_guess = 0
while True:
    N_guess += 1
    print ('Attempt #', N_guess, '\n-----------', sep='')
    # make a random guess
    guess = random.choice(full_code_list)
    print ('guess:', guess)
    # evaluate the guess and get the key
    key = EvaluateCode(guess, secret_code)
    print ('key:', key)
    if key == ['black', 'black', 'black', 'black']:
        break
    # remove codes from the code list that don't match the key
    full_code_list2 = []
    for i in range(0, len(full_code_list)):
        if EvaluateCode(guess, full_code_list[i]) == key:
            full_code_list2 += [full_code_list[i]]
    full_code_list = full_code_list2
    print ('N remaining: ', len(full_code_list), '\n', full_code_list, '\n', sep='')
print ('\nMATCH after', N_guess, 'guesses\n')

答案 9 :(得分:0)

看看这些简单的难题。我只是为了好玩而已。比特币整数溢出可以用Temple代码修复。甚至AS400都可以将此代码用于企业银行。只需考虑变形。

Download Guess Puzzle

Download Temple Puzzle