Wednesday 7 August 2019

Writing a Retro Text Adventure in Delphi


It’s no secret that I am a keen adventure gamer. I love playing them. I love programming them. The traditional type of text adventure (sometimes called ‘Interactive Fiction’) is a bit like a book in which the game-player is a character. You walk around the world entering human language commands to “Look at” objects, “Take” and “Drop” them, move around the world to North, South, East and West (and maybe Up and Down too). Unlike modern graphics games, an adventure game can realistically be coded from start to finish by a single programmer with no need to use complicated ‘game frameworks’ to help out.

While my Delphi adventure game has a graphical user interface it is, in essence, a text adventure just like the 70s and 80s originals
I wrote an adventure game called The Golden Wombat Of Destiny back in the ‘80s. Over the years  that game has gained cult status. It’s even been turned into a bizarre musical film on YouTube. You can play it online here: https://archive.org/details/TheGoldenWombatOfDestiny

Programming a game is not a trivial task. If you are using an object oriented language, you need to create class hierarchies in order to create treasures, rooms, a map and a moveable player.  You need good list-management to let the player take objects from rooms and add them to the player’s inventory. And you need to have robust File/IO routines to let you save and reload the entire ‘game state’.

If you really want to understand all the techniques of adventure game programming, I have an in depth course that will guide you through every step of the way using the C# language in Visual Studio.

But you don’t have to use C#. I’ve written similar games in a variety of languages: Ruby, Java, ActionScript, Prolog and Smalltalk to name just a few. Recently one of the students of my C# game programming course asked for some help with writing a game in Object Pascal (the native language of Delphi, and also of Lazarus). OK, so here goes…

Delphi lets you design, compile and debug Object Pascal applications
If you haven’t already got a copy of Delphi, you can download one here:  https://www.embarcadero.com/products/delphi/starter/free-download

This is not a tutorial on how to use Delphi. I am therefore assuming that you already know how to create a new VCL application, design a user interface and do basic coding. If you don’t, Embarcadero has some tutorials here: http://www.delphibasics.co.uk/

If you prefer an online video-based tutorial with all the source code, you can get a special deal on my Delphi & Object Pascal Course by clicking this link

The Adventure Begins

First I designed a simple user interface with buttons and a text field to let me enter the name of an object that I want to take or drop. The main game output is displayed in a TRichEdit box which I’ve named DisplayBox.

Now, I want to create the basic objects for my game.  I want a base Thing class which has a name and a description. All other classes in my game derive from Thing. Then I want a Room class to define a location and an Actor class for interactive characters – notably the player. I could put all these classes into a single code file. In fact, I prefer to put them into their own code files, so I’ve created the three files: ThingUnit.pas, RoomUnit.pas and ActorUnit.pas.

Here are the contents of those files:

unit ThingUnit;

interface

type
  Thing = class(TObject)
  private // hide data
    _name: shortstring;
    _description: shortstring;
  public
    constructor Create(aName, aDescription: shortstring);
    destructor Destroy; override;
    property Name: shortstring read _name write _name;
    property Description: shortstring read _description write _description;
  end;

implementation

constructor Thing.Create(aName, aDescription: shortstring);
begin
  inherited Create;
  _name := aName;
  _description := aDescription;
end;

destructor Thing.Destroy;
begin
  inherited Destroy;
end;

end.


unit ActorUnit;

interface

uses ThingUnit, RoomUnit;

type
  Actor = class(Thing)
  private
    _location: Room;
  public
    constructor Create(aName, aDescription: shortstring; aLocation: Room);
    destructor Destroy; override;
    property Location: Room read _location write _location;
  end;

implementation

constructor Actor.Create(aName, aDescription: shortstring; aLocation: Room);
begin
  inherited Create(aName, aDescription);
  _location := aLocation;
end;

destructor Actor.Destroy;
begin
  inherited Destroy;
end;

end.


unit RoomUnit;

interface

uses ThingUnit;

type
  Room = class(Thing)
  private
    _n, _s, _w, _e: integer;
  public
    constructor Create(aName, aDescription: shortstring;
      aNorth, aSouth, aWest, anEast: integer);
    destructor Destroy; override;
    property N: integer read _n write _n;
    property S: integer read _s write _s;
    property W: integer read _w write _w;
    property E: integer read _e write _e;
  end;

implementation

{ === ROOM === }
constructor Room.Create(aName, aDescription: shortstring;
  aNorth, aSouth, aWest, anEast: integer);
begin
  inherited Create(aName, aDescription);
  _n := aNorth;
  _s := aSouth;
  _w := aWest;
  _e := anEast;
end;

destructor Room.Destroy;
begin
  inherited Destroy;
end;

end.

And this is the code in the main file, gameform,pas. Note that the button event-handlers such as TMainForm.NorthBtnClick were created using the Delphi events panel prior to adding code to those methods:

unit gameform;
interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
  Vcl.ComCtrls,
  ThingUnit, RoomUnit, ActorUnit;

type
  TMainForm = class(TForm)
    DisplayBox: TRichEdit;
    NorthBtn: TButton;
    SouthBtn: TButton;
    WestBtn: TButton;
    EastBtn: TButton;
    LookBtn: TButton;
    CheckObBtn: TButton;
    TestSaveBtn: TButton;
    TestLoadBtn: TButton;
    Panel1: TPanel;
    DropBtn: TButton;
    TakeBtn: TButton;
    inputEdit: TEdit;
    InvBtn: TButton;
    procedure FormCreate(Sender: TObject);
    procedure LookBtnClick(Sender: TObject);
    procedure SouthBtnClick(Sender: TObject);
    procedure NorthBtnClick(Sender: TObject);
    procedure WestBtnClick(Sender: TObject);
    procedure EastBtnClick(Sender: TObject);
  private
    { Private declarations }
    procedure CreateMap;
  public
    procedure Display(msg: string);
    procedure MovePlayer(newpos: Integer);
    { Public declarations }
  end;

var
  MainForm: TMainForm;
  room1, room2, room3, room4: Room;
  map: array [0 .. 3] of Room;
  player: Actor;

implementation

{$R *.dfm}

procedure TMainForm.Display(msg: string);
begin
  DisplayBox.lines.add(msg);
end;

procedure TMainForm.CreateMap;
begin
  room1 := Room.Create('Troll Room', 'a dank, dark room that smells of troll',
    -1, 2, -1, 1);
  room2 := Room.Create('Forest',
    'a light, airy forest shimmering with sunlight', -1, -1, 0, -1);
  room3 := Room.Create('Cave',
    'a vast cave with walls covered by luminous moss', 0, -1, -1, 3);
  room4 := Room.Create('Dungeon',
    'a gloomy dungeon. Rats scurry across its floor', -1, -1, 2, -1);
  map[0] := room1;
  map[1] := room2;
  map[2] := room3;
  map[3] := room4;
  player := Actor.Create('You', 'The Player', room1);
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  CreateMap;
end;

procedure TMainForm.LookBtnClick(Sender: TObject);
begin
  Display('You are in ' + player.Location.Name);
  Display('It is ' + player.Location.Description);
end;

procedure TMainForm.MovePlayer(newpos: Integer);
begin
  if (newpos = -1) then
    Display('There is no exit in that direction')
  else
  begin
    player.Location := map[newpos];
    Display('You are now in the ' + player.Location.Name);
  end;
end;

procedure TMainForm.NorthBtnClick(Sender: TObject);
begin
  MovePlayer(player.Location.N);
end;

procedure TMainForm.SouthBtnClick(Sender: TObject);
begin
  MovePlayer(player.Location.S);
end;

procedure TMainForm.WestBtnClick(Sender: TObject);
begin
  MovePlayer(player.Location.W);
end;

procedure TMainForm.EastBtnClick(Sender: TObject);
begin
  MovePlayer(player.Location.E);
end;

end.

So this is already a good basis for the development of an adventure game in Delphi. I have a map (an array) or rooms and a player (an Actor object) capable of moving around. There’s still much to do, however – for example, I need some way of taking and dropping objects and saving and restoring a game. I’ll look at those problems in a future article.

If you are seriously interested in programming text adventure games or learning Delphi, here are special deals on two of my programming courses.

Learn To Program an Adventure Game In C#



You will learn to…

  • Write a retro-style adventure game like ‘Zork’ or ‘Colossal Cave’
  • Master object orientation by creating hierarchies of treasure objects
  • Create rooms and maps using .NET collections, arrays and Dictionaries
  • Create objects with overloaded and overridden methods
  • Serialize networks of data to save and restore games
  • Write modular code using classes, partial classes and subclasses
  • Program user interaction with a ‘natural language’ interface
  • Plus: encapsulation, inheritance, constructors, enums, properties, hidden methods and much more…

https://bitwisecourses.com/p/adventures-in-c-sharp-programming/?product_id=943143&coupon_code=BWADVDEAL


Learn To Program Delphi



  • 40+ lectures, over 6 hours of video instruction teaching Object Oriented programming with Pascal
  • Downloadable source code 
  • A 124-page eBook, The Little Book Of Pascal, explains all the topics in depth