simple game example

Started by
6 comments, last by Zahlman 16 years, 10 months ago
Hello, here's a simple game I coded up for someone I'm helping. Maybe some of you can find it useful:

// A simple text-based game of hangman

// This code is public domain. You can do what you want with it.

#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>


class Game {
	const std::string& word;
	std::string letters_picked;
	std::string remaining_letters;
	int chances_left;
	bool won_;

public:

	Game(const std::string& word, int chances):
		word(word), chances_left(chances), won_(false) {

		for(int i=0; i<word.length(); i++) {
			char letter = word;

			// Don't put duplicates in
			if(remaining_letters.find(letter, 0) == std::string::npos) {
				remaining_letters.push_back(letter);
			}
		}
	}

	// Print out the word with correct guesses filled in and everything else as blanks
	void print_word() {
		for(int i=0; i<word.length(); i++) {
			char letter = word;

			if(letters_picked.find(letter, 0) != std::string::npos) {
				std::cout << letter << " ";
			} else {
				std::cout << "_ ";
			}
		}
	}


	// Print each letter the user has picked, separated by spaces
	void print_letters_picked() {
		for(int i=0; i<letters_picked.length(); i++) {
			std::cout << letters_picked << " ";
		}
	}


	// Update the game with the given input
	void try_letter(char letter) {
		letters_picked.push_back(letter);

		// If the letter is correct, remove that letter from the remaining_letters
		// and check to see if they won.
		int pos = word.find(letter, 0);
		if(pos != std::string::npos) {
			remaining_letters.erase(0, pos);

			if(remaining_letters.length() == 0) {
				won_ = true;
			}
		} else {
			chances_left--;
		}
	}

	bool done() {
		return chances_left == 0 || won_;
	}

	bool won() {
		return won_;
	}

	int get_chances() {
		return chances_left;
	}
};


const char* word_list[] = {
	"pickles",
	"soup",
	"Curtis",
	"duck",
	"salad",
	"onions",
	"building",
	"tower",
	"tomato",
	"plate",
	"planet",
	"universe",
	"multiverse",
	"hyperbola",
	"displacement",
	"differential",
	"hypertension",
	"at",
	"the",
	"those",
	"people",
	"eat",
	"severed",
	"heads"
};

const int word_list_length = sizeof(word_list) / sizeof(word_list[0]);


// Print out the given word with spaces between each letter
void print_word(const std::string& word) {
	for(int i=0; i<word.length(); i++) {
		std::cout << word << " ";
	}
}


// Return whether we should start a new game or not
bool ask_continue() {
	while(true) {
		std::cout << std::endl << "Continue playing (y/n)? ";

		char response;
		std::cin >> response;

		if(response == 'y' || response == 'Y') {
			return true;
		} else if(response == 'n' || response == 'N') {
			return false;
		}
	}
}


// Play a single round of a game of hang man
void play(Game& game) {
	std::system("cls");

	game.print_word();

	std::cout << "     ";
	game.print_letters_picked();

	std::cout << std::endl << std::endl << "Body parts left: " << game.get_chances();

	std::cout << std::endl << std::endl << "Letter: ";
	char letter;
	std::cin >> letter;

	game.try_letter(letter);
}


int main() {
	std::srand( std::time(0) );

	bool continue_playing = false;
	
	do {
		// Pick a random word to use
		std::string word = word_list[ rand() % word_list_length ];

		Game game(word, 6);

		while(!game.done()) {
			play(game);
		}

		std::system("cls");

		print_word(word);

		std::cout 
			<< std::endl << std::endl
			<< (game.won() ? "Congratulations! You won." : "Congratulations! You lost.")
			<< std::endl;

		continue_playing = ask_continue();

	} while(continue_playing);

	return 0;
}




edit: STLified version courtesy of Zahlman: (Thanks!)

// A simple text-based game of hangman

// This code is public domain. You can do what you want with it.

#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <algorithm>
#include <iterator>

// Print out the given word with spaces between each letter
void print_word(std::ostream& where, const std::string& word) {
	std::copy(word.begin(), word.end(),
	          std::ostream_iterator<char>(where, " "));
}

class Game {
	const std::string word;
	std::string progress;
	std::string picked;
	int chances;

	bool won() const {
		return progress == word;
	}

	public:
	Game(const std::string& word, int chances):
	word(word), progress(word.length(), '_'),
	picked(), chances(chances) {}

	friend std::ostream& operator<<(std::ostream& os, const Game& g) {
		if (g.done()) {
			print_word(os, g.word);
			os << "\n\n"
			   << (g.won() ? "Congratulations! You won." :
			                 "Congratulations! You lost.")
			   << "\n";
		} else {
			print_word(os, g.progress);
			os << "     ";
			print_word(os, g.picked);
			os << "\n\nBody parts left: " << g.chances << "\n";
		}
		return os;
	}

	void guess(char letter) {
		bool found = false;
		picked.push_back(letter);
		// This can be done with standard library algorithms, but
		// requires more setup work than it's really worth, and won't
		// really generate any good reusable components.
		int len = progress.length();
		for (int i = 0; i < len; ++i) {
			if (word == letter) {
				found = true;
				progress = letter;
			}
		}
		if (!found) { --chances; }
	}

	bool done() const {
		return chances == 0 || won();
	}
};

// TODO: Read words in from a file.
const char* word_list[] = {
	"pickles",
	"soup",
	"Curtis",
	"duck",
	"salad",
	"onions",
	"building",
	"tower",
	"tomato",
	"plate",
	"planet",
	"universe",
	"multiverse",
	"hyperbola",
	"displacement",
	"differential",
	"hypertension",
	"at",
	"the",
	"those",
	"people",
	"eat",
	"severed",
	"heads"
};

const int word_list_length = sizeof(word_list) / sizeof(word_list[0]);

char get_letter() {
	std::string response;
	std::getline(std::cin, response);
	return (response + '\n')[0];
}

// Return whether we should start a new game or not
bool ask_continue() {
	while (true) {
		std::cout << "Continue playing (y/n)? ";
		switch (get_letter()) {
			case 'y': case 'Y': return true;
			case 'n': case 'N': return false;
		}
	}
}

int main() {
	std::srand(std::time(0));

	do {
		Game game(word_list[std::rand() % word_list_length], 6);

		while (true) {
			std::system("cls");
			std::cout << game;
			if (game.done()) { break; }
			std::cout	<< "\nLetter: ";
			game.guess(get_letter());
		}
	} while (ask_continue());
}

Very important: This example was meant to show the basic structure of a game. It uses a very minimal subset of the STL so as not to obfuscate this purpose. Do not code like this! Here's what I left out for this reason:
  • std::set should be used instead of a std::string for letters_picked and remaining_letters in Game.
  • All of the routines that use cout should accept a std::ostream as a parameter instead.
  • Iterators should always be used instead of indexes for STL containers.
  • The for loops to print out elements should be replaced with std::copy and ostream_iterators. Outputting the blanks for unknown letters should be done using std::replace--it might be handy to just do this in try_letter (Normally it makes more sense to just keep track of the incomplete word using an array with "holes" in it, but C++ makes it rather hard to do it the proper way).
(If someone wants to create a "proper" version with the above changes implemented, I'll gladly update this post and place it under the simplified version.) Some notes on the program design:
  • I intentionally used a crippled form of MVC for the Game class. The model and view were fused together for simplicity; I left the controller (play) out because doing so didn't add much complexity.
  • That being said, all of the print member functions should not normally be in the main class, unless removing them would add a large amount of complexity and if the flexibility gains aren't needed.
  • Each function was designed so that each statement would be at the same level of abstraction. Originally I had play embedded in main, which is an example of what not to do. (The Game constructor currently violates this, which can be remedied by using a std::set) If you want to know more about this, I recommend you buy Code Complete, an excellent book on programming style and code design.
Finally, I'd like to add that, unless you have a very good reason to C++, you should consider learning how to program using a different language. If you're already pretty far along in C++, I still suggest you only write a few games in C++, just to get a hang of the language, then switch to something more powerful (and simpler), such as python. If you know a decent amount of C++ already, I recommend Dive Into Python. Otherwise, take a look at How to Think Like a Computer Scientist. As always, google is your friend :) Hope this helps, Curtis edits: - Ugh, I hate 8 space tabs. - Linkified MVC. [Edited by - cw on June 11, 2007 11:54:04 AM]
Advertisement
Very nice! This appears to be a wonderful learning source.
Now if you want to write one in C#, I wouldn't mind that either! :)
Thanks for the code and the explenation, but i have a question. I just compiled it and found out that when i enter 'v' as a letter i always win :/ i've never used stl so i'm inexperienced with that. :)
Thanks for the feedback, guys :)

Quote:Original post by Oguz286
Thanks for the code and the explenation, but i have a question. I just compiled it and found out that when i enter 'v' as a letter i always win :/ i've never used stl so i'm inexperienced with that. :)

I don't know what to say, it works for me:




I'm compiling it with Visual C++ 2003, so it should work as long as you're not using VC++6. Are you're sure you actually won? It does say "Congratulations! You lost." when you lose.
Quote:Original post by cw
I'm compiling it with Visual C++ 2003, so it should work as long as you're not using VC++6. Are you're sure you actually won? It does say "Congratulations! You lost." when you lose.


Nope i was wrong about which letter it is. It really weird. If i get a wrong letter the numbers of limbs decreases by 1 (which is normal beheaviour) but when i get a good letter i win (and yes it says "congratulations, you won!"). I don't know what's causing it. By the way i'm compiling with VS2005 but it shouldn't make any difference.
Quote:Original post by Oguz286
Quote:Original post by cw
I'm compiling it with Visual C++ 2003, so it should work as long as you're not using VC++6. Are you're sure you actually won? It does say "Congratulations! You lost." when you lose.


Nope i was wrong about which letter it is. It really weird. If i get a wrong letter the numbers of limbs decreases by 1 (which is normal beheaviour) but when i get a good letter i win (and yes it says "congratulations, you won!"). I don't know what's causing it. By the way i'm compiling with VS2005 but it shouldn't make any difference.

I see. Maybe it got messed up when you copied and pasted it?
Quote:Original post by cw
I see. Maybe it got messed up when you copied and pasted it?


I copy pasted it again but to no avail. But sometimes it works as it's supposed to but sometimes i win when i still have a bunch of letters to go and i just guess one of them. Could it have something to do with memory? Because it doesn't misbehave everytime, but at random (or so it seems).

EDIT: It happened again with the word 'the'. First i entered 'e' and it appeared at the right place, then i entered 'h' and it showed 'the' and 'congratulations! you won.' without me entering 'the'. Same thing for 'hypertension', i just entered 'n' and i won immediatly. I'll see if i can find out why it doesn't always work.
I also reproduced the problem. I believe the error is here:

			remaining_letters.erase(0, pos);


This removes all letters from remaining_letters up until the found one (not including the found one) - not that letter itself.

Also, I don't think you're really allowed to hold the 'word' member by reference like that; the underlying referred-to word is a temporary (constructed from the char* selected out of the word list) and I don't believe its lifetime is guaranteed to be extended as needed.

This version tracks the user's progress by building a separate string containing underscores for unguessed letters and actual letters for guessed ones. We know we're done when this matches the original word. This actually simplifies the logic quite a bit. I also reuse the global print_word() functionality within the class (this is a good thing). Also, I have simplified the interface to the class: since all the "print" functions are called as a unit and there's no need to call them separately, they get joined together so that the caller only makes one call. (In general, you want to simplify your code - but while simplification of implementations often involves making more functions, simplifying the interface almost always involves making fewer - at least, fewer *public* ones).

I made a few other changes; feel free to review. I haven't done everything that might be done, and I may well have done things that not everyone would agree with ;)

// A simple text-based game of hangman// This code is public domain. You can do what you want with it.#include <iostream>#include <string>#include <cstdlib>#include <ctime>#include <algorithm>#include <iterator>// Print out the given word with spaces between each lettervoid print_word(std::ostream& where, const std::string& word) {	std::copy(word.begin(), word.end(),	          std::ostream_iterator<char>(where, " "));}class Game {	const std::string word;	std::string progress;	std::string picked;	int chances;	bool won() const {		return progress == word;	}	public:	Game(const std::string& word, int chances):	word(word), progress(word.length(), '_'),	picked(), chances(chances) {}	friend std::ostream& operator<<(std::ostream& os, const Game& g) {		if (g.done()) {			print_word(os, g.word);			os << "\n\n"			   << (g.won() ? "Congratulations! You won." :			                 "Congratulations! You lost.")			   << "\n";		} else {			print_word(os, g.progress);			os << "     ";			print_word(os, g.picked);			os << "\n\nBody parts left: " << g.chances << "\n";		}		return os;	}	void guess(char letter) {		bool found = false;		picked.push_back(letter);		// This can be done with standard library algorithms, but		// requires more setup work than it's really worth, and won't		// really generate any good reusable components.		int len = progress.length();		for (int i = 0; i < len; ++i) {			if (word == letter) {				found = true;				progress = letter;			}		}		if (!found) { --chances; }	}	bool done() const {		return chances == 0 || won();	}};// TODO: Read words in from a file.const char* word_list[] = {	"pickles",	"soup",	"Curtis",	"duck",	"salad",	"onions",	"building",	"tower",	"tomato",	"plate",	"planet",	"universe",	"multiverse",	"hyperbola",	"displacement",	"differential",	"hypertension",	"at",	"the",	"those",	"people",	"eat",	"severed",	"heads"};const int word_list_length = sizeof(word_list) / sizeof(word_list[0]);char get_letter() {	std::string response;	std::getline(std::cin, response);	return (response + '\n')[0];}// Return whether we should start a new game or notbool ask_continue() {	while (true) {		std::cout << "Continue playing (y/n)? ";		switch (get_letter()) {			case 'y': case 'Y': return true;			case 'n': case 'N': return false;		}	}}int main() {	std::srand(std::time(0));	do {		Game game(word_list[std::rand() % word_list_length], 6);		while (true) {			std::system("cls");			std::cout << game;			if (game.done()) { break; }			std::cout	<< "\nLetter: ";			game.guess(get_letter());		}	} while (ask_continue());}

This topic is closed to new replies.

Advertisement