【问题标题】:Using Switch-blocks to Parse input for a Text Adventure?使用开关块来解析文本冒险的输入?
【发布时间】:2011-01-22 14:51:33
【问题描述】:

好的,我现在的目标是进行一次基本的文字冒险。但是,要做到这一点,我需要/想要一个可以执行以下操作的 switch 语句:

  • 检查字符串中是否包含单词 SOMEWHERE。
  • 检查字符串中是否有两个单词的任意组合。

我将如何做到这一点?你能告诉我这个具体例子的编码吗:

提示用户输入数据。 switch 语句将“look box”作为一种情况进行检查,将“sleep”作为另一种情况进行检查。该程序不关心任何单词的顺序,但确实关心字母的顺序。

请详细说明一切。我刚开始编码。

编辑:感谢您的所有回答。我知道有更好、更复杂和更有用的方法来处理这个问题,但还没有达到我的水平。

【问题讨论】:

    标签: c# string switch-statement text-based adventure


    【解决方案1】:

    人们有时会问我为什么不造船。我很得心应手,我喜欢建造东西,我会航行。我总是告诉他们,喜欢航海的人不应该造船,因为你最终会在车库里花了三年时间造船,然后才能开始航行。如果你的目标是航行,那就买条船。如果你的目标是造一艘船,那就造一艘船。

    如果您的目标是通过编写文本冒险来学习 C#,那太好了,您将学到很多东西。如果您的目标是编写文本冒险,那么不要使用 C#,使用 Inform7。它易于学习,专门用于编写文本冒险,并且可能是世界上最高级别的语言。这是一种了不起的编程语言,我强烈推荐它。

    回答您的具体问题:这不是一个好方法。文本冒险处理器的实际工作方式是首先编写一个程序,将用户输入的句子分解为 tokens。您必须逐个字符地搜索字符串以查找单词之间的边界,例如空格、逗号、句点等。一旦你找到边界,然后你提取边界之间的子字符串,并尝试通过将每个单词与字典中的单词进行比较来识别它。

    一旦你有了一个标记序列,你就可以尝试将这个序列与一个语法进行匹配。也就是说,您会看到标记序列是否可以分类为像 {"look"} 这样的单词命令或像 {"look", "at", "the", "red", "按钮”}。你想把它分解——“look”是动词,“at”是介词,“red button”是动词的宾语,“the”是冠词,“red”是形容词,“button”是名词。

    听起来你是个初学者,所以先专注于词法分析;一次一个字符地遍历字符串,识别单词边界,并建立一个List<string> 的标记。语法分析技术可能相当复杂。先把简单的事情做好并扎实。

    【讨论】:

    • 我的目标是写一篇文字冒险,以了解更多关于 C# 的信息。感谢您的意见,但它并没有真正回答我的问题,但其他几个人回答了。
    • +1,很好的答案。我试图在我的回答中更多地解释它,但是,哦,好吧:)
    • @IanVal:你的问题是“我怎么做错了?”答案是你做错了。相信我,通过构建适当的词法分析器和解析器,您将了解更多关于编程的知识,而不是将尝试处理每种情况的千行“if”语句拼凑在一起。
    【解决方案2】:

    考虑到您的起步,我们可以先从简单的情况来看,但您不能使用switch 语句来实现此目的。

    为了简单起见,我们假设您的命令限制为 1 或 2 个单词,并且第一个单词是动词,第二个单词(如果存在)是名词。这给了我们很多可能性:

    North
    South
    Examine
    Take
    Drop
    

    等等……

    假设我们有一个输入字符串strInput

    string strInput = "examine hat";
    

    我们想先把它分开。我们可以使用String.Split

    string[] arguments = strInput.Split(' ');
    

    这会给我们一个字符串数组:

    arguments [0] 是 examine

    参数 [1] 是 帽子

    注意,我们并不总是有第二个,如果用户键入:

    `North`
    

    然后:

    参数 [0] 是

    我们需要检查一下!现在,检查这一点的可怕(但简单)方法是:

    if(arguments[0] == "North")
    {
        // code to go North
    }
    else if(arguments[0] == "Take")
    {
        // code to work with Take.  We'd check on arguments[1] here!
    }
    // etc...
    

    不幸的是,这段代码会变得冗长、复杂且无法使用。你怎么知道在任何阶段你能做什么和不能做什么?如何添加新命令?所以让我们使用C#的美妙的委托功能,同时介绍Dictionary。字典允许我们将一种类型(键)映射到另一种类型(在本例中为委托)。使用这种方法,我们可以创建代理来处理不同类型的命令。

    public delegate void HandleCommand(string[] _input);
    

    在这里,我们委派了一个代表。先别管它,但让我们介绍一些可以与命令一起使用的函数:

    public void Handle_North(string[] _input)
    {
        // code to go North.  This function could just as easily be something that handles
        // *all* directions and checks _input[0] to see where to go!
    }
    
    public void Handle_Take(string[] _input)
    {
        if(_input.Length > 1) // Did the user specify an object to take?
        {
            // code to handle Take.
        }
    }
    

    等等。现在我们需要创建一个字典来将命令映射到这些函数:

    Dictionary<String, HandleCommand> map = new Dictionary<String, HandleCommand>();
    

    在这里,我们声明了一个字典,它将字符串映射到我们的委托类型HandleCommand。现在我们需要填充它!

    map["north"] = Handle_North;
    map["take"]  = Handle_Take;
    // and the rest of our commands
    

    现在,鉴于我们之前的示例,让我们像以前一样拆分字符串,并调用正确的处理程序!

    string[] arguments = strInput.Split(' ');
    if(arguments.Length > 0 && map.ContainsKey(arguments[0]))
        map[arguments[0]](arguments);  // calls our function!
    

    现在我们有了一个可扩展的系统。添加新命令和处理程序很容易!它变得更加复杂,但本质上这是做你想做的事情的好方法。

    编辑:我知道您的问题说它不应该关心单词的顺序。如果你正在写一个文字冒险游戏,你最好形成一些动词/名词或类似的语法,而不是让东西随机输入。

    【讨论】:

      【解决方案3】:

      这是另一个想法:

          string input = "look at the sleep box";
          bool caseA = input.Contains("sleep");
          bool caseB = input.Contains("look") && input.Contains("box");
      
          int scenarioId;
          if (caseA && caseB)
              scenarioId = 1;
          else if (caseA)
              scenarioId = 2;
          else if (caseB)
              scenarioId = 3;
          // more logic?
          else
              scenarioId = 0;
      
          switch (scenarioId)
          {
              case 1:
                  Console.WriteLine("Do scenario 1");
                  break;
              case 2:
                  Console.WriteLine("Do scenario 2");
                  break;
              case 3:
                  Console.WriteLine("Do scenario 3");
                  break;
              // more cases
              default:
                  Console.WriteLine("???");
                  break;
          }
      

      它使用 if/then/else 来评估特定场景,包括可能的组合,例如 "look at the sleep box" 这样的输入,然后使用 switch 语句进行相应的执行。

      【讨论】:

      • 基本上和我的一样,但由于有临时的case 变量,所以更干净一些。
      • 如果用户输入“看看昏昏欲睡的拳击手”怎么办?
      • 这是一个很好的观点。我可以在要检查的数据的两侧添加空格。
      • @IanVal: 语言 是一组字符串。 regular language 是一种可以成功地与 regular expression 进行模式匹配的语言。 正则表达式是一种指定模式的简洁方式,例如“四个或多个字母后跟一个或多个空格”。许多人尝试使用正则表达式来构建词法分析器;我建议初学者不要这样做。从头开始构建您的词法分析器,您将通过这种方式学到更多知识,然后当您学习如何使用正则表达式时,您会更好地了解它们在做什么。
      • 如果您对语言模式识别的基础理论感兴趣,我关于该主题的系列文章是一个合理的介绍:blogs.msdn.com/b/ericlippert/archive/tags/regular+expressions
      【解决方案4】:

      您不能使用switch 执行此操作,您必须使用 if-else-if 类型的结构。

      string input=...
      if(input.Contains("sleep")){ //contains sleep? 
        //do stuff for some word
      }else if(input.Contains("look") && input.Contains("box")){ //contains look and box
        //do stuff for the combination thing
      }
      

      对于switch,每个case 必须是某个静态的、唯一的值。所以你不能使用.Contains 作为案例。

      【讨论】:

      • 太好了!您确定 switch 语句无法正常工作吗?这将节省我大量的时间......
      • @Ian 是的,遗憾的是,没有一些复杂的黑客是没有办法的。为什么 switch 语句可以节省你的时间?这段代码对我来说唯一烦人的是input.Contains 重复了很多次
      • 没关系。我只是不喜欢冗长的 if-else 语句。每个房间很可能会有 5-10 个动作,谁知道每个动作有多少数据,所以可能会让人感到困惑。
      • 对,因为冗长的 switch 比冗长的 if-else 块要好得多。最后你会发现这些都不行。您可能会发现词法分析器是更好的工具。
      • @IanVal,这个答案足以说明您在编码方面的专业知识水平,因此接受度很高。请参阅我的答案,了解一种以可扩展方式实现所需内容的方法,而不是尝试在输入中查找字符串。
      【解决方案5】:

      你不能直接使用switch,但话又说回来,我认为你不应该。您可能应该拥有在与包含switch 的逻辑不同的位置找到单词的逻辑。

      考虑使用枚举来包含所有可能的操作:

      public enum AdventureAction
      {
          LookBox,
          Sleep
      }
      

      考虑编写一个进行“解析”的方法:

      public static AdventureAction Parse(string text)
      {
          if (text.Contains("look") && text.Contains("box"))
              return AdventureAction.LookBox;
      
          if (text.Contains("sleep"))
              return AdventureAction.Sleep;
      }
      

      然后您可以使用简单的switch 语句来执行操作:

      var action = Parse(input);
      switch (action)
      {
          case AdventureAction.LookBox:
              // do something interesting with the box
              break;
      
          case AdventureAction.Sleep:
              // go to sleep
              break;
      }
      

      【讨论】:

      • 有趣...然而,似乎 if-else-if 方法更直接。
      • @IanVal:如果你想的不是很远,那就更直接了,是的。但是想一想如果您希望您的文本冒险以多种语言(例如德语、法语等)提供,您将如何扩展它?或者,如果您想重新使用相同的引擎来创建新的冒险游戏?如果你正确地分离逻辑,从长远来看,这些事情中的每一个都可以变得容易数百倍。
      【解决方案6】:

      我目前正在编写自己的文本冒险引擎,因为 Inform/Adrift/Quest 都有一个让我烦恼的致命缺陷——Inform 噩梦般的、混淆的语法(这来自一个喜欢事物的 UX 设计师对初学者来说尽可能简单),Adrift 的阅读器很丑,而且 Adrift/Quests 缺乏真正的类/对象支持。

      这可能不是最好的方法,但到目前为止它运行良好。我研究了正则表达式,但决定改用这种方式。

      首先要做的是将玩家的输入/命令字符串拆分成一个列表。幸运的是,在这些游戏中,这个列表的第一个元素几乎都是动词。

      • 蓝色

      您将需要可以通过键访问的动词/对象/等数据类,其中包含所有可以匹配的值,例如“look,examine,ex”。

      class Verb
      {
          string uniqueID;
          List<string> values;
      }
      
      class Object
      {
          public uniqueID; // Because this is shared with Verbs, you could do a better unique ID system, but hey...
          public List<string> articles;
          public List<string> adjectives;
          public List<string> noun;
      }
      

      您还需要一组“动作”子类,这些子类将根据玩家的输入进行匹配。在这里,您“构建”了需要与玩家输入匹配的句子结构。

      • 动作(基类)
        • 看{at} [对象]
        • 看{看}书
        • 跳跃
        • 跳转 {on/onto} [object]

      .

      class Action
      {
          string uniqueID;
          List<SentenceElement> sentence;
      
          void Execute();
          bool RestrictionsCheck();
      }
      
      class Look : Action
      {
          override void Execute();
          override bool RestrictionsCheck();
      }
      
      class LookAtObject : Action
      {
          override void Execute();
          override bool RestrictionsCheck();
      }
      
      class LookAtBook : Action
      {
          override void Execute();
          override bool RestrictionsCheck();
      }
      

      基本 Action 类有一个使用 SentenceElements 的句子构建器。可以是一个List,一段一段的描述句子。

      class SentenceElement
      {
          List<string> values;
          bool isOptional;
      }
      
      class VerbID : SentenceElement {}
      class ObjectID : SentenceElement {}
      class ObjectAny : SentenceElement {}
      class CharacterID : SentenceElement {}
      class CharacterAny : SentenceElement {}
      class TextOptional : SentenceElement {}
      class TextRequired : SentenceElement {}
      class Parameter : SentenceElement {}
      

      您现在可以搜索“Action”类,将第一个 SentenceElement 与玩家第一个输入动词进行比较,并将匹配的列表保留为“potentialActions”。 “string.Contains”会起作用。

      现在您必须通过逐步执行玩家的输入命令,并逐步遍历每个 SentenceElement 来比较它们,从而找到最接近的匹配动作。保留你在每个位置的索引(playerInputIndex、potentialActionsIndex、sentenceElementIndex)。

      如果找到匹配项,则增加 playerInputIndex 直到它与 SentenceElement 不匹配,检查您的设置(isOptional 等),然后移至下一个 sentenceElementIndex 并重新运行比较。最终,您将到达玩家输入或 SentenceElements 的末尾。

      当您有“isOptional”的 SentenceElements 时会增加复杂性,因此必须检查它,或者具有 ObjectAny 类型的 SentenceElements 的操作不应该与现有的匹配,但显示“哪个对象你想吃吗?”信息。此外,与动词不同,对象具有额外的匹配参数,例如前缀/形容词/名词。

      这个系统还需要一个新的类来处理你想做的每一个动作。每个动作都有自定义的“限制”,它还必须在运行之前通过,例如“引用的角色是否还活着?”等。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-08-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多