-2

I am trying to implement a binary search to guess a number between 0 and 255 (both inclusive). My guess would be analyzed by a liar function which returns 1 if the key to find is greater than my guess, -1 if the key is less than my guess and 0 if I guessed the right number. However the liar function can lie up to 8 times.

Now the game is controlled by a bigger program (see this repo) which terminates the game as soon as the liar returns 0, so in writing my binary search I don't need to account for an end condition, I just keep guessing till the liar runs out of lies and finally return 0 making the game runner stop everything.

This is my binary search:

import random
from typing import Callable

def Main(rng: random.Random, liar: Callable[[int], int]):
    low = high = guess = res = 0
    
    def makeAguess(counter):
        nonlocal low, high, guess, res
        if (counter % 8) == 0:
            low = 0
            high = 255
            counter = 0
        guess = (low + high) // 2
        res = liar( guess );
        if res > 0:
            low = guess + 1
        else:
            high = guess - 1
        makeAguess( counter + 1 )

    makeAguess( 0 )

A binary search over 256 values is granted to find the key in 8 turns, but since the liar can lie 8 times I need to run at most 9 searches to find the key, so the game should end after at most 8 * 9 = 72 guesses.

However I keep getting:

RecursionError error: maximum recursion depth exceeded in comparison

And if I swap the 8 in if (counter % 8) == 0: with a 9 I get a reasonable:

guessed invalid value: -1

The algorithm seems correct and since that's my very first Python code, I think there is something wrong in how I am using the language.

Anyone can spot the bug?

EDIT:
You can run the game by doing

git clone https://github.com/Artemis21/guessers-and-liars

cd guessers-and-liars/guessers

nano Create theGuesser.py

then pasting my code into the nano editor and saving the file.

Finally run

python main.py -r 1

from the guessers-and-liars directory and wait for the response.

anotherOne
  • 1,513
  • 11
  • 20
  • There is an odd `;` in your code and you're not sharing how you're actually calling this function (or what the value of `rng` and the `liar` function would look like) – Grismar Mar 11 '21 at 23:48
  • 4
    You have no terminating condition. Your program always ends up calling ```makeAguess``` at the end. – lnogueir Mar 11 '21 at 23:49
  • I don't agree with *but since the liar can lie 8 times I need to run at most 9 searches to find the key, so the game should end after at most 8 * 9 = 72 guesses* because you do not know when liar is lying. – Razzle Shazl Mar 11 '21 at 23:50
  • What do we know about pattern of lying? – Razzle Shazl Mar 11 '21 at 23:52
  • @Grismar thanks for the semicolon! But it is still exceeding the maximum recursion depth. As for `rng` and `liar` it's all in the linked repo, I can try to add info on how to run the game – anotherOne Mar 11 '21 at 23:53
  • 2
    When should the function stop? I.e. when is it done guessing? And where in your code does that show? (like @inogueir says, your function never terminates and always ends up calling itself, no matter the values of its variable or the return value of `liar`) – Grismar Mar 11 '21 at 23:54
  • 1
    I would definitely not make this recursive. Recursion is not helping you maintain the code, and recursion is also fatally giving you a recursion depth problem. Binary search can be easily done iteratively without recursion, and is much easier to reason about. – Razzle Shazl Mar 11 '21 at 23:56
  • @RazzleShazl it doesn't matter when the liar function lies. If it doesn't lie for 8 turns after I started a binary search, I will guess the right number. So it has to lie at least once every 8 turns, so at the 9th search it cannot lie anymore. – anotherOne Mar 11 '21 at 23:57
  • @Grismar the function doesn't need to stop, it only needs to make the liar return a `0`, at that point the game runner will stop everything. @RazzleShazi I will try to implement a recursive approach, thanks, but anyway I don't understand why this code isn't working – anotherOne Mar 11 '21 at 23:59
  • What is unclear to me is what happens after your program finds `0`. Because the thread running your `Main` is busy adding stack frames / eating up your memory. Are you sure the larger program is ending your program in a timely fashion? Python is fast! Think thousands of loops in a millisecond, fast. You program found a solution is 20ms, but it probably only take the thread another 200ms to blow your stack limit. I am handwaving with my numbers but they are in the correct ballpark. – Razzle Shazl Mar 12 '21 at 00:02
  • "it only needs to make the liar return a 0" -- but that function NEVER RETURNS. That is the fundamental problem. – Tim Roberts Mar 12 '21 at 00:04
  • You have a recursive function with no end in sight. *NO*, just no. – Razzle Shazl Mar 12 '21 at 00:04
  • By the way, "the liar can lie up to 8 times" does not imply that the 9th try will be real. What if he told the truth 8 times and then decided to lie 8 times? I think you have to pull 25 results and see which one occurred more than 8 times. – Tim Roberts Mar 12 '21 at 00:05
  • From the linked repo: "Your aim is to receive output of 0 from the function after calling it the fewest possible times." and also: "Practically, the guess function will never return 0, it will just end the game. So the guesser only needs to handle a return of -1 or 1." – anotherOne Mar 12 '21 at 00:07
  • Liar will never falsely give you `0` if you guessed incorrectly. Thus, conversely, if liar returns `0` then we **know** it is the correct answer. *If you return 0 when the guess is not equal to your number, your opponent will win anyway* – Razzle Shazl Mar 12 '21 at 00:41

1 Answers1

1

I see you like to play with danger.

The fix is to add a return statement to your recursive function.

You are relying on another thread to (unceremoniously) terminate this Main thread before this Main thread can exceed your stack frame limit. Always be very, very careful with recursive functions that you know precisely when it recurses (deeper) or returns (shallower).


From the link:

If you return 0 when the guess is not equal to your number, your opponent will win anyway

Liar will never falsely give you 0 if you guessed incorrectly. Thus, conversely, if liar returns 0 then we know we guessed correctly.

How does this help us? It means that if the liar lied even once during that guess round, our binary search will go out of bounds because we are now searching in a sub-range that does not contain the target.

You can see this from the output of my sample code below (which either converges on the correct number, or returns -1)


I heard you like iterative solutions:

from random import randint
from typing import Callable

def guessNumber(liar: Callable[[int], int]) -> int:
    l, r = 0, 255
    while l <= r:
        m = (l + r) // 2
        guess = liar(m)
        if guess == -1:
            l = m + 1
        elif guess == 1:
            r = m - 1
        else:
            return m
    return -1

def liar(guess: int, liesLeft: int = 8, answer: int = randint(1, 256)) -> bool:
    if guess > answer:
        result = 1
    elif guess < answer:
        result = -1
    else:
        return 0
    if liesLeft and randint(0, 8) == 0:  # 1 in 8 chance of lying
        liesLeft -= 1
        result = -result
    return result

for i in range(10):
    print(f'attempt #{i}: {guessNumber(liar)}')

Output:

attempt #0: -1
attempt #1: 212
attempt #2: -1
attempt #3: 212
attempt #4: -1
attempt #5: -1
attempt #6: -1
attempt #7: 212
attempt #8: 212
attempt #9: 212

I heard you like recursive solutions. One-liner change:

def guessNumber(liar: Callable[[int], int]) -> int:
    l, r = 0, 255
    while l <= r:
        m = (l + r) // 2
        guess = liar(m)
        if guess == -1:
            l = m + 1
        elif guess == 1:
            r = m - 1
        else:
            return m
    return guessNumber(liar)
Razzle Shazl
  • 1,287
  • 1
  • 8
  • 20
  • I tried to add a `return` in this way: [Try it online!](https://tio.run/##VVBBbsMgEDybV@wlkq2QJk0VKbKaSlXPvfQa9UATbKPixcJYVV7v7gKuGg67wMzODAy30Dl8mmfTD84H8AqvrheNdz2E22CwhYy8KWvVl9VCXHUD78pg6bGt88TDR2wSrFG@/iOfzwbDpwSuVS0K637gBJ1pO2rtpMeRutdcdwJoiYLVe/WtXyNcXtyEQXseLtChdRdlgWRkVJFJRLIGEUwDywCs4FjBiXR5MhvveJvd94cDnxZ6ApdIJfPX0aKC7Rb2hKWY/L4yR6@SJQMvdz4JXsMjXWmbKc@Zcvf6TeaMOmJeh8ljjPLvD2AJSYpkOs@/ "Python 3 – Try It Online") But the code keeps giving the same error. – anotherOne Mar 12 '21 at 00:14
  • @SheikYerbouti added a sample solution that uses an iterative binary search. You can rerun the program and each time it is a different number. – Razzle Shazl Mar 12 '21 at 00:25
  • Just want to comment on my answer about something unrelated to your problem because it can cause confusion for new py developers: `liesLeft` and `answer` in the signature of `def liar` are instantiated with a default value only *once* in the lifetime of the program. It is a quirk of python. I am exploiting this to avoid saving state into a parent class or global namespace. Some reading if you are interested about [Mutable Default Argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument). – Razzle Shazl Mar 12 '21 at 00:30
  • I tried the recursive version and it works. However (as far as I've understood) your `guessNumber()` does only one binary search and then returns `-1`. While in theory I should make a function trying one binary search after the other till the game ends by itself – anotherOne Mar 12 '21 at 00:44
  • @SheikYerbouti yet my output has 10 runs ... try pasting the entirety of my code as-is into a new file and run it. – Razzle Shazl Mar 12 '21 at 00:46
  • but that's because you called the function 10 times. But from the rules in the GitHub repo it seems that your guesser function would be called just once. Anyway thank you very much for the help. I'll try also a recursive approach that does everything with one call and will figure out how the game works. I really appreciate your answer. – anotherOne Mar 12 '21 at 00:52
  • @SheikYerbouti Don't give up now that you are so close to connecting the dots! Write a `guesser` function that you call just once. Then this `guesser` function calls `guessNumber` ad-infinitum until `guessNumber` returns something other than `-1`. Then that is your answer. – Razzle Shazl Mar 12 '21 at 00:55
  • @SheikYerbouti I made a 1-line change to `guessNumber` and it is now recursive like you wanted. – Razzle Shazl Mar 12 '21 at 01:33