Backlog

by Ter13
Welcome to my brain's backwash
ID:2305280
 
DM is a simple programming language designed to help get you off the ground and make a multiplayer game quickly. It is not a powerful language. It is not a portable language. It is not even a great choice for commercial, professional game development. It is, however, a good way to learn general object-oriented programming concepts and to do some rapid prototyping of simple 2D multiplayer games.

BYOND is a platform for games written in DM.
Dream Maker is a self-contained IDE for making games on BYOND.
Dream Seeker is a client that allows users to connect to games made with BYOND.
Dream Daemon is a server daemon that allows users to host games made with BYOND.
the hub is a central service and protocol that allows users to provide games across the internet.

BYOND games are constructed of various media. Images (dmi, png), sound effects (wav, ogg), music (ogg, midi), source code (dm), script (dms), maps (dmm), interface skins (dmf), and more all come together to create games limited in complexity only by your motivation and knowledge.

Generally speaking, games made with BYOND will have assets split into three categories:

Binary (dm, dms, map, dme)
Compiled Resources (dmf, dmi, png, wav, ogg, etc)
Runtime Resources (anything)

BYOND constructs games by passing code that you write through a compiler. That compiled code is then used to create what's called a DMB, or a Dream Maker Binary. Dream Daemon uses this binary file to initialize the world and run behavior defined by the developer.

When the compiler runs over your code, it will find files that are referenced by the code and collect them into an RSC file, which or a Resource Cache. This file allows all needed graphics, sounds, web pages, and other hard-included assets to be treated as a single archive.

Runtime resources are generally loaded using file system tools built into the language. This allows developers to load and store data that isn't necessarily compiled into the game between sessions.


A compiled BYOND game is often referred to as a world. The hub contains many different games, and each game can have many different worlds running at once. A number of players can connect to each world as well to play the game.

Each world has its own data and player audience, but information can also be shared between worlds if the developer so chooses.

All worlds share a common language, but each has been programmed to act differently by their respective developers. Using hub communication, developers are able to create games much larger and more interconnected than a single instance of Dream Daemon would allow. In this way, BYOND is not suited out of the box for large-scale MMOGs (Massively Multiplayer Online Games), but clever developers may be able to construct an environment that serves thousands of simultaneous players even if each individual world can only house a few hundred at a time maximum.

Now that you are familiar with BYOND, we can get started introducing the software you'll be using to learn how to make an awesome game and cultivate your own internet community!
Hello world:

Let's dive right in and show you just how easy BYOND is to use.

Open Dream Maker.

Select File->New Environment...

Environment files are .dme files. They are the base unit of a project. They include all the separate code files of a project, and will for the most part be automatically generated and filled for you, so you don't have to worry about editing them. Eventually, you'll find that outside of the auto-generated regions of the file, they are just like DM files.

Your new project will include a new DM file and a DME file. Your DM file will contain some sample code. Don't worry about this sample code yet. We'll cover this at a later time.

/*
These are simple defaults for your project.
*/


world
fps = 25 // 25 frames per second
icon_size = 32 // 32x32 icon size by default

view = 6 // show up to 6 tiles outward from center (13x13 view)


// Make objects move 8 pixels per tick when walking

mob
step_size = 8

obj
step_size = 8


That's what your DM file should look like. Let's just select it all and delete it. Press Ctrl+A, then press the delete key on your keyboard. DM's code editor tabs work just like any text editor, or any other Integrated Development Environment if you already have a head start in programming.

With our newfound fresh start, let's make the simplest program possible.

We're going to output a message when the user connects to your game.

What you are going to type:

mob
<tab>Login()
<tab><tab>src << "Hello DM!"


Literally press the tab key on the keyboard for tabs. These help you clarify something called scope. We'll cover what that is later on, but you will need to follow these instructions pretty exactly.

What the code will look like in the editor:

//tabbed, whitespace delimited
mob
Login()
src << "Hello DM!"


Users of other programming languages might be relieved to know that DM is very flexible in how it can be written. You can use spaces instead of tabs, but I'm pretty sure that's breaking a commandment or three, and you can also use curly brackets to denote scope. Let's take a look at some of the variations:

//brackets and semicolons
mob {
Login() {
src << "Hello";
}
}


//brackets and semicolons, compressed:
mob{Login(){src<<"Hello";};}


For advanced users, use whatever formatting you prefer, but the gold standard, most common syntax is one that's easy to read and easy to write. That's why we'll be sticking to tabbed, whitespace delimited, or the first example.


Compile and run your code. Press Ctrl+K, or Build->Compile in the menu, and then press Ctrl+R, or Build-Run.

You will see a Dream Seeker instance open up, and you should see a nice "Hello DM!" in the big white box in the body of the window. Great job! You are now a DM programmer.

When you are done patting yourself on the back enthusiastically, be sure to close Dream Seeker before continuing on to the next section. We don't need any spare windows laying around, and if you include any resources in your project, you'll notice that your rsc will be locked, preventing compilation while Dream Seeker or Dream Daemon is running a copy of that game. This will be important to know later on when your projects become more complex.
Theory break.

Let's talk about what we just did for a minute.

mob //object type
Login() //proc
src << "Hello DM!" //instruction


Objects / Types / Classes

DM is an Object-Oriented language. That means that the majority of data you are going to be working with are in the form of objects. Objects have structure. This structure comes in two forms: properties, and methods. In DM, properties are called variables. Methods are called procs.

Think of a property as a place that stores information.

Think of a method as an action that an object can take.

Object-oriented languages focus heavily on defining behavior on groups of data called object instances. An object is sort of in a way, two things. There's the class (called a type in DM), and the instance. Think of a class or type as a blueprint that will let you build something. The something that you build is called an instance, or just sometimes an object.

Instances have all of the properties and methods of the class that they were constructed from. So when we are setting up a class by writing its code (often called declaring and defining, two similar but different concepts), we're actually establishing how instances of that class will behave, and what information they will store when they are constructed from that class.

This is all very confusing at first, but it really starts to make sense as you sort of flail about with it. For people who already know other languages, I'm sure this will be very old hat.

Mob is one of DM's built-in types. The main built in types are:

//data types:
/datum

/client
/world
/icon
/list

//atomic types:
/area
/turf
/obj
/mob

/image


The data types are pure-information objects. atomic types are differentiated from data types in that they are capable of being somewhere in the world. Datums and ATOMs ([A]rea [T]urf [O]bj [M]ob) are generally how these two divides are categorized.

These types are polymorphic. That's a fancy word that means "many forms". Polymorphism is a concept object-oriented languages use to make writing code very modular and easy to understand. A simple way to understand this, is:

X
Y


All Ys are Xs, but not all Xs are Ys.

let's try this again:

vehicle
boat
yacht
canoe
bicycle
automobile
car
truck


All of these statements are true:

A boat is a type of vehicle. A yacht is a type of boat. A yacht is a type of vehicle. Not all vehicles are boats. All boats are vehicles. All yachts are boats. A canoe is not a Yacht. Both canoes and Yachts are boats. A bicycle is not a boat. A bicycle is not a yacht. A car is an automobile. A car is not a truck. A truck is a vehicle. Some vehicles are boats. Some vehicles are bicycles. Some vehicles are automobiles.

The properties we are exploring are relationships between parents and children.

boat is a child of vehicle. vehicle is the parent of bicycle. boat is a sibling of bicycle. automobile is a sibling of boat. yacht is a child of boat. boat is the parent of canoe. vehicle is an ancestor of truck.

Ancestry and inheritance is a very important concept in programming, and we're going to cover this more in depth when we cover what inheritance actually means for how we write code. For the moment, you should be familiar with this sort of structure and terminology.

Turning back to the built-in types DM offers, we can better categorize them as such:

list
client
icon
world
datum
atom
area
turf
movable
obj
mob


As you can see, there are a few types we didn't mention earlier. That's because they aren't really directly useful types. /atom and /atom/movable is not a real type. They can't be constructed. Trying to construct an /atom will create a /turf instead. Trying to construct an /atom/movable will construct an /obj instead. These types exist to allow you to change the methods and properties common to their child types. As such, any change made to /atom will carry through to /area, /turf, and /movable, and by extension /obj and /mob. This is because structural changes to objects travel upward through the tree (you define your tree upside down, so down the code is up the tree).

A parent will pass all of its traits to its children. See why I mentioned evolution earlier? It's sort of like that, but less squishy.

The whole structure, as I mentioned is often called a type tree, or an object hierarchy.


mob is an atomic object. It's movable, meaning it can move around the world just like an obj. The only difference is that mobs tend to be things that "see" and "hear". They can have a player (client) connected to them.

When a client connects to a world, it is actually given a mob automatically. Login() is called on that mob. Login() is a proc, or a method. It's a special type of proc sometimes called a hook. You are meant to change what it does so that you can make things happen when the client takes control of a mob.

Notice how Login() is tabbed inside of the mob type? That's called scoping. When code is tabbed into something else, that means that that particular line of code happens inside of the block directly above it. In this case, tabbing inside of mob means we're now declaring the structure of mob.

Now, when we tab into the Login() proc, we're suddenly working in what's called local scope:

//global scope
mob
//class scope
Login()
//local scope
//class scope
//global scope


Local scope is actually a linear series of instructions, or simple commands that happen one at a time, one after another immediately when the proc that you are tabbed into is called, (invoked, or when it is happening). Different commands do different things, but they all follow a simple logic and structure. The point of writing these instructions is to solve a task. There are a huge variety of ways to solve any task. Some are good, some are bad. The more instructions you wind up writing, generally the longer it takes to complete the task. Not all instructions are created equal, however, so sometimes more code is faster than less code. Sometimes code of the same length will take a different amount of time than another similar, but different method. Sometimes the same code will take a different amount of time just based on certain situational factors. You'll learn how to gauge what's best for you in time. At the moment, though, you should be focusing on understanding what you are doing as best you can.

src << "Hello DM!" //instruction


Our login proc contains only a single line, and a total of three instructions.

src is what's called a label. a label is any value that stands in place of an uncertain value. Labels are variable, meaning they can have different values when the proc itself is running.

In this case, src is a label that will always represent the object instance that this proc was called on. Remember that we're defining the class here, so we use src to refer to an instance that hasn't been created yet because again, this proc will run on any /mob that has been created from the class. That's why the value of src is uncertain until the proc is actually being interpreted.

<< is an operator. Operators transform data. The << operator is read as the "output" operator. It outputs the value on the right to the value on the left. When mobs receive output, they will check if they have a client, and then attempt to display that output to the client's interface.

So to summarize, when a client logs into the mob src, "Hello DM!" is output to src, which is the mob the client tried to log into, which then displays "Hello DM!" on the client connected to src's interface.

Yes, this is incredibly simple, but there's a lot going on that you will need to understand so that you can build on that information later.
Tick Tac Toe:

Let's create a simple first game to get you a little more familiar with DM.

We're going to create a simple single-player tic-tac-toe game doing nothing fancy.

First things first, we need some art. Luckily, BYOND provides a nifty text-mode, so we don't actually need to draw anything yet!

world
turf = /turf/blank

turf
blank
text = "."
nought
text = "O"
cross
text = "X"


We're seeing some new stuff here now, so let's try to work out what's going on.

First, we're defining a property of world. Properties, if you remember are places you can store information. In the case of world, we're changing a property called turf. From here on we're going to call properties variables. Why? Because that's what DM chose to name properties!

world comes with quite a few variables built into it. One of them is turf. When you set the value of turf to a type path, that type of turf will be used as the default turf.

What are turfs exactly? Well they are tiles. BYOND is a tile-based game engine, and every single space in the world is a turf. Turfs are literally the ground beneath your feet! Turfs can't move. They can neither be created nor destroyed. They simply fill all of your maps. Turfs are places that things can be! They contain movables, notably /objs and /mobs. They also can be made to look super snazzy.

So let's go further down in the code:

turf
blank
text = "."
nought
text = "O"
cross
text = "X"


Here, we have created three children of /turf. Their paths are: /turf/blank, /turf/nought, and /turf/cross.

Notice how we've given them all a different text variable? That's so we can tell them apart when viewing the game in text mode.

Next, we need to create a map file for our game of noughty crosses. Go to File->New... (or CTRL+N), and in the dialog box that pops up, select a map file (.dmm) in the type dropdown. Name it "board".

Now, of course, set it to a size of 3x3. The map won't look like anything until you swap the drawing mode to text mode. You can do this via options->Icon Type->Text. If you think it looks a bit smallish, you can also go to options->Zoom and increase the zoom size. This makes working with tiny maps bearable.

Your map should already be full of /turf/board objects. If it's not, you can manually paint them on by selecting the /turf/board type in the object panel at the left and painting them in place with the add or box tool.

Let's expand on /turf/board a bit to make the game interactive!

turf
board
text = "."

Click()
new/turf/nought(src)


Now we're running into something new!

Click() is a hook that's called on any atom when a player clicks on that atom in their client with their mouse.

We're telling it to create a /turf/nought instance. But this is a format we haven't seen before. Well, () means invoke. In local scope, () is actually an operator that will invoke whatever is on the left of it. In this case, it's new/turf/nought. new is a keyword. It creates the type on the right of it. In this case, it's /turf/nought. So we're creating an instance of the /turf/nought class. But what's src doing in that invocation operator? Well, the invocation operator actually takes something called arguments. Arguments are sort of like variables that you can hand off to a proc that will allow it to serve a variety of different behaviors using the same code.

In this case, the atom/New() proc takes a single default argument, which is "loc". Or rather, where the atomic object will be created. Creating a new turf doesn't really create a new turf. Rather, it changes the current turf into a different kind of turf. Turfs are the only objects that behave this way in DM, so just remember you aren't really creating anything. Just changing. For all other types that you use the new keyword with, a whole new instance will be created entirely separate from the old one.

In this case, src is the turf that was clicked on, so we're creating a new /turf/nought at the turf/board that was clicked on.

Let's compile and give it a shot!

Uh oh. Something's wrong. There's a big ol' M at the bottom-left corner. What's that doing there? It's not on our map! Well, actually if you hover over it, you can see the name of the object in the status bar. It's YOU! Or rather, it's the mob your client is connected to. Remember in Hello DM, how we changed mob's Login() proc? Well the default action of the Login() proc is to put the newly created mob as close to the bottom-left of the map that it is able if it isn't already on the map at the time of connection.

Weird, right? Well, we don't want that mob on the map. Let's change Login().

mob
Login()


Wait, we didn't do anything? Yeah we did. Run your world. See what happens. Hey now! The screen is black! Did we screw up by not writing anything?

Let's try again.

mob
Login()
..()


Now we're back to where we started! Well, the first time we messed with Login(), we did what's called overriding it. When you modify a proc on an object type, that body of what you wrote becomes what that proc does. What it did before will no longer happen. The map is black because the mob isn't on the map, so the client isn't looking at anything. In fact, it's looking at nothing. The client attempts to draw the area around the mob, and if the mob is on the map, there's no there there to look at. Neat, right?

So why did the second one fix it? Well, ..() is a language feature called the supercall. ".." means super, or "previous", and () is to call, or invoke. .. is a shortcut for the previous version of the proc you overrode. In other words, ..() is to make the old behavior happen.

Let's actually make Login() do what we want now, though.

mob
Login()
client.eye = locate(2,2,1)
client.perspective = EYE_PERSPECTIVE


We're seeing something new again. client. We've talked about clients. They are an object type that stores information about the player that is connected to the world. The mob is just sort of a skin that they wear that is actually somewhere in the world. It's a graphical representation that the client drives around. mobs have a variable called "client", which allows you to do something called "access" that mob's client instance.

x.y means access x's y.

Accessing is a way to navigate through the variables and procs of an object. client stores a reference to the player's client, so you are actually accessing a variable itself. eye is a variable property of the client. It stores the atom that the viewport centers on. Normally, it's set to the mob that the client is connected to, but you can change it to another object and the viewport will follow that object around instead of the mob.

locate() is a way to grab a turf from the map. In this case, we're feeding it coordinates from the bottom-left of the map. We're grabbing the second tile right, second tile up on the first z-layer. locate() will grab the specific turf instance at that location.

Lastly, the = operator is called the assignment operator. This sets the variable on the left to the thing on the right.

The next line is pretty similar. The client has a variable called perspective. This affects how the client is determining what is visible. The two main properties are MOB_PERSPECTIVE, which is the default. This means the client won't draw anything the mob can't see. And EYE_PERSPECTIVE, which means that the client will draw as though the object stored in client.eye was doing the seeing.

And since we didn't supercall in Login(), the mob never gets put on the map. Now compile and run. You'll notice that the board is empty! Start clicking! You should have some noughts showing up all over!


Now we need to make the game playable against an AI. We're going to set it up in such a way that the AI will move completely randomly every turn, and the player will always go first.


Let's plan out what we need to do:

1) We need to tell the player that they have moved immediately after clicking a board turf.

2) We need to have the ai pick an open board turf at random.

3) We need to create a cross at the turf.


We can do this quite easily by creating a global variable to store the open spaces. This variable has to store multiple items, though. How will we acheive this? With a list, of course! Lists are a built-in type that can store any number of any kind of value. Let's learn how to define a new global variable.

var
list/open = list()


Defining a new global variable is pretty self explanatory. You use the var keyword to name the variable so you can use it elsewhere. Since everything is technically in global scope, class and local scopes can use global variables too. However, variables defined in class scope can't be used in global scope, and variables in local scope can't be used in class or global scope. Variables only exist after they have been declared!

Since var isn't tabbed into an object or a method, it's a global variable!

But what's this list/open bit? This is called typecasting. When you are declaring a variable, you can specify the type of thing it should store. You don't have to do this, as all variables in DM can store any value DM supports. But if you store a value typecast to a specific type, you can safely assume that variable has a value that has all the properties and methods of that type. At least, during compilation. If you don't typecast, trying to access a property of the data stored in that variable will cause the compiler to give you a nasty little runtime error.

var / [type] / [label]

The label part of the declaration is the name of the variable that you will use when reading from or writing to the variable.

In our case, we're declaring AND defining the variable by using an assignment operator to an initial value of list(). list() is short for new/list(). It simply creates a list object. In this case, it's empty.

Now, let's make sure this list gets filled up with empty spaces:

turf
board
text = "."

New()
open += src


New() is new. It's actually not the same thing as new, but it's related. when you use the new keyword to create an object, the instance you created has the New() hook invoked on it. This allows you to define the behavior of objects when they are initially created, and do some neat dynamic setup of your game.

open += src is also something we haven't seen before. += is the "addition assignment operator". It allows you to add the thing on the right to the thing on the left. In the case of a list, it allows us to add any kind of a value as an additional element in the list. We're adding src, or the turf/board instance itself when its created to the global.open list in this case.

Now then, we also need a way to let the AI move when the player makes each of their moves.

Let's write that code:

turf
board
Click()
new/turf/nought(src)
usr.Moved(src)

mob
proc
Moved(turf/move)
open -= move
if(open.len)
var/turf/t = pick(open)
open -= t
new/turf/cross(t)


You'll see something a little weird in Click(). It's usr. Usr is another built in label/variable for all procs. It's similar to src, but instead of being the object the proc was called on, it's the mob that initiated the last proc.

In this case, it's the mob that belongs to the client that clicked on the board turf.

We're calling .Moved() with an argument of src to tell the mob that they Moved, and that this turf was the place they moved. Notice how we're doing this AFTER we technically replace src with /turf/nought? Again, turfs are never destroyed. Even though the Click() proc is only defined on /turf/board, and now src has turned into /turf/nought, the currently executing invocation of /turf/board/Click() keeps running, only src is now typed /turf/nought and has all the properties of a /turf/nought, and none of the properties of /turf/board. You have to be careful about this, because the compiler isn't going to catch any errors this kind of a change can cause, and you'll get a real nasty runtime error, which are like compiler errors, only they don't stop you from compiling. The game keeps running even though it's failing to work right.

We also defined Moved() under mob. Notice the proc keyword? Well Moved() isn't built in. We're not overriding an existing proc like very other proc we've set up. This keyword is just like var when defining a new variable, only it's for procs. It denotes structure entirely new to the scope its used in. Just like vars, procs can be global and class scoped. local procs are not supported in DM.

You'll notice the -= operator being used on open. This is the "subtraction assignment operator" We're subtracting the turf passed to Moved() from the global open variable. Then we check a property of lists called len. This will tell you how many values are in the list. If we have any values left, that means there's at least one move left, so we let the computer take a move.

the if() statement is new. it's a block-level instruction. It separates code out into what are called branches. The if statement will only allow the code tabbed inside of it to run if the condition inside the brackets is true. In this case, open.len being anything but 0 or null (empty, no value) will be considered true, allowing the next instruction to be the first tabbed into the if statement. If the if statement evaluates false, all of the code in the if statement is ignored. It jumps to the bottom of the if statement, which in this case, is the end of the proc.

See that local variable declaration? var/turf/t = pick(open), again we're casting a variable named t to the type turf. This is a local variable, so it will not be accessible in a lower scope. Its value is the result of a proc called pick(). pick() takes a list argument and will grab one random value out of the list. Once we have that value, we take the turf the computer picked out of the open tiles list and then create a new /turf/cross at that location.

Give it a run. You'll have to keep track of when you win or lose yourself. That'll be our next project, is allowing the game to figure out who won.
Figuring out who wins or loses is easy in tic-tac-toe. For a human. It's much harder to break down your exact thought process into code.

Let's create a simple flow chart for figuring out who won.

1) Pick each tile at the left of the board. See if there are three of the same piece in a row to the right. If any row has three of the same, that piece's player wins.

2) If no one has won, pick each tile at the bottom of the board. See if there are three of the same piece in a row upward. If any row has three of the same, that piece's player wins.

3) If no one has won, pick the bottom-left tile, and look for three of the same northeast. If there are three in a row, that piece's player wins.

4) If no one has won, pick the top-right tile, and look for three of the same southeast. If there are three in a row, that player's piece wins.

This is what one solution might look like when written in DM:

mob
proc
CheckWin(type)
var/list/check_start = list(locate(1,1,1),locate(1,2,1),locate(1,3,1),locate(1,1,1),locate(2,1,1),locate(3,1,1),locate(1,1,1),locate(1,3,1))
var/list/dirs = list(EAST,EAST,EAST,NORTH,NORTH,NORTH,NORTHEAST,SOUTHEAST)
var/turf/t, dir, score
for(var/count in 1 to 8)
t = check_start[count]
dir = dirs[count]
score = 0
while(t.type==type)
++score
if(score==3)
GameOver(type)
return
else
t = get_step(t,dir)
if(!open.len)
GameOver()


This is by far the most complex thing we've ever seen to date. Let's break it down line by line.

First, we define a local list called check_start. We've never seen list() with arguments before. In this case, we're filling it with the result of locate(). What we're doing here is creating a list of the 8 starting points of each line we need to look across.

Since we have a list of 8 starting points, we also need a list of 8 directions to tell the game's AI to look toward. That's exactly what the dirs var declaration is doing. We're creating a list with 8 directions. These directions are constant values in DM. EAST, NORTH, SOUTH, WEST, NORTHEAST, SOUTHEAST, NORTHWEST, and SOUTHWEST are all special values that DM takes when working with tile directions.

See var/turf/t, dir, score? We're declaring three variables on a single line. One that's typecast /turf named t, one that's named dir with no typecast, and one called score with no typecast.

for() is also new. for() is a type of block-level instruction called a loop. for() comes in a variety of forms, each doing a slightly different job.

for(index in start to end) is one such format that for() allows. We declare a variable, and the code will repeat with a different value for index based on start to end. It begins at start, and ends at end, increasing count by 1 at each step. When we reach the bottom of the for()'s block, it repeats from the beginning of the for's block until it reaches the end of the last iteration.

Within the for, we are assigning our earlier declared turf t variable to the item found at index count of the list check_start.

Remember that check_start has 8 elements. They are turfs, the left edges, followed by the bottom edges, followed by the two diagonal edges. Since we're accessing count, and count is starting at 1 and going to 8, we're actually grabbing the values at position 1 through 8 one loop iteration at a time.

After we grab this iteration's turf, we also grab this iteration's direction and store it in the local dir var.

Then we set the score local variable to zero. We're going to use this to keep track of how many pieces are in a row.

the while() loop is another new block-level statement. It's sort of like a repeating if() statement. It repeats the block tabbed under the condition until the condition is no longer true.

++ is a new operator. It increases the value to the right of the operator by 1. In this case, it's increasing score by 1 every time it grabs a turf whose type property matches the type argument passed to CheckWin(). Then, we reach an if-else statement. An else statement is sort of like a second conditional block that is jumped to and run only when the true condition branch isn't run. When either the true (if) or false (else) branch completes, it jumps beneath the whole if-else block, which in this case is the end of the while() block, which then repeats until the condition is no longer true. Once the while() condition is no longer true, we jump past the while() block, which is the end of the for iteration. We repeat that for() until we have repeated 8 times, and thereby checked 3 tiles in each direction from each starting point. All possible win conditions have been checked at this point.

Notice in that if() block, we are calling GameOver() with the type passed as an argument to the CheckWin() function? That's going to handle our win condition logic, which we'll cover below.

You might also be wondering what == is. == is the equality operator. = is the assignment operator. You are making something equal to something else. == is for checking if two values are equal. It returns 1 if true, or 0 if false.

But after that GameOver() proc finishes its job, return will be called. return is a special keyword that causes the current proc call to end immediately without executing any more code. return takes a value as well. This value can allow you to use a proc call as though it were a value. if you don't pass a value to return, it returns null, which is a special value that means "nothing". Null is sort of like zero, but it's not zero. It's null. It's undefined. It's just a special place that represents nowhere.

As for the else branch, it's using a new built-in proc that we haven't covered before. get_step() is sort of like locate(), but instead of world coordinates, it takes a reference atom, and a direction. It will return the turf in the direction from the reference atom. This way, we can use directions to get a turf relative to something when you are uncertain of the exact coordinates.

After everything has run, it will finally check open.len to see if it is 0. ! is the logical inversion, so !false means true, and !true means false. Or in this case, if open.len is zero that's the only time !open.len would be true. We simply call a null GameOver() to trigger a draw here.


Let's take a look at the GameOver() handler.

mob
proc
GameOver(type)
if(type==/turf/nought)
src << "You won!"
else if(type==/turf/cross)
src << "You lost!"
else
src << "Draw!"
for(var/turf/t in block(locate(1,1,1),locate(3,3,1)))
if(t.type!=/turf/board)
new/turf/board(t)


We've got a few new things going on here. Output to the mob's player makes a sudden return now that we're starting to inform the player of what's going on.

Notably, you'll see we're actually using a direct type path in the if conditions. You can compare type paths for equality. This will tell you if the object is exactly that type. /turf is not equal to /turf/nought. Again, we're passing the type into game over from the CheckWin() proc, that way we can determine who actually did the winning.

You'll also notice a new pattern. else if(). else if() only runs if the if or else if above it is false. an else below an else if will only run if the else if above the else is false. This is a shorthand way to chain if-else statements into a series of tests.

As you can see, we have three GameOver conditions. A player win, a player loss, and a draw. We acheive a draw by passing any value that's not /turf/nought, or /turf/cross. An easy thing to pass to mark a draw is nothing. Remember null? Well you don't have to type null for an argument to be null. arguments default to null if you don't set them to anything.

The last bit is a new type of for() loop. Remember how I said that for() loops come in many flavors? Well, this particular for() loop is a for type in list for loop. This loop will loop over only the objects of the type you specify, setting t to each one it finds in the list specified.

the list we're passing as the for list is actually the return value of a new proc called block(). block() will return a block of turfs from the bottom-left to the top-right, given two turfs passed as the corners of the block. This is a fast way to get a specific chunk of the map if you only care about the tiles.

So for each turf in a block from 1,1 to 3,3 (the whole map in our case), we're checking if the turf's type is !=... Okay, so what's !=? ! is an operator denoting "NOT", it's the logical inversion of something. == is the equality operator, so != is the logical inequality operator. It checks if the left and right values are not the same. So it's true if both values aren't the same thing, and false if both values are the same thing.

Provided the turf that we found is not a type of /turf/board, we create a new /turf/board instance at that location. This automatically rebuilds the global.open list, because remember /turf/board/New() adds the turf back into the open list.

mob
proc
Moved(turf/move)
open -= move
CheckWin(/turf/nought)
var/turf/t = pick(open)
open -= t
new/turf/cross(t)
CheckWin(/turf/cross)


And finally, we're going to modify our Moved() proc.

We need to check for a win of /turf/nought after the player moves. We've gotten rid of the open condition check because our win check includes the draw check for each player immediately after their move.

Compile and run. You should now have a pretty nice tic-tac-toe game.

One cool thing is that if you lose, the computer will now suddenly give you the first move, and if you win, the computer takes the first move. This was an accident of design, but it did work out in our favor because that's typically how tic-tac-toe is played in sequential matches. We get this benefit because the game over immediately clears the board without killing the Moved() proc. It's still running, so assuming the player wins, the computer's turn from this game winds up running into the next game. Neat, right?
All of the code from the above excercise:

world
turf = /turf/board

var
list/open = list()

turf
board
text = "."

Click()
usr.Moved(src)

New()
open += src

nought
text = "O"
cross
text = "X"

mob
Login()
client.eye = locate(2,2,1)
client.perspective = EYE_PERSPECTIVE

proc
Moved(turf/move)
open -= move
new/turf/nought(src)
CheckWin(/turf/nought)
var/turf/t = pick(open)
open -= t
new/turf/cross(t)
CheckWin(/turf/cross)

CheckWin(type)
var/list/check_start = list(locate(1,1,1),locate(1,2,1),locate(1,3,1),locate(1,1,1),locate(2,1,1),locate(3,1,1),locate(1,1,1),locate(1,3,1))
var/list/dirs = list(EAST,EAST,EAST,NORTH,NORTH,NORTH,NORTHEAST,SOUTHEAST)
var/turf/t, dir, score
for(var/count in 1 to 8)
t = check_start[count]
dir = dirs[count]
score = 0
while(t.type==type)
++score
if(score==3)
GameOver(type)
return
else
t = get_step(t,dir)
if(!open.len)
GameOver()

GameOver(type)
if(type==/turf/nought)
src << "You won!"
else if(type==/turf/cross)
src << "You lost!"
else
src << "Draw!"
for(var/turf/t in block(locate(1,1,1),locate(3,3,1)))
if(t.type!=/turf/board)
new/turf/board(t)