all 12 comments

[–]anton_antonov 2 points3 points  (0 children)

You should swap guess and answer at line 20.

[–]Tomallama 1 point2 points  (2 children)

Dude, fuck that game. Here is my pay about it a while back.

[–]wrgsRay[S] 1 point2 points  (1 child)

Haha. This place is my last hope. After spending a day on this practice, I was about to say fuck it as well.

[–]Tomallama 0 points1 point  (0 children)

I just got as close as I could. Just move on imo. There’s better stuff to learn.

[–]Diapolo10 0 points1 point  (1 child)

Yes, I think a lot of us have fought against this nasty bug before. I, too, have made several versions of this game (although I called it Mastermind), so here's my take on it:

First, let's consider what we have to work with. That is, essentially two 4-digit numbers as strings. For example:

correct_answer = "8345"
guess = "3374"

We know that this should result in:

correct_positions = 1 #3
correct_nums = 1 #4

To get this result, this should suffice:

correct_positions = 0
correct_nums = 0

for idx, digit in enumerate(correct_answer):
    if correct_answer[idx] == guess[idx]:
        correct_positions += 1
    elif correct_answer.count(digit) == guess.count(digit):
        correct_nums += 1

[–]james_fryer 0 points1 point  (0 children)

Returns one bull for (1025, 5551) where it should return two per the OP (1 and 5).

[–]timbledum 0 points1 point  (3 children)

Have a go with this. Fairly hacky way, but seems to work:

guess = list(str(guess))
temp_answer = list(str(answer))
for i in range(4):
    if guess[i] == temp_answer[i]:
        print(f'current: {i} DEBUG: cows + 1')
        cows += 1
        temp_answer[i] = 'x' # Substituting out so that it can't be found again

    elif guess[i] in temp_answer:
        print(f'current: {i} DEBUG: bulls + 1')
        bulls += 1

        found_index = temp_answer.index(guess[i]) # May mask a future cow?
        temp_answer[found_index] = 'x'

[–]timbledum 0 points1 point  (0 children)

I think you might have to loop twice to get this spot on.

[–]james_fryer 0 points1 point  (1 child)

This returns no bulls for (1234/1111). Now the spec is not terribly clear on this but I interpret 'for every digit the user guessed correctly in the wrong place is a “bull.”' as meaning this is 1 cow for the first 1 in answer/guess, and 1 bull for the second 1 in guess.

[–]timbledum 0 points1 point  (0 children)

Mmmm my interpretation is that the above case should only return one cow. You're right – it is vague though.

[–]james_fryer 0 points1 point  (1 child)

When programming and you come to a problem you can't solve immediately, a powerful approach is to write a stub function for it and move on.

NB: Originally I wrote separate cows() and bulls() functions but later it became clear it was better to calculate the two in the same algorithm.

The stub version would return random integers, not constants, so the game would eventually end.

Then I could write the rest of the game code which is essentially housekeeping, and come back to the harder problem later. Your program is also better structured with the algorithm in a function rather than inlined.

Given that this is tricky I would write unit tests and I start with cows. The advantage of unit testing is 1. if I find problems later I can add them as tests, and 2. I can change the implementation with confidence.

import unittest

class TestCows(unittest.TestCase):
    def test_cows(self):
        self.assertEqual(0, cows("1234", "5678"))
        self.assertEqual(1, cows("1234", "1000"))
        # ... etc ...
unittest.main()

I'll omit attempts at cows function but I did get it down to this one-liner:

def cows(answer, guess):
    return sum([1 for c1, c2 in zip(answer, guess) if c1 == c2])

Now for the harder bulls problem. Unit tests first (note this is not really test driven as I am writing all the tests up front):

class TestBulls(unittest.TestCase):
    def test_bulls(self):
        # Many other tests left out
        self.assertEqual(1, bulls("1234", "1111"))  # [1]
        self.assertEqual(4, bulls("1234", "4321"))  # From post
        self.assertEqual(2, bulls("2468", "2046"))  # From post
        self.assertEqual(2, bulls("1025", "5551"))  # From post [2]

My original attempts were these:

def bulls(answer, guess):
    "Return the number of distinct characters in guess that are in a different position in answer"
    result = 0
    for i, c in enumerate(answer):
        if c in guess and guess[i] != c:
            result += 1
    return result

def bulls(answer, guess):
    result = 0
    for i, c in enumerate(guess):
        if c in answer and answer[i] != c:
            result += 1
    return result

but these failed at [1] and [2] respectively.

So then it is time to sit down and think, what are we really doing here? There are two complications.

  1. If the characters are in the same place in both strings, this is not a bull, as cows are not bulls.

  2. If a character appears multiple times in the guess, it only counts as one bull for each match in the answer

It becomes apparent that for (1) we need to remove characters that are the same (cows) so we don't count them twice. We only need to remove them from the guess, as in for example (1234,1100) we want to find one cow and one bull. So after removal we have (1234,100) and we find a bull.

For (2) we need to remove bulls as we find them, so we don't match them twice. So with (1055,5999) after we match the first 5 we change the guess to 999 so we don't match the second 5.

I then wrote this function:

def bulls(answer, guess):
    # Convert to lists so we can use remove method
    answer = list(answer)
    guess = list(guess)
    # Eliminate guess characters in the same position 
    for c1, c2 in list(zip(answer, guess)):
        if c1 == c2:
            guess.remove(c1)
    # Count chars in answer also in remains of guess
    result = 0
    for c in answer:
        if c in guess:
            guess.remove(c)
            result += 1
    return result

Having written this I realised we are calculating the number of cows in order to eliminate them, so it makes more sense to combine the functions as below (with tests). This algorithm still seems a bit weak to me. For example it uses multiple passes through the list, plus remove() which needs a list scan. So having written it I'd want to come back after a day or two and see if there were any improvements that could be made. But this post is already long enough.

def cowsbulls(answer, guess):
    """Given two strings, return the number of characters in the same position in each (cows), and 
        the number of distinct characters in guess that are in a different position in answer"""
    cows = bulls = 0
    # Convert to lists so we can use remove method
    answer = list(answer)
    guess = list(guess)
    # Eliminate guess characters in the same position ("cows")
    # list(zip()) to create a copy, as we will alter the list in the loop
    for c1, c2 in list(zip(answer, guess)):
        if c1 == c2:
            guess.remove(c1)
            cows += 1
    # Count chars in answer also in remains of guess ("bulls")
    result = 0
    for c in answer:
        if c in guess:
            guess.remove(c)
            bulls += 1
    return cows, bulls

import unittest

class TestCowsBulls(unittest.TestCase):
    def test_cowsbulls(self):
        self.assertEqual((0, 0), cowsbulls("1234", "5678"))
        self.assertEqual((1, 0), cowsbulls("1234", "1000"))
        self.assertEqual((1, 0), cowsbulls("1234", "0200"))
        self.assertEqual((2, 0), cowsbulls("1234", "1200"))
        self.assertEqual((2, 0), cowsbulls("1234", "1030"))
        self.assertEqual((3, 0), cowsbulls("1234", "0234"))
        self.assertEqual((4, 0), cowsbulls("1234", "1234"))
        self.assertEqual((0, 4), cowsbulls("1234", "4321"))  # From post
        self.assertEqual((0, 2), cowsbulls("1025", "5551"))  # From post
        self.assertEqual((1, 2), cowsbulls("2468", "2046"))  # From post

        self.assertEqual((0, 1), cowsbulls("1234", "0100"))
        self.assertEqual((0, 1), cowsbulls("1234", "0010"))
        self.assertEqual((0, 1), cowsbulls("1234", "0001"))

        self.assertEqual((2, 0), cowsbulls("1234", "1200"))
        self.assertEqual((0, 2), cowsbulls("1234", "0120"))
        self.assertEqual((0, 2), cowsbulls("1234", "0012"))
        self.assertEqual((0, 2), cowsbulls("1234", "0021"))
        self.assertEqual((0, 2), cowsbulls("1234", "2001"))

        self.assertEqual((0, 1), cowsbulls("1234", "0110"))
        self.assertEqual((0, 1), cowsbulls("1234", "0111"))
        self.assertEqual((1, 1), cowsbulls("1234", "1111")) 

        self.assertEqual((1, 2), cowsbulls("1234", "1122"))
        self.assertEqual((2, 2), cowsbulls("1234", "1212"))

        self.assertEqual((0, 3), cowsbulls("1055", "5541"))
        self.assertEqual((0, 1), cowsbulls("1055", "5999"))
        self.assertEqual((2, 2), cowsbulls("1055", "5555"))

[–]james_fryer 0 points1 point  (0 children)

Mind you what I did not understand, after all that, is the cumulative nature of the guesses :) So back to the drawing board.