首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >一款小GOTO文字冒险游戏

一款小GOTO文字冒险游戏
EN

Code Review用户
提问于 2020-10-14 13:47:42
回答 9查看 4.7K关注 0票数 17

EDIT_START:我想感谢所有的人给了我如此好的答案!我很难选择任何一个答案,因为我看到你所有的答案都是有效的,从他们自己的角度来看是好的。我想澄清我自己的问题。我的问题不是“我如何不使用后藤?”,而是“我如何更好地使用后藤?”这意味着我想不惜一切代价使用GOTO作为程序室/状态转换。这是为了教育的目的,也是为了发现C的极限,我会尽快给我的问题一个赏金,以回报。不管怎样,谢谢大家!我将在我的程序中为您设置一个标签;-) EDIT_END:

我在和一个人讨论如何在堆栈溢出区使用GOTO。有人能教我一些使用后藤的隐秘技巧吗?你有什么改进的建议吗?你可以享受我的小冒险游戏,试试看。^^

在你阅读源之前玩游戏,否则你会被宠坏。

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

enum _directions{
    DIR_0 =    0b0000,
    DIR_E =    0b0001,
    DIR_W =    0b0010,
    DIR_WE =   0b0011,
    DIR_S =    0b0100,
    DIR_SE =   0b0101,
    DIR_SW =   0b0110,
    DIR_SWE =  0b0111,
    DIR_N =    0b1000,
    DIR_NE =   0b1001,
    DIR_NW =   0b1010,
    DIR_NWE =  0b1011,
    DIR_NS =   0b1100,
    DIR_NSE =  0b1101,
    DIR_NSW =  0b1110,
    DIR_NSWE = 0b1111
} DIRECTIONS;

void giveline(){
    printf("--------------------------------------------------------------------------------\n");
}

void where(int room, unsigned char dir){
    printf("\nYou are in room %i. Where do you want GOTO?\n", room);
    if(dir & 8) printf("NORTH: W\n");
    else printf(".\n");
    if(dir & 4) printf("SOUTH: S\n");
    else printf(".\n");
    if(dir & 2) printf("WEST:  A\n");
    else printf(".\n");
    if(dir & 1) printf("EAST:  D\n");
    else printf(".\n");
}

char getdir(){
    char c = getchar();
    switch(c){
        case 'w' :
        case 'W' :
            return 'N';
        case 's' :
        case 'S' :
            return 'S';
        case 'a' :
        case 'A' :
            return 'W';
        case 'd' :
        case 'D' :
            return 'E';
        case '\e' :
            return 0;
    }
    return -1;
}

int main(int argc, char *argv[]){
    START:
    printf("THE EVIL GOTO DUNGEON\n");
    printf("---------------------\n");
    printf("\nPress a direction key \"W, A, S, D\" followed with 'ENTER' for moving.\n\n");
    char dir = -1;
        
    ROOM1:
    giveline();
    printf("Somehow you've managed to wake up at this place. You see a LABEL on the wall.\n");
    printf("\"Do you know what's more evil than an EVIL GOTO DUNGEON?\"\n");
    printf("You're wondering what this cryptic message means.\n");
    where(1, DIR_SE);
    do{
        dir = getdir();
        if(dir == 'S') goto ROOM4;
        if(dir == 'E') goto ROOM2;
    }while(dir);
    goto END;
    
    ROOM2:
    giveline();
    printf("Besides another LABEL, this room is empty.\n");
    printf("\"Let's play a game!\"\n");
    where(2, DIR_W);
    do{
        dir = getdir();
        if(dir == 'W') goto ROOM1;
    }while(dir);
    goto END;
    
    ROOM3:
    giveline();
    printf("Man, dead ends are boring.\n");
    printf("Why can't I escape this nightmare?\n");
    where(3, DIR_S);
    do{
        dir = getdir();
        if(dir == 'S') goto ROOM6;
    }while(dir);
    goto END;
    
    ROOM4:
    giveline();
    printf("Is this a real place, or just fantasy?\n");
    printf("\"All good things come in three GOTOs.\"\n");
    where(4, DIR_NSE);
    do{
        dir = getdir();
        if(dir == 'N') goto ROOM1;
        if(dir == 'S') goto ROOM7;
        if(dir == 'E') goto ROOM5;
    }while(dir);
    goto END;
    
    ROOM5:
    giveline();
    printf("This is a big river crossing. I guess I need to JUMP.\n");
    where(5, DIR_SWE);
    do{
        dir = getdir();
        if(dir == 'S') goto ROOM8;
        if(dir == 'W') goto ROOM4;
        if(dir == 'E') goto ROOM6;
    }while(dir);
    goto END;
    
    ROOM6:
    giveline();
    printf("This place doesn't look very promising.\n");
    where(6, DIR_NSW);
    do{
        dir = getdir();
        if(dir == 'N') goto ROOM3;
        if(dir == 'S') goto ROOM9;
        if(dir == 'W') goto ROOM5;
    }while(dir);
    goto END;
    
    ROOM7:
    giveline();
    printf("\"Give a man a LOOP and you feed him FOR a WHILE;\n");
    printf(" teach a man a GOTO and you feed him for a RUNTIME.\"\n");
    where(7, DIR_NE);
    do{
        dir = getdir();
        if(dir == 'N') goto ROOM4;
        if(dir == 'E') goto ROOM8;
    }while(dir);
    goto END;
    
    ROOM8:
    giveline();
    printf("This looks like an endless LOOP of rooms.\n");
    where(8, DIR_NW);
    do{
        dir = getdir();
        if(dir == 'N') goto ROOM5;
        if(dir == 'W') goto ROOM7;
    }while(dir);
    goto END;
    
    ROOM9:
    giveline();
    printf("You've found your old friend Domino. He doesn't looks scared, like you do.\n");
    printf("\n\"Listen my friend,\n");
    printf(" If you want to escape this place, you need to find the ESCAPE KEY.\"\n");
    printf("\nWhat does this mean?\n");
    where(9, DIR_N);
    do{
        dir = getdir();
        if(dir == 'N') goto ROOM6;
    }while(dir);
    goto END;
    
    printf("You never saw me.\n");
    
    END:
    giveline();
    printf("The End\n");
    return 0;
}
EN

回答 9

Code Review用户

发布于 2020-10-14 15:02:30

goto的争论由来已久,1966年埃德加·迪克斯特拉( Edgar )提出了一份名为“去发表被认为有害的声明”的著名论文。这是有争议的,争论一直持续到今天。尽管如此,他的大部分结论至今仍然有效,goto的大多数用途被认为是有害的意大利面编程

然而,人们普遍认为goto的某些用途是可以接受的。具体地说:

  • goto应该只用于向下跳,而不是向上跳。
  • goto只应用于错误处理和函数结束时的集中清理。

这是一个古老的“设计模式”,我相信/恐惧来源于基本的“关于错误的.”因为它是错误处理的首选方法。基本上,在这个虚构的示例中使用goto只被认为是确定的:

代码语言:javascript
复制
status_t func (void)
{
  status_t status = OK;

  stuff_t* stuff = allocate_stuff();
  ...

  while(something)
  {
    while(something_else)
    {
      status = get_status();
      if(status == ERROR)
      {
        goto error_handler; // stop execution and break out of nested loops/statements
      }
    }
  }
 
 goto ok;

 error_handler:
   handle_error(status);

 ok:
   cleanup(stuff);
   return status;
}

如上例所示,使用goto被认为是可以接受的。有两个明显的好处:干净的方法打破嵌套语句和集中式错误处理和清理在函数的末尾,避免代码重复。

尽管如此,用return和包装器函数编写同样的东西还是有可能的,我个人认为这个函数更简洁,避免了"goto认为有害“的争论:

代码语言:javascript
复制
static status_t internal_func (stuff_t* stuff)
{
  status_t status = OK;
  
  ...
  
  while(something)
  {
    while(something_else)
    {
      status = get_status();
      if(status == ERROR)
      {
        return status;
      }
    }
  }

  return status;
}

status_t func (void)
{
  status_t status;

  stuff_t* stuff = allocate_stuff();  

  status = internal_func(stuff);

  if(status != OK)
  {
    handle_error(status);
  }

  cleanup(stuff);
  return status;
}

编辑:

我发布了一个单独的长篇大论的答案,这里关于所有不相关的事情。包括如何使用适当的状态机设计重写整个程序的建议。

票数 22
EN

Code Review用户

发布于 2020-10-15 01:23:09

您编写的代码或多或少是一台状态机,编写方式可能是用汇编语言构造的。这样的技术在技术上是可行的,但是它不能很好地扩展,而且您可能最终会遇到非常难以调试的问题。您的代码只需要稍微调整一下,就可以使用更传统的C语言方法来实现状态机,这更容易阅读、维护和调试。

代码语言:javascript
复制
int main(int argc, char *argv[])
{
    int state = START;
    char dir = -1;
    while(1)
    {
        switch (state)
        {
            case START:
                printf("THE EVIL GOTO DUNGEON\n");
                printf("---------------------\n");
                printf("\nPress a direction key \"W, A, S, D\" followed with 'ENTER' for moving.\n\n");
                state = ROOM1;
                break;
                
            case ROOM1:
                giveline();
                printf("Somehow you've managed to wake up at this place. You see a LABEL on the wall.\n");
                printf("\"Do you know what's more evil than an EVIL GOTO DUNGEON?\"\n");
                printf("You're wondering what this cryptic message means.\n");
                where(1, DIR_SE);
                do{
                    dir = getdir();
                    if(dir == 'S') { state = ROOM4; break; }
                    if(dir == 'E') { state = ROOM2; break; }
                }while(dir);
                break;
            
            case ROOM2:
                giveline();
                printf("Besides another LABEL, this room is empty.\n");
                printf("\"Let's play a game!\"\n");
                where(2, DIR_W);
                do{
                    dir = getdir();
                    if(dir == 'W') { state = ROOM1; break; }
                }while(dir);
                break;
            
            ...
            
            case END:
                giveline();
                printf("The End\n");
                return 0;
        }
    }
}

代码与以前大致相同,只做了几处小调整:

  • 增加了封闭回路和开关
  • 将标签从ROOMX:更改为case ROOMX:
  • goto ROOMX;state = ROOMX; break;的跳转
  • STARTROOMX等定义的常量(未显示)

以这种方式构造代码可以提高代码的可读性,并避免了goto意大利面可能带来的许多问题。确保您不会无意中从一个房间的代码掉到下一个房间的代码(如果您绕过设置新状态的代码,只需在同一个房间内再试一次)就容易多了。您还避免了goto的许多限制,比如无法“跳过”变量长度数组的声明(参见C99语言规范第6.8.6.1节中的示例2)。您还可以添加一个显式的default案例,以智能地处理任何意外的或错误的房间选择。

这种结构也为改进开辟了各种途径。您可以获取每个case的内容并将其封装到一个函数中,并且可以将每一种情况简化为case ROOMX: state = do_roomx(); break;。使用每个房间的代码封装,您可以单独单元测试室。

您还可以注意到,每个房间的代码都遵循可预测的顺序(giveline() ->、打印描述、->、where()、->、读取、输入、->、选择下一个房间),并编写了一个可以处理任意房间的通用函数do_room(struct room_data* room)。然后,您将创建一个数据结构struct room_data,它保存每个房间所需的所有信息(描述文本、移动方向、每个出口引线的位置等)。这将更类似于游戏引擎的工作方式。您的代码将变得更短、更通用,并且每个单独的空间都将被实现为数据而不是代码。您甚至可以将房间数据存储在外部文件中,然后您将有一个通用的游戏引擎,您不必每次修改迷宫时都要重新编译。

问:“我如何更好地使用后藤?”就像问:“我怎么才能用更好的方式打自己的脸呢?”答案是:没有。像你这样使用goto与我所知道的“更好”这个词的定义是不相容的。您正在使用C本身处理的构造( switch块),并使用显式跳转重新实现它。你得到的功能更少,潜在的问题更多。唯一接近“更好”的方法是放弃不必要的gotos。

请记住,C语言只是在汇编语言之上的一个薄的、可移植的单板。goto是您的CPU“跳转”指令的包装器。据我所知,没有一个CPU有类似于switchfor之类的指令。这些都是语法糖,编译器为您重写成由“跳转”指令驱动的序列。例如,这样一个简单的循环:

代码语言:javascript
复制
for (i = 0; i < limit; i++)
{
    ... code ...
}

就好像它是这样写的:

代码语言:javascript
复制
    i = 0;
LOOP_START:
    if (!(i < limit))
        goto LOOP_END;
    ... code ...
LOOP_CONTINUE:
    i++;
    goto LOOP_START;
LOOP_END:

continue语句将等价于goto LOOP_CONTINUEbreak语句将等效于goto LOOP_END

switch块的实现类似。每个大小写都是带有标签的代码块,switch根据输入值跳转到标签。break跳到最后。这通常类似于您编写代码的方式。关键的区别是switch块不会在不同情况下直接跳转。如果您想要执行多个情况,可以使用一个循环来多次运行switch块。

最终,switch版本和goto版本在编译后可能看起来几乎完全相同。当您使用switch时,您可以让编译器有机会避免某些问题,例如确保在跳过局部变量初始化器时不会跳入局部变量的范围。编写goto-based版本时,编译器将按编写的方式编译代码,并相信您知道自己在做什么。如果您坚持显式地使用goto,那么您最终会遇到导致人们发明像switch这样的东西的问题。当“不惜一切代价”使用goto时,这些成本常常是一个不一致和不可预测的程序。如果你在寻找如何编程不当的建议,那你就错了。

票数 21
EN

Code Review用户

发布于 2020-10-14 15:18:40

缺少对用户输入

的错误检查

函数getdir()应该检查有效输入,也许是应该接收一个有效方向数组。输入无效方向时,应向用户发送输入无效的消息。

干码

goto的使用迫使您重复不应该重复的代码,例如

代码语言:javascript
复制
    where(2, DIR_W);
    do {
        dir = getdir();
        if (dir == 'W') goto ROOM1;
    } while (dir);
    goto END;

意大利面代码

整个程序似乎是一个如何编写意大利面码的例子,这是一个非常难以编写、调试和维护的非结构化代码。

如果代码是结构化的,并且使用了while循环或for循环,那么代码实际上会更小,也更容易理解。

在ENUM

中使用二进制

你输入的字符越多,就越容易出错。由于比特在枚举中很重要,我建议使用八进制或十六进制,最好是十六进制。然后可以使用一个字符定义每个枚举。

与其在代码中使用神奇的数字,不如定义掩码。

票数 9
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/250656

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档