galgame吧 关注:1,834,276贴子:27,785,649

浅谈galgame和图灵完备性

只看楼主收藏回复

我一直想做一个这样的galgame,但是因为数学和计算机水平太差未能实现,因此把想法放在这供吧友们批评。


IP属地:上海1楼2025-02-11 15:36回复
    有人曾说:“galgame就是看PPT。”这话切中要害,但不全对。
    说它切中要害,是因为没有存档和读档功能的galgame和(不允许在运行时修改自身文件的)PPT,和古典的正则表达式一样,都是有限状态机。由于不具有图灵完备性,这样的galgame虽然能够实现顺序、分支和循环,却不能“编程”,这也是一些人认为galgame“游戏性差”的根源所在。
    然而,如果允许使用存档格子,那么galgame就有了图灵完备的可能,这意味着我们在剧情之外,可能对galgame进行某种“编程”。不同于任何一种剧情,galgame的编程将打破玩家与主角的界限,不再是主角利用某些设定做出某些事,而是玩家利用“世界的规则”进行某种运算从而推动剧情的进展。
    本文将讨论这种编程的可行性。


    IP属地:上海2楼2025-02-11 15:43
    回复
      2026-06-16 13:39:57
      广告
      不感兴趣
      开通SVIP免广告
      啥玩意?类似跑团?还是说要搞什么叙诡?


      IP属地:北京来自Android客户端3楼2025-02-11 15:47
      回复
        我们先来回忆一下存档格子的使用方法吧。
        玩家不能直接操作存档格子中的数据,只能操作当前终端(即正在进行的游戏)的数据。如果存档格子中有数据,想要修改它,必须使用“load”功能将它加载到终端,在终端操作(前进、后退、选择选项)后,再用“save”功能保存到存档中。
        熟悉汇编语言的同学立马就看出来了,这不就是简单的寄存器+内存吗?确实,我们可以做这样一个对应:
        终端——寄存器
        存档格子——内存
        load——读内存,相当于mov <register> [address]
        save——写内存,相当于mov [address] <register>
        是的,带有存档功能的galgame某种意义上就是一台计算机,我们感兴趣的是以它的能力足以“计算”些什么。


        IP属地:上海4楼2025-02-11 15:50
        回复
          在传统galgame中,save/load等功能是由玩家手动控制的。然而,现在我们要赋予主角自动执行save/load等指令的权力。换句话说,这个游戏里的人物能访问记录着他们的命运的——存档格子。
          这是必要的,因为这使得这样一个对应得以实现:
          剧本——程序。
          所谓程序就是一段可以自动操作内存的算法。
          我们将一段存档格子划分出来,作为“快速存档”。这些位置无法通过save/load访问,而是要通过下文介绍的quicksave/quickload访问。通过下文的例子,我们将明白这些程序可能会以怎样的方式自己运行起来。


          IP属地:上海5楼2025-02-11 15:58
          回复
            quicksave会将当前的游戏状态复制一份,存入“快速存档”区域中最靠前的空白的存档格子(这个位置我们称为“栈顶”)。
            quickload会将最新的快速存档(即位于栈顶前一格的存档)加载到游戏中,但同时会【摧毁】这个存档。
            有的同学可能看出来了,这不就是push/pop吗?
            是的,通过“快速存档”和“普通存档”的划分,我们实际上分出了栈内存和堆内存,这为“函数”的调用提供了便利。一旦我们能实现函数的调用,我们将做到一件在传统galgame中不可能实现的事——在galgame中玩另一个galgame。


            IP属地:上海6楼2025-02-11 16:05
            回复
              但是现在面临一个问题:寄存器的数量太少了。确实,终端(对应寄存器)一般来说只有一个(该不会有人真的联机打galgame吧?)。
              更进一步说,这个寄存器正是eip(Instruction pointer)寄存器,它记录了程序运行到了哪条语句——在我们的语境下相当于,剧本进行到了哪个章节。
              因此,我们假定所有的操作,例如add(两数相加)、sub(两数相减)、cmp(两数比较)这些操作,全都可以直接从内存(存档格子)中读取,而不用先将内存中的数据加载到寄存器。
              由于一个存档包含了许许多多细节,除了它在剧本中的位置以外,还有很多隐藏的数据,例如几周目啊,先前做过哪些有影响的选择啊等等,我们可以认为寄存器和内存的位宽是非常庞大的。
              基于这些因素,我们可以写出如下的一段代码(剧本)。


              IP属地:上海7楼2025-02-11 16:19
              回复
                是计算机大佬


                IP属地:山东来自Android客户端8楼2025-02-11 16:24
                回复
                  2026-06-16 13:33:57
                  广告
                  不感兴趣
                  开通SVIP免广告



                  IP属地:浙江来自Android客户端9楼2025-02-11 16:40
                  回复
                    (希望下面的代码看上去是对齐的吧,我在电脑上编辑的)
                    ```
                    scene A:
                    ... ;一些剧情a
                    quicksave ;将当前的状态压入栈顶
                    save 0x0d000721 ;将当前的状态存入第0x0d000721号存档格子。
                    jump <scene B> ;跳转到某个场景,具体是哪个场景与0x0d000721号存档格子中的数据有关。
                    scene A2: ;scene A 第二部分的起始位置。
                    ... ;一些剧情a
                    scene B:
                    ... ;另一些剧情b
                    save 0x0d000721 ;将修改后的状态存入第0x0d000721号存档格子。
                    quickload ;从栈顶弹出之前保存的状态。
                    jump <scene A2> ;跳转到之前的场景。
                    ```
                    其中,剧情b可以是一个完整的galgame,含有顺序、分支和循环,且有权读写第0x0d000721号存档格子。执行完这么一圈后,栈恢复原状,玩家看到的状态恢复了原状,只有0x0d000721号存档格子被编辑了。这个格子存放的实际上就是函数的参数和返回值。因此,quicksave+jump实现了汇编中的call,而quickload+jump实现了汇编中的ret。


                    IP属地:上海10楼2025-02-11 16:40
                    回复
                      插眼


                      IP属地:广西来自Android客户端11楼2025-02-11 16:42
                      回复
                        cy
                        看着像yuno


                        IP属地:上海来自Android客户端12楼2025-02-11 16:45
                        回复
                          cy


                          IP属地:福建来自iPhone客户端13楼2025-02-11 17:14
                          回复
                            接下来我们用galgame写一个小程序吧。
                            以下,我们提供两个额外的指令:
                            ```add [<address 1>] [<address 2>]```。它将两个存档格子的内容相加(我们假定存档的内容都是整数,在这个例子中我们不存储游戏进度信息),将结果放在寄存器%eax(也就是游戏界面)中。当需要显示(输出)这个整数时,用花括号{}表示。
                            ```inc [<address 1>]```。它将一个存档格子中存储的数字增加1,然后写回原位。
                            同时我们也使用mov,但仅仅用于写入常量。
                            ```
                            initialize:
                            mov [0x00] 0 ;这个位置用来存放指针偏移量
                            mov [0x01] 0
                            mov [0x02] 1
                            start:
                            add [ 0x01 + [0x00] ] [ 0x02 + [0x00] ]
                            【小猫】喵,经验加{%eax}!
                            inc [0x00]
                            save 0x02 + [0x00]
                            jump start
                            ```
                            这段代码的效果是:
                            【小猫】喵,经验加1!(存档)
                            【小猫】喵,经验加2!(存档)
                            【小猫】喵,经验加3!(存档)
                            【小猫】喵,经验加5!(存档)
                            【小猫】喵,经验加8!(存档)
                            ……
                            注意,我们没有在剧本中有意地写下小猫的台词!小猫会自己读取存档格子中的数据,从而“算出”她该说的台词。我们利用galgame的save功能和一些简单的加法运算,实现了一种具有内存泄漏的计算斐波那契数列的方式。这便是“利用世界的规则来计算”的含义。


                            IP属地:上海14楼2025-02-11 17:14
                            回复
                              2026-06-16 13:27:57
                              广告
                              不感兴趣
                              开通SVIP免广告
                              汇编难用喵


                              IP属地:江苏来自Android客户端15楼2025-02-11 17:17
                              收起回复