Skip to main content
GameDev.net gamedev.net
🔒 Locked

Guess The Number (Reversed Roles)

Started by DevFred Apr 28, 2008 at 12:27 PM 6 replies 900+ views
Original Post
DevFred
DevFred
I've been away from C++ for a couple of years, and today I made a little game. Did I screw up badly somewhere? :)

#include <cassert>
#include <iostream>
#include <string>
using namespace std;

const int LOWER = 1;
const int UPPER = 255;

class Guesser
{
private:
    int lower;
    int upper;
    int guess;
    int tries;

    void makeEducatedGuess()
    {
        guess = lower + ((upper - lower) >> 1);
        assert (lower <= guess);
        assert (guess <= upper);
        ++tries;
    }

public:
    Guesser(int lower, int upper)
    : lower(lower), upper(upper), tries(0)
    {
        assert (lower < upper);
        makeEducatedGuess();
    }

    int currentGuess() const
    {
        return guess;
    }

    int numberOfTries() const
    {
        return tries;
    }

    void correct()
    {
        lower = upper = guess;
    }

    void tooLow()
    {
        lower = guess + 1;
        makeEducatedGuess();
    }

    void tooHigh()
    {
        upper = guess - 1;
        makeEducatedGuess();
    }

    bool fuzzy() const
    {
        return lower != upper;
    }

    int answer() const
    {
        assert (!fuzzy());
        return lower;
    }
};

bool play()
{
    cout << "Think of a number between " << LOWER
         << " and " << UPPER << "." << endl << endl;
    Guesser guesser(LOWER, UPPER);
    while (guesser.fuzzy())
    {
        cout << "Is it " << guesser.currentGuess()
             << "? ([Y]es / too [L]ow / too [H]igh): ";
        string answer;
        getline(cin, answer);
        if (answer.size() == 0) return false;
        switch (answer[0])
        {
        case 'y':
            guesser.correct(); break;
        case 'l':
            guesser.tooLow(); break;
        case 'h':
            guesser.tooHigh(); break;
        }
    }
    cout << endl << "The number you were thinking of is "
         << guesser.answer() << " (" << guesser.numberOfTries()
         << " tries)" << endl << endl << endl;
    return true;
}

int main()
{
    while (play());
    cout << endl << "Goodbye." << endl;
}

Dave Hunt
Dave Hunt
I don't know. What's it doing that it's not supposed to? Or what's it not doing that it is supposed to? A bit more detail regarding your problem would go a long way toward getting some useful help.

edit - nevermind. I got the impression for your last sentence that it wasn't working. I didn't realize you were asking for a critique.


[Edited by - Dave Hunt on April 28, 2008 3:31:05 PM]
Harry Hunt
Harry Hunt
Let'see

1)
guess = lower + ((upper - lower) >> 1);


The >> 1 is unnecessarily obfuscated. Why don't you just write / 2? Clearly performance isn't an issue here (and those kinds of "optimizations" are best left to the compiler)

2) You should re-think your method names. Usually using verbs makes it more obvious what a method does than using adjectives or nouns.

3) Your switch misses a default. If the user types in something other than y/l/h, the sate of your system will be undefined.

Apart from that, nice job.
Zahlman
Zahlman
Pretty good. :) I have to reach pretty deep into the bag of constructive criticism to come up with:

1) Simplify arithmetic: you're not looking for the lesser value plus half the difference; you're looking for the average. Thus, '(lower + upper) / 2' suffices.

2) The guesser should probably not be responsible for tracking how many times it guessed.

3) Instead of calling makeEducatedGuess() in several places so that the "guess" is ready and waiting in currentGuess(), why not do the calculation on demand instead, i.e. in currentGuess() itself? (abstract theory idea: just because you are using a programming paradigm that's meant to let you manage large amounts of state more easily, doesn't mean you shouldn't still be trying to minimize state.)

4) The guesser doesn't really need to confirm to itself when the answer is correct.

5) Why not parameterize the game instead of just letting it use the two constant sizes? Even if you just pass in hard-coded parameters, you at least have flexibility that you could use later.

6) The game-quit mechanism is a bit suspect. You should really only quit the game between rounds I think.

7) Check for empty strings with .empty(), as you would with other standard library containers.

8) You should be able to detect if the user cheats.

9) Don't use std::endl as a substitute for newlines. It also flushes the buffer.

#include <string>#include <cassert>#include <iostream>using namespace std;const int LOWER = 1;const int UPPER = 255;class Guesser{    int lower, upper;    public:    Guesser(int lower, int upper)    : lower(lower), upper(upper)    {        assert (lower < upper);    }    int guess() const    {        int g = (lower + upper) / 2;        assert ((lower <= g) && (g <= upper));        return g;    }    bool tooLow() // return whether this is valid (i.e. false if cheated).    {        lower = guess + 1;        return lower <= upper;    }    bool tooHigh() // idem    {        upper = guess - 1;         return upper >= lower;    }};// I'll factor out a helper function for getting a single-character input.// I'm using the convention that .get() on streams uses of returning -1 for// empty input and the char's unsigned value otherwise.int prompt() {    string input;    getline(cin, input);    // You could also add lowercasing logic here maybe.    return input.empty() ? -1 : input[0];}typedef enum { NOTDONE, DONE, CHEATED } game_state;game_state testGuess(const Guesser& g){    while (true) { // will be exited when we get valid input        cout << "Is it " << g.guess()             << "? ([Y]es / too [L]ow / too [H]igh): ";        switch (prompt()) // the -1 case will just fall through.        {            case 'y': return DONE;            case 'l': return g.tooLow() ? NOTDONE : CHEATED;            case 'h': return g.tooHigh() ? NOTDONE : CHEATED;        }        cout << "Didn't understand that" << endl;    }}void play(int lower, int upper){    cout << "Think of a number between " << lower         << " and " << upper << "." << endl << endl;    Guesser guesser(lower, upper);    int tries = 0;    game_state state = NOTDONE;    while (state == NOTDONE) { testGuess(guesser); ++tries; }    if (state == CHEATED) {        cout << "\nYou cheated!!!! D:"    } else {        cout << "\nThe number you were thinking of is "             << guesser.guess() << " (" << tries << " tries)\n\n" << endl;    }}bool play_again() {    while (true) { // will be exited when we get valid input        cout << "Play again?";        switch (prompt()) // the -1 case will just fall through.        {            case 'y': return true;            case 'n': return false;        }        cout << "Didn't understand that" << endl;    }}int main(){    do { play(LOWER, UPPER); } while (play_again());    cout << "\nGoodbye." << endl;}
DevFred
DevFred
Quote:
Original post by Harry Hunt
The >> 1 is unnecessarily obfuscated. Why don't you just write / 2?

Because >>1 and /2 behave differently on signed values, and I prefer the >>1 way of rounding down instead of towards zero.

Quote:
Original post by Harry Hunt
3) Your switch misses a default. If the user types in something other than y/l/h, the sate of your system will be undefined.

Really? I always thought a switch without a fitting case would be a no-op.

Thanks for your feedback!

Quote:
Original post by Zahlman
1) Simplify arithmetic: you're not looking for the lesser value plus half the difference; you're looking for the average. Thus, '(lower + upper) / 2' suffices.

Yeah, but (lower + upper) / 2 can overflow. The other formula never overflows. Remember the binary search bug in Java pre 1.6? :)

Quote:
Original post by Zahlman
3) Instead of calling makeEducatedGuess() in several places so that the "guess" is ready and waiting in currentGuess(), why not do the calculation on demand instead, i.e. in currentGuess() itself?

Because the guess is needed by tooLow and tooHigh - the guesser needs to know WHAT is too low or too high.

Quote:

7) Check for empty strings with .empty(), as you would with other standard library containers.

Ah, thanks.

Quote:

8) You should be able to detect if the user cheats.

The user simply cannot cheat in my version.

Quote:

9) Don't use std::endl as a substitute for newlines. It also flushes the buffer.

Again, thanks.
Harry Hunt
Harry Hunt
Quote:
Original post by DevFred
Really? I always thought a switch without a fitting case would be a no-op.


It is, but I don't mean it in an assembly kind of sense... if the user presses an incorrect key, the game will simply continue but neither of the correct/tooLow/tooHigh methods will be invoked. This leaves your game in an undefined state. Sure, the "Guesser" will simply come up with the same guess again, but it would be nice if the game just displayed a message like "Wrong key, try again"...

Zahlman
Zahlman
Quote:
Quote:

3) Your switch misses a default. If the user types in something other than y/l/h, the sate of your system will be undefined.

Really? I always thought a switch without a fitting case would be a no-op.

Thanks for your feedback!


He's wrong, BTW. It will behave as you expect, and as you want in this case.

Quote:
Original post by DevFred
Quote:
Original post by Harry Hunt
The >> 1 is unnecessarily obfuscated. Why don't you just write / 2?

Because >>1 and /2 behave differently on signed values, and I prefer the >>1 way of rounding down instead of towards zero.

Quote:
Original post by Zahlman
1) Simplify arithmetic: you're not looking for the lesser value plus half the difference; you're looking for the average. Thus, '(lower + upper) / 2' suffices.

Yeah, but (lower + upper) / 2 can overflow. The other formula never overflows. Remember the binary search bug in Java pre 1.6? :)


You're worried about these things when you have a hard-coded range of 1-255?

Quote:
Quote:
Original post by Zahlman
3) Instead of calling makeEducatedGuess() in several places so that the "guess" is ready and waiting in currentGuess(), why not do the calculation on demand instead, i.e. in currentGuess() itself?

Because the guess is needed by tooLow and tooHigh - the guesser needs to know WHAT is too low or too high.


But it knows: the thing that is too low or too high is the guess that it just previously made. If you do things that way, you can call guess() a zillion times in a row and it will return the same thing each time, so you just call guess() within tooLow or tooHigh to find out what the new bounds are. (In my code, I have 'guess' as a typo for 'guess()'. Well, more accurately, something I overlooked while editing.)

Quote:

8) You should be able to detect if the user cheats.

The user simply cannot cheat in my version.


I don't think you understand. Suppose, for example, that the user always claims the guess is too low, even if the guesser guesses 255?
DevFred
DevFred
Quote:
Original post by Zahlman
You're worried about these things when you have a hard-coded range of 1-255?

Yeah I know it's stupid, but I just like >>1 more than /2 :)

Quote:

If you do things that way, you can call guess() a zillion times in a row and it will return the same thing each time, so you just call guess() within tooLow or tooHigh to find out what the new bounds are.

Ah, you mean I could just calculate the guess again and again, because as long as the bounds don't change, it will return the same guess? That works as long as the guess is deterministic, yes. But what if I want to add a little randomness, so the computer doesn't seem like... a computer? :)

Quote:
Quote:
Quote:

8) You should be able to detect if the user cheats.

The user simply cannot cheat in my version.

I don't think you understand. Suppose, for example, that the user always claims the guess is too low, even if the guesser guesses 255?

The guesser never actually guesses 255, because after having guessed 254 and knowing it's too low, the two bounds will match and the guesser will KNOW it must be 255. Just try it :)

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.