Can this algorithm for Mastermind with 4 positions and 10 numbers be improved from 7 to 6 steps in the worst case?

70 views Asked by At

I am trying to implement a Mastermind solving algorithm in python. For this, I tried implementing the Minimax solver by Knuth, which promises a worst case of 5 steps for 4 positions and 6 colors with repetition.

My version involves 10 numbers, 4 positions and no repetition. Also, the guesses cannot start with 0. So there are 9*9*8*7 = 4536 possible codes. I let the code play "against" itself for all possible combinations, presetting the first 2 guesses, and for the 4536 possible guesses, I get 241 plays where the solver takes 7 steps. So in this case, 7 steps is the worst case. Is it possible to optimize the algorithm to win everytime in 6 steps at most?

This is my code:

import random


possible_combs = []
possible_feedbacks = [[0,0], [0,1], [0,2], [0,3], [0,4], [1,0], [1,1], [1,2], [1,3], [2,0], [2,1], [2,2], [3,0]]

def check_dup(number):
    if len(set(str(number))) != 4:
        return False
    else:
        return True
    
for i in range(1000,10000):
    if check_dup(i):
      possible_combs.append(i)

all_combs = possible_combs.copy()

def match_answer(number, last_number):
    pluses = sum([1 for a, b in zip(str(number), str(last_number)) if a == b])

    minuses = len(set(str(last_number)) & (set(str(number)))) - pluses
    return [pluses, minuses]


def constrain(pergj, answers, feedbacks):
    leng = len(answers)
    valid = True

    if leng == 0:
        return valid
    else:   
        for i in range(0,leng):
            if match_answer(pergj, answers[i]) != feedbacks[i]:
                valid = False
        
    return valid

#the minimax implementation with some prunning to make it faster
def get_next_guess(all_combs, possible_combs, answers):
    min_guess = 10000
    next_guess = 1234

    for i in all_combs:
        max = -1000

        for j in possible_feedbacks:
            if max > min_guess:
                break
            eliminated = possible_combs.copy()

            for k in possible_combs:
                if max > len(eliminated):
                    break

                elif j != match_answer(k, i):
                    eliminated.remove(k)
            
            if len(eliminated) > max:
                max = len(eliminated)
            
        if max < min_guess and i not in answers:
            min_guess = max
            next_guess = i

    return next_guess

#because the second iteration of the minimax was too slow, I precomputed these values for each feedback that is possible after the starting guess of 1234
def get_second_guess(feedback):
    if feedback == [0,0]:
        return 5067
    elif feedback == [0,1]:
        return 5167
    elif feedback == [0,2]:
        return 2546
    elif feedback == [0,3]:
        return 2015
    elif feedback == [0,4]:
        return 1342
    elif feedback == [1,0]:
        return 1035
    elif feedback == [1,1]:
        return 5236
    elif feedback == [1,2]:
        return 5236
    elif feedback == [1,3]:
        return 1023
    elif feedback == [2,0]:
        return 5014
    elif feedback == [2,1]:
        return 2035
    elif feedback == [2,2]:
        return 1023
    elif feedback == [3,0]:
        return 1035

def check_minus(secret, guess):
    str_secret= str(secret)
    str_guess = str(guess)
    count=0
    for i in range(0,4):
        if str_guess[i] in str_secret and str_guess[i] != str_secret[i]:
            count += 1
    return count

def check_plus(secret, guess):
    str_secret= str(secret)
    str_guess = str(guess)
    count=0
    for i in range(0,4):
        if str_guess[i] == str_secret[i]:
            count += 1
    return count


attempts = []

#play against itself for all possible values
for number in all_combs:

    possible_combs = []
    for i in range(1000,10000):
        if check_dup(i):
            possible_combs.append(i)

    secret = number 
    found = False

    answers = []
    feedbacks = []

    while not found:

        eliminated = possible_combs.copy()

        if answers == []:
            answer_attempt = 1234 #random.choice(possible_combs)
        elif len(answers) == 1:
            answer_attempt = get_second_guess(feedback)
        else:
            answer_attempt = get_next_guess(all_combs, possible_combs, answers)

        answers.append(answer_attempt)

        pluses = check_plus(secret, answer_attempt)
        minuses = check_minus(secret, answer_attempt)

        if int(pluses) == 4:
            feedbacks.append([4,0])
            found = True
            attempts.append(len(answers))

        else:
            feedback = [int(pluses), int(minuses)]
            feedbacks.append(feedback)

            if feedback == [0,0]:
                for i in possible_combs:
                    if len(set(str(answer_attempt)) & (set(str(i)))) > 0:
                        eliminated.remove(i)
                possible_combs = eliminated.copy()       

            for i in possible_combs:   

                if feedback != match_answer(i, answer_attempt):
                    eliminated.remove(i)

            possible_combs = eliminated.copy()

print(attempts)

One possible value which I got 7 attempts was e.g., 9872.

Is there any optimization possible? Or is 7 attempts max as good as it gets in this case?

Note: By minuses I mean correct color, wrong position. By pluses: Correct color, correct position.

0

There are 0 answers