0

How could I explain how and when to use an if-statement vs. a switch statement in a "Choose Your Own Adventure" game?

The game is basically structured that if you put in a certain answer, it changes the flow or direction of the game. I've usually used if-statements with functions, but is that the most efficient and simple way to teach?

Thanks!

Edit: Wow, thank you SO much for so many great answers!! Just one last note: if you were a 13-year-old trying to grasp this concept without any previous knowledge of programming, how would you try to go about to understand it? Seriously, thanks so much for the help!!

m00nbeam360
  • 1,357
  • 2
  • 21
  • 36
  • 2
    Are you a teacher? Why would you teach against using switch statements? It's rarely a question of efficiency, but a switch can often be optimized to a jump table. Long if/else branches are more clearly expressed as a switch in many circumstances (i.e., checking equality of numeric types). – Ed S. Jun 13 '13 at 23:39
  • 2
    Not exactly a teacher, but thanks for the help! – m00nbeam360 Jun 13 '13 at 23:40
  • First thing I thought of was **[this](http://smrtdsgn.com/wp-content/uploads/2010/04/infographiclarge_v2.png)** (which can be found **[here](http://stackoverflow.com/questions/3333372/need-to-create-a-choose-your-own-adventure-type-guide-best-approach-to-use)**). With any choose-your-own-adventure, there are a lot more if-else clauses than anything else; as per the diagram. That being said, if you have numerical values, switch statements are the way to go. – kgdesouz Jun 13 '13 at 23:41
  • 1
    If you're comparing values to enums, switch. Otherwise, if-elseif. – Mooing Duck Jun 13 '13 at 23:41
  • 1
    Because of the title of your question and the C++ tag, I would argue that the correct answer is: "Neither". if and switch are for hard-coded logic conditions. If you are doing this in C++ you should be looking for a data-driven solution. – kfsone Jun 14 '13 at 00:18
  • While this question isn't a perfect fit, given the responses which have been contributed, it should be open. – Chris Stratton Jun 14 '13 at 17:24

6 Answers6

3

There are (give or take) five different solutions to "I've received this input, what do I do now".

  1. If/else if/else ... chain. Advantage is that you can use any expression that can be made into true or false. Disadvantage is that it can get pretty messy when you have a long chain of them.
  2. Switch - great for "there are lots of almost similar things to do". Drawback is that the case labels have to be integer values (or char values, but not strings, floating point values, etc).
  3. A table which makes the long if/else into a much simpler if(table[index].something) ...
  4. Function pointers - sort of a table pointer variant - store a pointer to the function that does whatever you want to do if you move in that direction.
  5. Objects using virtual functions. Again, a variant on the table solution, but instead of storing function pointers, we store objects with some member function that we can use to "do whatever you need to do".

The correct solution in this case is perhaps a combination/variation on one of the latter 3 - in my opinion, at least.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • I like the detail of this answer, however, for a 13 year old, I think that everything beyond point 3 would be beyond their comprehension. Also, for `switch` (point 2), I think that `great for "there are lots of almost similar things to do"` is not correct. I would cahnge this to `great for doing many compares against one integer value`, since you don't need to do many similar things. You can do many completely different things depending on one value. – Adrian Jun 14 '13 at 02:55
2

Use a switch statement when you're identifying cases of numeric or ordinal values.

switch (number)
{
    case 1: DoSomething();
        break;

    case 2: DoSomethingElse();
        break;
}

Use if, elseif and else for more complex conditions, like numeric ranges.

if (number > 0 && number <= 100)
{
   DoSomething();
}
else if (number > 100 && number <= 1000)
{
   DoSomethingElse()
}
else
{
   NotifyOutOfRange();
}
Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
  • Would this also apply to strings? Let's say they're looking for a specific direction, like "left." Would a switch be better? – m00nbeam360 Jun 13 '13 at 23:42
  • I modified the wording slightly. – Robert Harvey Jun 13 '13 at 23:44
  • 2
    @m00nbeam360 you can't use a switch on strings, only integer types. – David Brown Jun 13 '13 at 23:46
  • Although you can parse the string into one of the values from an `enum`, then use that in the `case`. If you have a limited number of options, that may be a good idea. – ssube Jun 13 '13 at 23:52
  • You must have a `break;` statement at the end of each switch statement or the execution will fall through to the next case statement below. – Adrian Jun 14 '13 at 06:00
  • @Adrian: One of the reasons I've never cared much for `switch` statements. In C#, I generally `return` in each `case`, so that the `break` statements can be omitted. – Robert Harvey Jun 14 '13 at 15:13
2

In this very scenario you've described an if statement is pretty much your best option since the code needs to compare an answer provided by the user with some pre-defined options.

Those options will most likely be strings. switch statements in C++ cannot work on strings. Thus a series of if statements will probably be simpler.

A switch statement can be used when the answer only consists of a number or a single character.

For example, the code piece for the game's main menu could look like this:

Console output:

Please select an action:
1) Start a new game.
2) Go to options screen.
3) Quit game.

code:

int userChoice = getUserInput();

switch(userChoice){
case START_NEW_GAME: //1
    startGame(); break;
case OPTIONS: //2
    showOptions(); break;
case QUIT: //3
    exit(); break;
}
s3rius
  • 1,442
  • 1
  • 14
  • 26
  • You most certainly _can_ use [string literals as case labels](http://stackoverflow.com/a/16721551/845568) – Captain Obvlious Jun 13 '13 at 23:47
  • Thanks for the answer! So the example a tutorial gives me uses a char, is that possible?? So like case 'A': (which I've never heard before) – m00nbeam360 Jun 13 '13 at 23:48
  • @CaptainObvlious That's not using string literals as case labels. That's using the hash value of a string literal as a case label. Thus: a numeric value. But still a nice trick. – s3rius Jun 13 '13 at 23:50
  • Thanks! Um, so, I'm going to have to explain this to kids with intro to programming, so how should I approach switch statements for this? – m00nbeam360 Jun 13 '13 at 23:51
  • 1
    If you want to teach them the basics of how a switch-case works, I'd suggest you start very small. Show them a purely academic example of a switch that has a few `case 1:`, `case 2:`, etc and prints different stuff depending on input. A very nice application for a switch is making a little pocket calculator, when you can have `case '+':`, `case '*':`, etc as cases. – s3rius Jun 13 '13 at 23:53
  • @MooingDuck Yes, it's a bad idea but only if there is no additional validation in the `switch` condition to prevent collisions. Not all that difficult to take care of that problem though. I really need get around to posting the full solution for that. Damn this beautiful Utah weather ;) – Captain Obvlious Jun 13 '13 at 23:54
  • 1
    @CaptainObvlious I'm curious.. how would you put a collision prevention into the switch? The code won't compile if `operator"" _C` generates the same hash for two cases, but nothing comes to mind for (automatic) prevention. – s3rius Jun 13 '13 at 23:59
  • @s3rius By using a `std::map` (or other appropriate container) with the hash value as the key and the strings as the values. It's an extra step and imposes an extra but small hit at runtime to validate the `switch` condition. I have an older implementation on [ideone.com](http://ideone.com/sL3txv). It throws an exception on an invalid value but the newer versions properly support `default`. – Captain Obvlious Jun 14 '13 at 00:07
  • @CaptainObvlious Correct me if I'm wrong, but that doesn't help at all. Two `case` labels could still generate the same hash through `dbj2Hash`. In this case you'll get a compile-time error for having two `case` labels with the same value. No need for an additional runtime error. The only thing I see that your `CaseLabelSet` does is that it prevents the `default` case from being executed properly for `Test("hello")`. (You said that this is because it's an older implementation, but I can literally see no reason for `CaseLabelSet` to be present.) – s3rius Jun 14 '13 at 00:21
  • Consider a situation where the strings "A" and "B" produce the same hash value but only "A" is used in a case label. If "B" is used as the `switch` condition the case label for "A" gets called instead of `default`. To prevent this `CaseLabelSet` ensures the string exists in the `map` instead of relying solely on the hash value. the older versions throw an exception _only_ because I was **lazy**. The latest implementation automatically adds a value the represents a default case label and does not throw. It also prevents duplicate strings from being added to the map at compile time. – Captain Obvlious Jun 14 '13 at 00:49
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/31744/discussion-between-captain-obvlious-and-s3rius) – Captain Obvlious Jun 14 '13 at 00:52
1

switch statements are for speed. That's why they are only numeric. The compiler will attempt to make a lookup table for non-sparse (i.e. contiguous) value ranges which can improve performance significantly when the code is constantly being executed. This is because it only needs to do 1 comparison for a contiguous range to determine what piece of code to execute.

switch statements potentially can cause hard to find bugs since at the end of each case you need to specify a break or the execution will fall through to the next case.

if/else if/else statements are for more general use. They are in general, slower than an equivalent switch statement if there are many comparisons against the same value. However if that if chain of statements is not executed a lot and the chain is not that long, the performance improvement is negligible.

For more general usage, if is the way to go. In a CYOAG, you will not be needing speed. The slowest part of the game is the user.

To explain this to a 13 year old:

If you think that you will be executing a comparison on a single integer (whole number) value over 1,000,000 or more times all at once and you need it to be done as quickly as possible, use a switch statement. Otherwise, doesn't matter. Just be careful when using a switch, because if you don't have a break at the end of each case you will be scratching you head trying to figure out what just happend? when two or more cases are executed.

Adrian
  • 10,246
  • 4
  • 44
  • 110
0

You can use switch statements if you have too many "else if" statements :)

More seriously, if a player has many different choices (like picking from 26 different options (a-z), then switch is the way to go).

jh314
  • 27,144
  • 16
  • 62
  • 82
  • 1
    In some cases, you might be able to use a function array. It really depends on the case. (Always go with whatever makes your code more maintainable and readable :D) – user123 Jun 13 '13 at 23:42
0

Well, you can't really put strings into switch/case. That leaves you with if. or you'll need to implement parser that reads strings, maps them to some enum type, and then uses said enum within switch/case.

However a better (more extensible) way would be to use map of function callbacks.

Something like this:

#include <map>
#include <string>
#include <iostream>

typedef void(*ActionCallback)();
typedef std::map<std::string, ActionCallback> ActionMap;
static bool running = true;

void leftAction(){
    std::cout << "\"left\" entered" << std::endl;
}

void rightAction(){
    std::cout << "\"right\" entered" << std::endl;
}

void quitAction(){
    std::cout << "game terminated" << std::endl;
    running = false;
}

int main(int argc, char** argv){
    ActionMap actionMap;
    actionMap["left"] = leftAction;
    actionMap["right"] = leftAction;
    actionMap["quit"] = quitAction;

    while(running){
        std::string command;
        std::cout << "enter command. Enter \"quit\" to close" << std::endl;
        std::cin >> command;

        ActionMap::iterator found = actionMap.find(command);
        if (found == actionMap.end())
            std::cout << "unknown command " << command << std::endl;
        else
            (*found->second)();
    }
    return 0;
}

The big advantage of this approach is that you can change commands at runtime. Add new commands, remove them, etc. You could even go a bit further and add lisp/python/lua binding and make it even more extensible.

SigTerm
  • 26,089
  • 6
  • 66
  • 115