Escape from Planet Delta

«Home / Play

Escape from Planet Delta是以OASYS系統製作的文字冒險遊戲,OASIS是一個用於製作文字冒險遊戲的物件導向的系統。最早Escape是在1980年代中期在Commodore64上以The Quill製作的,在1991年時移植至OASYS系統。Escape發佈於public domain,可以任意複制和散佈source和可執行檔。

OASYS是Object-Oriented Adventure System的縮寫,它的作者是Russell Wallace,在1992年於comp.binaries.ibm.pc發佈的,並且可以在各大FTP站取得。下載的zip包包含一個說明文件檔、二個範例(其中一個是escape)、反組譯器、編譯器及解譯器的C的source、以及MS-DOS的反組譯器、編譯器及解譯器的可執行檔。原始的zip包裡的source缺少include檔不過不難移植到非MS-DOS的平台。

1. 編譯程式碼及執行

OASYS製作的遊戲是先以OASYS程式語言撰寫好程式碼後,再以oac編譯器編譯為物件碼,再透過oai解譯器執行。

1.1 OAC

首先你需要使用一個文字編輯器編輯你的遊戲原始碼,一般附屬檔名為s,例如test.s。接著使用oac編譯器把你的遊戲原始碼編譯為物件碼。

請執行以下命令編譯原始碼

$oac test.s

或者省略附屬檔名

$oac test

編譯成功的話會輸出同檔名的物件檔,不包含附屬檔名。如編譯test.s成功後,產生名為test的物件檔。

編譯過程如果有任何錯誤,則會輸出錯誤訊息。請再根據錯誤訊息,修正程式錯誤,並再重複以上的編譯命令及修正錯誤的流程,直到成功編譯出物件檔。

1.2 OAI

編譯成功的物件檔,可透過oai解譯器執行。

請執行以下命令執行物件檔

$oai test

載入並成功啟動遊戲的話,可以看的遊戲輸出的訊息,以及接受文字命令的輸入提示符號。透過文字命令進行遊戲,quit離開。

2. OASYS程式語言 Language

底下介紹基本的OASYS程式語言語法規則,更詳細的說明請參考OASYS說明文件。

2.1 大小寫 Case

雖然底下的範例都是以大寫字母作示範,但OASYS程式語言不區分大小寫英文字母。

2.2 空白字元 Spaces

空白字元包含space、tab及換行字元。多個空白字元都會被合併視為一個單一空白字元。

2.3 識別字 Identifiers

跟所有其他程式語言的識別字一樣,識別字由英文字母及數字組成,但必需以英文字母或底線字元作為開頭第一個字元。

如以下範例都是有效的識別字

ROOM
SATURNS
HYDRUGONBOMB

以下是不合法的識別字的例子

9TO5

2.4 註解 Comments

註解有兩種形式,一是單行註解,另一是多行註解。

... // 這是單行註解

/* 這是一個
    多行註解  */

2.5 宣告的次序 Order of definitions

基於效率的考量,OASYS在解析編譯程式碼的時候只會掃描一遍。這表示說,在程式碼中的任何參考都必須事先宣告。

2.6 類別及物件 Classes and objects

OASYS是基於物件的系統,像是位置或玩家都是物件。當遊戲開始時並沒有任何物件,必須在開始時建立並初始化後才能使用。等到物件不再使用了,物件則被刪除。

物件是基於類別來建立的,所以第一步就是定義類別。

例如

CLASS PLAYER {{ME} {SELF} {MYSELF}}
CLASS ROOM {}
CLASS MACHINE_GUN {{GUN} {MACHINE GUN}}

一個類別以關鍵字CLASS開頭,接著是空白字元,然後是類別名稱識別字,接著空白字元,然後是一對大刮號。大刮號裡面可以定義可有可無的用來存取以此類別生成的物件別名,每一個別名也用一對大括號刮起來。例如類別PLAYER的物件,可以以ME、SELF或MYSELF來存取。而像是ROOM類別的物件則無法讓玩家透過任何名字存取,只能作為內部使用。而MACHINE_GUN類別物件有兩個可以存取的別名,分別是GUN及MACHINE GUN。

2.7 屬性 Properties

就像許多物件導向的程式語言一樣,OASYS的類別也有屬性,例如描述或重量。只是宣告的方式是以PROPERTY宣告一個屬性列表,這些屬性並不是特別專屬于那個類別,而是所有類別都能使用宣告的任何屬性。

例如

PROPERTY INT WEIGHT
PROPERTY STRING DESCRIPTION
PROPERTY OBJECT IN

如上面的例子,屬性以一個PROPERTY關鍵字開始,接著空白字元,然後是一個屬性類型關鍵字,接著是空白字元,最後是屬性名稱識別字。

屬性類型總共有三種,INT、STRING或OBJECT。INT是整數型別,初始值為0,如上面的WEIGHT屬性。STRING是字串型別,初始值為"NULL STRING",如上面的描述屬性。OBJECT則是一個指向其它物件的參考的屬性,初始值為OBJECT 0。

2.8 方法 Methods

方法是OASYS裡面要作的事情怎麼完成的動作。如下是一個簡單範例

METHOD INT SQUARE INT X
{
  RETURN X * X
}

這是一個計算平方數的方法。一個方法以METHOD關鍵字開始,接著空白字元,接著可有可無的回傳值類型,接著是空白字元,然後是方法名稱識別字,此例中是SQUARE。再來是可有可無的參數列,此例中有一個類型為INT的叫作X的參數。接著是方法主體,以一對大括號括起來。

底下是另一個例子

METHOD LOOK VERBS {{LOOK} {DESCRIBE LOCATION}}
{
  PRINT PLAYER IN DESCRIPTION
}

這個方法叫作LOOK,它沒有回傳值所以沒有回傳值類型。另外關鍵字VERBS表示接下來的一對大括號裡面,是這個方法的別名列表,定義的方法類似CLASS的別名。定義方法的VERB別名後,就能在遊戲的過程中,使用此定義的別名作為操作命令輸入遊戲中。

下面是比較複雜的例子

METHOD GIVE_TO OBJECT X IS_CARRIED OBJECT Y IS_VISIBLE
   VERBS {{GIVE X TO Y} {OFFER X TO Y} {GIVE Y X} {OFFER Y X}}
{
  ...
}

這樣就能以如下的方法呼叫到GIVE_TO方法

GIVE FISH TO TIGER
OFFER FISH TO TIGER
GIVE THE TIGER THE FISH
OFFER THE TIGER THE FISH

注意到GIVE_TO方法的參數列的定義裡有IS_CARRIED及IS_VISIBLE,其實這是另外兩個定義的修飾方法,用來修飾X和Y參數,底下會再介紹。另外上面的例子裡面可以看到THE這個單字,這個特別的單字也是作為修飾用,會被系統自動忽略。

2.9 變數 Variables

類似在定義PROPERTY一樣,你也可以定義GLOBAL變數,但只能有一個實體,如下所示

INT SCORE

如果加上PROPERTY關鍵字,就變成了物件屬性,可以被多個物件擁有。另外在方法裡定義的變數則為LOCAL變數,可見範圍及生命周期只在方法內部有效。如下面的例子

METHOD INT SQUARE INT X
{
  INT RESULT

  RESULT = X * X
  RETURN RESULT
}

此外方法的輸入參數X的地位則相當於一個LOCAL變數。

2.10 THIS

所有的方法都會假定為被指定為某個物件呼叫。例如,當你輸入指令時,就預設假定為指令是被PLAYER物件所呼叫。方法呼叫時,系統維護一個隱含的THIS物件參考,表示此方法是被此物件呼叫的。另外像上面的SQUARE方法的實作裡,並沒有使用到THIS物件,但系統還是一樣會指派一個THIS物件參考。

2.11 INIT方法 METHOD INIT

每一個遊戲都需要一個INIT方法,INIT方法是遊戲程式的進入點,遊戲啟動時第一個被系統呼叫的方法就是INIT,可以在此作初始化。

2.12 選擇器方法 Selector methods

上面的GIVE_TO方法裡定義的參數列中,我們看到IS_CARRIED及IS_VISIBLE。前面提到這是修飾方法,也就是選擇器方法。

這二個方法定義如下

METHOD INT IS_CARRIED
"You haven't got that.\n"
{
  RETURN THIS IN == PLAYER
}

METHOD INT IS_VISIBLE
"That isn't here.\n"
{
  RETURN THIS IS_CARRIED OR THIS IN == PLAYER IN
}

選擇器方法不帶參數,回傳值類型都是INT,也就是0或非0,即FALSE或TRUE。在左大括號前的字串,是定義為當選擇器方法回傳FALSE時,顯示給usr的提示訊息。

2.13 SELECT_ADDRESSEE方法 METHOD SELECT_ADDRESSEE

在遊戲中,玩家可以和其它物件互動或交談。使用如下的語法

GUARD, DROP THE MACHINE GUN

如果沒有前面的"GUARD,",則表示玩家執行“DROP THE MACHINE GUN",加上前面的"GUARD,"後,這個動作則變為是由GUARD執行。

一般來說,要把命令指派給物件,要先輸入物件的名字,接著一個逗號,然後是指令。至於那些物件能夠和玩家互動或交談呢?

有個特殊的選擇器方法叫作SELECT_ADDRESSEE就是用來做這個檢查的。如果你有定義一個SELECT_ADDRESSEE方法,則需要時它就會被呼叫,否則系統總是會顯示預設的訊息"You can't talk to that"。

下面是一個範例

METHOD SELECT_ADDRESSEE
"It doesn't understand you!\n"
{
  RETURN (THIS IS GUARD OR THIS IS ROBOT) AND THIS IS_VISIBLE
}

2.14 玩家 Player

PLAYER是需要預先定義的物件,在INIT方法裡需要被產生出來。如

PLAYER = CREATE PLAYER

如上的例子PLAYER物件是以PLAYER類別產生出來的,當然這也不是必需這樣,你也能以其它類別的產生PLAYER物件。如

PLAYER = CREATE ROOM

上面的例子中,PLAYER物件是以ROOM類別建立的。這表示說,在遊戲過程中玩家可以在不同時間轉換為不同角色。

例如在Commodore 64上的魔戒這個遊戲裡,4個哈比人Frodo、Sam、Merry和Pippin,可以用4個類別來表示這4個角色。玩家可以使用BECOME這個命令來操控這4個角色。遊戲開始時,玩家是先從FRODO這個角色開始,然後使用BECOME SAME切換為SAM。一個使用OASYS的可能實現方式如下

CLASS FRODO {{FRODO}}
CLASS SAM {{SAM}}
CLASS MERRY {{MERRY}}
CLASS PIPPIN {{PIPPIN}}

...

OBJECT FRODO
OBJECT SAM
OBJECT MERRY
OBJECT PIPPIN

...

METHOD INT IS_BECOMABLE
{
  RETURN THIS SELECT_ADDRESSEE AND (THIS IS FRODO OR
                                    THIS IS SAM OR
                                    THIS IS MERRY OR
                                    THIS IS PIPPIN)
}

METHOD BECOME OBJECT X IS_BECOMABLE VERBS {{BECOME X}}
{
  PLAYER = X
}

METHOD INIT
{
  ...
  FRODO = CREATE FRODO
  SAM = CREATE SAM
  MERRY = CREATE MERRY
  PIPPIN = CREATE PIPPIN

  PLAYER = FRODO      // Player starts off playing Frodo
  ...
}

3. 攻略 Walkthrough

Escape from Planet Delta by Russell Wallace

Originally written with The Quill on the Commodore 64 in the mid eighties and
ported to OASYS in January 1991. This game is public domain and source and
object code may be freely copied and used.

The plot is as follows: You were on a starship flying to the planet Betelgeuse
Delta, fourth planet of the Betelgeuse star system. On approaching the planet
for landing, the ship developed engine trouble and appeared to be about to
crash. Unfortunately there was not quite enough lifeboat space for everyone, so
your shipmates beat you up, threw you into the control room, locked the door,
grabbed the lifeboat, ejected and are probably on their way back to Earth by
now. The ship crash landed but amazingly is still mostly intact and you
survived the landing. However there is no chance of your being able to fly it
out of here, so you will have to look for the Starfleet base on the planet and
try to find some means of escape.

Commands can be multiple words, but words like ALL, IT and AND are not
supported. SAVE saves the game and LOAD reloads your position. VERBOSE, BRIEF
and NORMAL give you different lengths of location descriptions.

Good luck!

底下是一系列需要輸入的指令,括號裡的文字是一些關鍵訊息。

(Control Room)
open box
open box
open box
open box
open box
open box
open box
open box
open box
open box
open box
(以上總共11次open box)
(You open the box and remove an American Express credit card and an
oxy-acetylene torch.)
get card
get torch
open door with card
(You manage to unlock the door with the credit card.)
e
(Corridor)

以上的solution只是拋磚引玉起個頭,改天有什麼好心人士提供或我自己過關了再補上。

4. 移植雜記

4.1 試移植

如上面提到的,在網路上下載的v1版本oa_script.zip有缺檔案,另外找到v1.01版本oasys101.zip就完整了。

一開始我先在emsc環境試下po,看看有沒有很多err。還好不多,也很快就修正可以編譯出目標檔。

基本上以以下命令編譯

$em++ OAI.C OALIB.C

然後試著用node執行看看,會有當掉的錯誤。原因是我先用下載的oac.exe編譯escape,但是下載下來的執行檔是古董程式,那時候的int是2個bytes。所以如果我用現在的編譯工具編譯oai.exe,新的oai會認為一個int是4個bytes。所以主要問題出在這裡。

想辦法用硬幹的方式修正後,也可以成功在console下看到print的遊戲文字了。但是emsc不能直接移植scanf,所以試po到此為止。

以以下命令執行編譯出來的程式

$node a.out.js

4.2 建立oac和oai專案作改版

在Visual studio下建立oac和oai專案,之所有用vs建立專案只是方便移植,任何可以達到目的的方式或工具都可以。

建立vs的oac和oai專案後,用類似試po時的改法,先改版一個可以執行的版本。接著就是針對這個可以執行的版本作更多的跨平台改版。這裡只要針對oai改版即可,先不用管oac,除非我們之後還要可以編譯並執行usr輸入的程式,否則現在只要改版oai並一律使用目前可執行的oac的版本。

移植oai有兩個關鍵點。

第一點是載入檔案的地方都改成從byte stream載入,這樣方便在上層一次載入檔案資料再一次解析再執行。第二個地方是usr輸入的處理,只要把原本無窮的REPL迴圈改成事件驅動即可。

第一點沒有甚麼特別的,現在只說明一下第二個重點,其實很簡單。底下是原本的主REPL迴圈

LoadGame(argv[1]);
NewGame();
for (;;) {
  assert (sp == 0);
  if (!getinput ()) {
    continue;
  }
  command ();
  if (restart) {
    if (!getyn ("Would you like to play again? (Y/N) "))
      return 0;
    NewGame();
  }
}

現在只要改成當使用emsc編譯時,不要進入一個無窮的REPL迴圈,這時把這個迴圈打散,以事件趨動的方式每次從javascript那邊呼叫並傳遞進來作為usr輸入的字串時,再進行下一步的command解析處理。

#ifdef __EMSCRIPTEN__
LoadGameFromStream((char*)BIN);
NewGame();
getinput();
emscripten_exit_with_live_runtime();
#endif

BIN是將透過oac編譯好的escape遊戲的物件檔binary使用bin2c之類的工具轉成c程式碼裡的array,底下是ESCAPE.h的一個部份範例。

#ifndef ESCAPE_INC
#define ESCAPE_INC

const unsigned char BIN[] = {
0x6f,0x61,0x73,0x00,0x9f,0x01,0x00,0x00,0x0d,0x00,0x00,0x00,0x2a,0x4e,0x55,0x4c,
...
};

#endif // ESCAPE_INC

底下是對getinput的修改

void input (char *s)
{
  printx = printy = 0;
#ifdef __EMSCRIPTEN__
  // 不作事, 等待從javascript傳遞usr輸入字串.
#else
  gets (s);
#endif
}

int getinput_i(void)
{
  // 作原來的處理.
}

int getinput (void)
{
  print ("> ");
  input (buf);
#ifndef __EMSCRIPTEN__
  return getinput_i();
#else
  return FALSE;
#endif
}

至於提供給javascript的api,用來模擬usr輸入字串的cSendCmd如下

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
extern "C" {
int EMSCRIPTEN_KEEPALIVE cSendCmd(char *pBuff)
{
  strcpy(buf, pBuff);
  if (getinput_i()) {
    printf("%s\n", pBuff);
    command();
  }
  getinput();
  return 0;
}
} // extern "C"
#endif

另外javascript的部份,底下只列出怎麼呼叫cSendCmd模擬usr輸入

<script>
function sendCommand(e) {
  if (13 === e.keyCode || 14 === e.keyCode) {
    sendCommand_i();
  }
}

function sendCommand_i() {
  var el = document.getElementById('cmd');
  var cmd = el.value;
  el.value = '';
  if ('' == cmd) {
    return;
  }
  var ptr  = allocate(intArrayFromString(cmd), 'i8', ALLOC_NORMAL);
  Module.ccall('cSendCmd', 'number', ['number'], [ptr]);
  var resValue = Pointer_stringify(retPtr);
  _free(ptr);
}
</script>

這樣基本就完成了移植。