ID:34297
 
Keywords: tutorial
by Zilal

Deconstruction 1: Checkers

 

In the last chapter, to get you programming quickly, we created a game and then examined its elements. In this chapter, we're going to do the same thing but in the opposite direction, to explore the vital skill of translating game needs into code. We're going to do it with checkers.

 

Sections in this chapter:

I. Identifying game needs

II. Mental models

III. Refining game needs

IV. Translating for the computer

V. Development testing

VI. Introduction to user friendliness

VII. Legal move checking

VIII. Introduction to algorithms

 

I. Identifying game needs

Consider the relationship between you and your computer as a partnership. The computer is here to automate tasks for you; you have to tell it which tasks to automate and how to do it. Look at a computer as a brilliant but dense friend who is completely unable to see the big picture, take a hint, or pick up on anything that isn't explained to him in painstaking detail. (Maybe you already have a friend like that.) Things that are obvious to people aren't so obvious to computers. So when you want the computer to run a checkers game for you, you're going to have write a program that tells it exactly what to do.

That's not really the hard part. The hard part is seeing all those little steps. We humans take for granted our remarkable ability to glance at a scene and know exactly what's going on; we don't realize just how miraculous we are, performing subconsciously the thousands of calculations necessary just to go to the store and buy some milk. It would take a team of engineers years to write the instructions necessary for a robot to do the same (without serious mishap, anyway!).

How do you decide on the tasks to be automated? Think first in terms of needs: what do I need from the computer in order to play checkers?

 

I need it to set up the pieces for the start of the game.

I need it to let me pick up a piece and put it somewhere else.

 

And actually, that's all you need to play checkers with a friend. That's what happens in a tabletop checkers game, after all: you and a friend set up the pieces, then you move them around, deciding for yourselves what's a legal move and when somebody's won. You rely on your shared understanding to make the game progress. And checkers games for the computer have been created with just that.

Of course, that's hardly taking advantage of the computer's power to automate. If that's all we're doing, we may as well stay home. So for a real computer game, let's throw in a couple more needs:

 

I need it to only allow legal moves.

I need it to recognize when someone has won.

 

We'll see how each of the major needs can be broken down into smaller needs as we go along. Once you've got those smaller needs for your game, you just need to know how to translate them into objects, variables and procedures. To be able to do that, it helps to have a better understanding of how our programming language, DM, works.

 

II. Mental models

A mental model is pretty much what it sounds like: an idea in your head of how something works. How accurate our mental models are determines a lot about how we interact with the world's complex systems. If I don't have an accurate mental model of how something works, I might try to:

 

Keep my car from from freezing during a really cold winter by putting 100% antifreeze in it

Stop the drug trade by throwing street-level dealers in jail

Get my friend to open up by criticizing her for how little she shares her feelings

 

These actions would be operating from faulty mental models of how antifreeze, the drug trade, and relationships work. The antifreeze one is an especially good example. If a little is good, more must be better, right? But antifreeze needs to be mixed with water to work effectively.

Obviously, having an accurate understanding of a system is very important. If you don't know just how the computer processes code, you'll waste time trying to find the right way to program your game. And if your players don't know how your game is supposed to work, they'll be confused when playing, or complain your game has bugs when it doesn't meet their (misguided) expectations. Later on in the book, you'll see how to design help files that allow players to develop a good mental model of how your game works; right now, we're going to develop our own mental model for how DM works.

First, some programming terms that might sound familiar:

 

Object: a "thing" that can have its own characteristics and actions. A checkers piece can be an object. It has characteristics (what color it is, where it's located) and actions (it can move). We store the characteristics in variables, and program the actions in procs.

 

Variable: a "container" for information you want to store. It's called a variable because the information can be anything--that is, it can vary. The information that gets stored in this container can be decided while you're programming a game ("steps a piece can move at disabledce = 1") or it can be input during the game ("Whiteplayer's name = Jeremy"). The bit of information held by the variable is a value.

 

Proc: a section of code that tells the computer to perform a specific task. You could use a proc to tell the computer how to set up the game board, figure out if a move is legal, or display what one player said to the other.

 

To get an idea of how the computer handles these things, let's try a visual approach.

Picture a checkers piece sitting on a board. That's our object. If we put on x-ray glasses, we can see all the invisible information attached to it: the checkers piece has a bunch of slots with little lables like "color" and "location." Inside every slot is a wee card that says "red" or "coordinates 2,8". If the computer needs to know the color of the piece, it just looks in the "color" slot for the info. So remember, variables are like slots and values are like cards to go in them.

Attached to the checkers piece on little watch chains are also invisible pamphlets. The pamphlets have names like "Move" and they contain useful instructions on how to move around the board. Any time the computer is told to move that piece, it just has to pull up the pamphlet attached to it to read how; then it follows the instructions. Procs are like instructional pamphlets.

 

Now, we could just leave these pamphlets lying around the game for the computer to find. That would also work. Then when we clicked on a checkers piece, the computer would go looking for the "Move" proc... and I'm sure it'd find it, because computers don't mind extra work. But someone else needs to be able to find the "Move" proc in the code: us. And we do mind extra work. So we're going to attach these functions to their objects because that will make our code much easier for us to understand and work with.

We did the same in our very first game:

 

mob
var/moves
var/obj/stone/holding

verb
say(msg as text)
world << "[usr]: [msg]"

A mob is an object. In DM, there are four kinds of objects programmers usually work with: areas, turfs, objs, and mobs. Each comes with some built-in variables that make it easy to represent a game board, game piece, or player. "Moves" and "holding" are variables. The say() verb is a kind of proc.

This programming concept of attaching everything you can to an object is called object-oriented programming, or OOP. It's popular because OOP code is easier to change around and harder to break. OOP is very modular. You can move around little pieces like building blocks. Other OO languages include C++, Java, Python, and Visual Basic. The opposite of object-oriented programming is procedural-oriented programming, which would be the equivalent of storing all the pamphlets in their own box... or even putting all the instructions for everything in one huge pamphlet to flip through.

Now that you've got a better concept of how the programming language works, let's see how to apply it to our game, checkers. To eventually figure out which objects we'll need to create and which actions we'll need to translate into procs, we must refine our list of what we need the computer to do.

 

III. Refining game needs

It takes practice and experience to be able to identify all the steps the computer will need to take to let us play a game, especially if you've never done any programming. We'll go slow for our checkers deconstruction; let's talk a little bit about how to figure out the items on this list.

Here's our old list. We need the computer to:

 

Set up the pieces for the start of the game

Let me pick up a piece and put it somewhere else

Only allow legal moves

Recognize when someone has won

 

A great way to figure out the other steps is to look at these and ask the question, "How do you tell...?" Let's take our first item:

 

Set up the pieces for the start of the game

 

I can think of two questions for this one: "How do you tell when the game has started?" and "How do you tell where the pieces go?"

You've probably never been sitting with a friend playing checkers and thought, "How am I going to know when this game has started?" It's silly. You just agree on when to start. But the computer, which is terrible at subtlety, needs a clear way to know when the game should start. A simple option is to have the computer start the game automatically once two people have joined.

What about "How do you tell where the pieces go?" In our last chapter, we learned how to set up the map ahead of time, but we have a different situation with checkers. Players shouldn't be able to play around with the pieces until another player has joined and the game has started. So there are a few ways to keep them from doing that. We could have the pieces put right on the map, as in the last game, then make a variable to hold whether the game has started, and change it once two people join, then check it within the obj/Click() proc—that is, when someone tries to click on something. You might be able to figure out how to do that yourself, so I'll show you another way: we'll make a proc that tells the computer where to put the pieces.

Should we make a "game" object to attach this proc to? Well, it depends. If we were making a checkers program where lots of people could join and have their games all going at the same time, sure. But let's stick to a program that just plays one game between two people. In that case we can leave the proc unattached and it won't be confusing.

For that matter, allowing just two people to join needs a proc too. Thankfully, DM provides the Login() proc for us to override.

The more you think about the game, the more steps you'll find for the computer to do ("How will the computer tell who's playing what color? Oh, we'll have to assign that in the beginning"). Here's what you might come up with for checkers.

We need the computer to...

 

Let two people join

      Assign colors

      Start the game

Set up the pieces for the start of the game

Let me pick up a piece and put it somewhere else

      When someone clicks on a piece, change the graphics so they know

      When someone clicks on an empty square, move the piece there

Only allow legal moves

      When a piece is picked up, check to see if belongs to that player

      When a piece is put down, check to see if it can go there

      Also see if another piece has been captured

Recognize when someone has won

      Check to see if all of one player's pieces have been captured

      Check to see if one player is blocked from moving

 

Once we recognize what our needs are, it's time to translate them into objects, variables and procs the computer can use to run the game.

 

IV. Translating for the computer

To do this, we'll have to remember another concept, the object prototype. When we program, we're not really making computerized game boards and checkers pieces. We're just making the blueprint for them.

 

Object prototype: a blueprint that tells the computer how to make a certain kind of object, with certain variables and procs. Note that it doesn't tell the computer when or where to make it, just how.

 

To be able to make checkers, we'll need just three object prototypes: a piece prototype, a square prototype, and a prototype for the player to be. The need in DM for an object to represent a square on the playing surface itself might not make sense to you, but think of it this way: the player will have to click on the square they want to move their piece to, and anything clickable should be an object. After last chapter's experience, are you able to visualize some of what our code will look like? Here's what we'll write to start:

 

//Checkers created by <your name> on <today's date>

var/mob/redplayer
var/mob/whiteplayer
var/mob/whoseturn

mob
var/color
var/obj/piece/holding
verb
say(msg as text)
world << "[usr]: [msg]"

obj
piece
icon = 'piece.dmi'

Click()
usr.holding = src

turf
square
icon = 'square.dmi'

Click()
if (!usr.holding) return 0
usr.holding.loc = src
usr.holding = null

The variables at the beginning don't belong to any object, and we'll be able to access them throughout the game. Some other notes about what you see above:

 

1. A slash (/) is the same thing as hitting return and tabbing; it means whatever follows the slash belongs to what came before it. Since whatever follows "var" is bound to be small, it makes sense to keep it on the same line.

2. We can access the value of an object's variable through the "." operator. It looks like a humble period, but it's more like a command that, here, tells the computer to find the value contained by that particular usr's "holding" variable. It deals with "instances"—created objects—rather than prototypes, and we can only use it inside procs.

3. When any variable or proc is built in to DM, we don't need to "declare" it by putting "var/" or "proc/" in front of it. That's just for the stuff we make up ourselves.

 

You can guess which .dmi icons we'll need to make, and what kind of a map. We'll need a piece icon with two different states, one for red and one for white; we'll need a square icon with dark and light states; and we'll need a map made up of 8x8 alternating squares. Try to make those now. Refer to the previous chapter if you need any reminders. Remember to generate instances from icon states to be able to make both light and dark squares for the game board. Allow yourself to get fancy with the icons if you'd like to be able to keep and share your checkers game; otherwise, don't put too much effort into it!

Compile to save and check for errors. If you try running the game, you won't see much happen. We haven't programmed in any of our actual tasks; we just set up a framework to build off. So let's look at our first couple tasks.

 

Let two people join

      Assign colors

      Start the game

Set up the pieces for the start of the game

 

DM's wonderful Login() proc will allow us to accomplish this task. Above the say() verb, tab to the same level as the var declarations and put in:

 

    Login()
if (!redplayer) //If they're the first player in...
redplayer = src
src.color = "red"
src << "You will be the red player. Waiting for player two..."

if (src != redplayer && !whiteplayer) //If they're the second player in...
whiteplayer = src
src.color = "white"
src << "You will be the white player."
StartGame() //Start the game now

if (src != redplayer && src != whiteplayer)
src << "You are a spectator."

Remember, "src" means the source of the proc, or who the proc is attached to in the code. "Usr" refers to whoever typed the verb or made the click that led to the line of code that has that "usr" in it. Since players are just logging in, no one has typed or clicked anything yet, making "src" appropriate.

We use the if statement (really a special kind of built-in proc) here to see whether we have none, one, or two players. The &&, or "and," operator is useful; it means that both statements must be true for the code in the block to proceed. What do our if statements mean?

 

if (!redplayer) means "If there's no red player, do this…"

if (src != redplayer && !whiteplayer) means "If the player isn't already the red player, and there's no white player, do this…"

if (src != redplayer && src != whiteplayer) means "If the player isn't already the red player and they're not the white player either, do this…"

 

Now we need to write a StartGame() proc. At the bottom of your code, put:

 

proc
StartGame()
world << "Game starting. Red moves first."
for (var/turf/square/T in world)
if (T.icon_state != "dark") continue //if it's not a dark square, go to next
if (T.y <= 3)
var/obj/piece/O = new /obj/piece (T)
O.icon_state = "red"

if (T.y >= 6)
var/obj/piece/O = new /obj/piece (T)
O.icon_state = "white"

Here we use a "for" loop to go through every dark square in the world. As in the last chapter, what's going on is we're creating a temporary (only available within the proc) variable called "T" that's going take turns holding every square in the world as a value. If the square isn't dark, we use the continue command to tell the computer to move to the next square.

If it is a dark square, the computer checks to see where on the board it is. If it's in the bottom three rows (that is, if its vertical coordinate, y, is less than or equal to 3) we put a red piece on it; if it's in the top three rows, we put a white piece on it. To evaluate the y coordinate of the squares, we use the <= ("less than or equal to") and >= ("greater than or equal to") operators.

Try compiling and running the game now. What's going on?

 

V. Development testing

We have a runtime bug. The screen is black! It's almost as if we didn't put any objects on our map. Since we did put objects on our map, we know it's that we just can't see them. So why can't we see them? Are we blind? No, it happens that we're not in the right place to see them. We didn't call the parent proc of our modified Login(), so Login() never did what it was built in to do. The built-in Login() proc places the player on the map; let's call the parent of our modified version by putting ..() on the first line of the Login() code.

If you try the game now, you'll be able to see the board, but no checkers. That's because we need two people to log in to be able to start the game. Since it's inconvenient to have a friend stand by as your personal testing slave the entire time you're desiging a game, you'll have to be both players yourself. In the checkers game you're currently running, click the "Host…" button. Type in a number you can remember for the port, change visibility to "invisible," and click OK. You're now hosting checkers so that other players can join… but nobody will, since no one will be able to see it.

 

Open the BYOND program. Under the File menu choose "Logout." Then log back in as Guest. Under the File menu, choose "Open Location…" and type in the port number you entered earlier. You should connect to your hosted game. Now you have two copies of Dream Seeker running, one with the perspective of each player.

 

Testing games this way allows us to see many things we might otherwise have missed. For instance, there's no message to tell the world that a new player has joined. So above the "You will be the <color> player" messages, put:

 

                  oview(src) << "[src] joins the game as the red player."

 

and

 

oview(src) << "[src] joins the game as the white player."

 

and above the "You are a spectator" message, put:

 

                  oview(src) << "[src] joins the game as a spectator."

 

The oview() proc is like view(), but it excludes the usr (or whoever we specify) from the list, so our messages will be output to all the players on the board except whoever just logged in.

As you program, compile the new versions of your game and test them. After you compile, go to the "Host" Dream Seeker and select "Reboot World" from the Options menu. The game will restart with the new code, and anyone already connected will rejoin. (If you get the error that the .rsc, the resource file, is locked up, then you'll have to start over.)

It's vital to test features right after you code them, rather than writing large chunks of code and testing it all at once. Your memory will be fresher and you'll catch little problems before they turn into big problems. That's especially important if you're designing an original game; certain things that seemed like a good idea when you dreamed them up might turn out not to work at all, and it's good to know that as soon as possible. We'll call this type of testing "development testing" to distinguish it from "play testing," or testing the game with real players.

 

VI. Introduction to user friendliness

If you experiment with the mouse in the game now, you'll see you can move pieces around—but it's not clear when you've picked one up. We'll have to change that. In our first game, we indicated that a piece was picked up by simply removing the it from the board. In game like checkers, where the location of a piece in relation to other pieces matters, that wouldn't be very user-friendly.

User friendliness makes a huge difference in the player's experience, and one aspect is obviousness: it should be obvious when a player's selected a piece, whose turn it is, and so on. In this case, it's something that's already in our list of tasks:

      

Let me pick up a piece and put it somewhere else

      When someone clicks on a piece, change the graphics so they know

      When someone clicks on an empty square, move the piece there

 

The right graphics can make or break a game. Note that the graphics don't have to be 3D, terribly realistic, or even perfectly smooth to get players interested. They just have to be appropriate for the game.

It's essential to provide a response for your player—that is, to show the player by either sight or sound that he has selected something. An easy way to indicate which piece has been selected is to draw a circle around it. Go to your piece icon, right-click one of the states to copy it, then create a new .dmi file called "selected." Paste in the piece and double-click to edit it. Draw a fat, bright circle around the edge of the piece and then erase the piece inside. This will be our new selection indicator. (If it has an icon state name, delete that.)

 

At the beginning of the piece/Click() proc, put:

 

src.overlays += 'selected.dmi'

 

And below the if statement in the square/Click() proc, put:

 

usr.holding.overlays.len = 0

 

The overlays var is a built-in variable. It holds a list of objects or icons to display on top of its source. We can add to it with the += operator, which says "the thing on the left now equals itself plus the thing on the right." When we set this list's "len," or length, to 0, it's the same as getting rid of all the items in it.

Remember to compile or save all.

If you move the pieces around randomly now, you'll notice that the movement is animated when a piece is moved one square away, but not when it's moved greater distances. DM has a built-in variable called "animate_movement," and its default setting is 1 (which often means "positive" or "yes" in programming). We want to set that variable to 0 for our pieces. So under the icon assignment in the obj/piece code, put:

 

  animate_movement = 0

 

As you've noticed, you can pick up any piece and move it anywhere on the board. It's time to work on legal move checking.

 

VII. Legal move checking

Most board games, from chess to CandyLand, have some kind of rule that tells you which places you can move what piece at what time. The most challenging part of programming a board game is often writing the code that checks to see whether a move is legal. There's a surprising amount involved in a legal move in checkers, but it makes a great example for us to practice with and it's all very doable. Here one of our original tasks, with some new sub-tasks throw in:

 

Only allow legal moves

      When a piece is picked up,

Make sure it belongs to the player

Make sure it's their turn

      When a piece is put down, check to see if it can go there

            Make sure it's a dark square

            Make sure there isn't already another piece on it

            Make sure piece is moving forward

            Make sure new square is adjacent, or there's an enemy-occupied square between

      Also see if another piece has been captured

 

The second new sub-task, "Make sure it's their turn," will require us to use the whoseturn variable we put at the beginning of the program. At the beginning of StartGame(), make whoseturn = redplayer. And we'll need a way to switch turns after a player does set a stone down. At the end of square/Click(), call the NewTurn() proc, which we'll define above the StartGame() proc:

    NewTurn()
switch (whoseturn.color) //checks what the "color switch" is set to...
if ("red") whoseturn = whiteplayer
if ("white") whoseturn = redplayer

whoseturn << "It is now your turn."
oview(whoseturn) << "It is now [whoseturn]'s turn."

We evaluate whoseturn.color rather than whoseturn directly above because, to allow the command to be so efficient, switch has to know what its "if" values will be when the game is compiled. That means we can't put a variable in the parentheses, because a variable's value can turn out to be anything once the game is running. We need to use an actual value, like "red."

You can probably guess how to check whether it's the player's turn when they click on a piece. The first new sub-task, "Make sure it belongs to the player," is also very simple, and we'll code both with if statements at the beginning of the piece/Click() proc.

 

        Click()
if (whoseturn != usr)
usr << "It's not your turn!"
return 0

if (src.icon_state != usr.color)
usr << "That's not your piece! You're playing [usr.color]."
return 0

if (usr.holding) //if they already selected a piece...
usr.holding.overlays -= 'selected.dmi'
usr.holding = null //unselect it

src.overlays += 'selected.dmi'
usr.holding = src

We use both "usr" and "src" in this proc now. Remember that the src is who the proc belongs to in the code (the piece, in this case), and the usr is who typed the verb or made the click to get to that line (the player).

Some of our other sub-tasks are also simple:

 

      When a piece is put down, check to see if it can go there

            Make sure it's still the player's turn (game hasn't ended)

            Make sure it's a dark square

            Make sure there isn't already another piece on it

 

Here's how we'll code those. At the beginning of the square/Click() proc, put:

 

            if (whoseturn != usr)
usr << "It's not your turn!"
return 0

if (src.icon_state == "light") return 0
if (locate(/obj) in src.contents) return 0 //if there's already a piece

When we tell the proc to return, we're telling it to stop there and go back to whatever code called it, if any. There was no calling proc in this case, but we're returning a particular value anyway, as a message to ourselves: if 1 usually means "yes" in programming, 0 means "no." By returning the value of 0 for a negative outcome, we can see at a glance which parts of a proc deal with negative outcomes. This is useful when editing code.

The next line uses built-in procs and variables that are new to us. The locate() proc allows you to search for a certain type of object in an area or list of things; if it finds such an object, locate() returns the value of 1, so our if statement will evaluate as true. The list it's searching within is src.contents. "Contents" is a built-in variable, a list that keeps track of all the objects in or on another object (such as the pieces on a square).

In the square/Click() proc we just changed, we gave the usr a message before we returned 0 in order to make our game friendlier. If the player tried to pick up a stone and nothing happened, he might think the game was broken—unless he got a message otherwise. But since almost everyone already knows that you can only move diagonally in checkers, and can't land on top of another piece, we won't put in a message for those events (though you certainly could).

Compile and try the game again! As you can see, we still have some work ahead of us. We're going to need to write some calculations to handle where the pieces are allowed to move.

Many people think of computer programming as something that belongs in the same group of subjects as math and science. Programming does require a lot of logic, but also a lot of translation. In that respect, it's more about language than about math, and you can make great, deep games without needing to use any math at all. However, for some kinds of games, you'll need to learn or create algorithms.

 

VIII. Introduction to algorithms

Does the word "algorithm" seem a little intimidating? Don't worry—you've already made some, without even knowing it. An algorithm is a set of calculations that will always give you the result you're looking for. In a broad sense, any computer program can be an algorithm; in the sense we're going to use, this code from our StartGame() proc is:

 

        for (var/turf/square/T in world)
if (T.icon_state != "dark") continue //if it's not a dark square, go to next
if (T.y <= 3)
var/obj/piece/O = new /obj/piece (T)
O.icon_state = "red"

if (T.y >= 6)
var/obj/piece/O = new /obj/piece (T)
O.icon_state = "white"

Remember that we could have used the map to set up the pieces for the start of the game, but instead we had the computer calculate where they should go, based on these instructions. We didn't tell the computer how many squares there are. We didn't even tell it how many pieces should be set up—yet our instructions still allowed the computer to set up the pieces perfectly. We could even change the width of our board, and the computer will keep up by adding more pieces exactly where they should be.

So we're going to do something similar with our next tasks:

 

            Make sure piece is moving forward

            Make sure new square is adjacent, or there's an enemy-occupied square between

      Also see if another piece has been captured

 

It's possible to do these things without a nice efficient algorithm. We could write very extensive code saying things like "if the piece is at 1,1 and wants to jump to 2,2, allow it. If the piece is at 1,1 and wants to jump to 1,2, don't allow it"… and so on for every single possibility in the game. We'd also have to add a bunch more lines if we changed the size of the board. Next to that, a little bit of math doesn't sound so intimidating after all.

First let's just write the code to check if the piece is moving in the right direction. To do that, we'll have to know whether or not it's been kinged, so we'll need a "kinged" variable. Beneath the piece's animate_movement variable, declare a new one: "var/kinged."

Then, in our square/Click() proc, beneath the if statements that are already there, put:

 

    if (!usr.holding.ForwardCheck(src))
usr << "Only kinged pieces can be moved backward!"
return 0

You can read that first line as "If the piece the user is holding doesn't pass the forward check, do this." Now we have to have a ForwardCheck() proc, which will be attached to the piece, and we're going to pass something into it: src, which in this case is referring to the square the player is trying to put the piece on, the one the usr clicked to start this process. Below all the piece code, tabbed to the same level as the word "piece," put:

        proc
ForwardCheck(turf/square/thesquare)
if (src.kinged) return 1
if (src.icon_state == "red" && thesquare.y > src.y) return 1
if (src.icon_state == "white" && thesquare.y < src.y) return 1
return 0

The source of the calling proc, square/Click(), was passed in as an argument, just like our say() verb takes text as an argument. In the say() verb, we called it "msg"; in this proc, we're going to name that information "thesquare." By writing it as turf/square/thesquare, we're saying that's what's supposed to be passed in, and the program should treat it as if it were an object made from the turf/square prototype.

Inside the ForwardCheck() proc, we use src again to refer to the source of the proc—this time, the piece. If the piece is kinged, it can move anywhere. If the piece is red, it can move only to squares above the one it's on (that is, with a y coordinate greater than the y of its own square); for white, it's the opposite.

And we need to make sure that either the square it's moving to is adjacent, or there's an enemy piece between. Here's our new square/Click() proc.

        Click()
if (whoseturn != usr)
usr << "It's not your turn!"
return 0

if (src.icon_state == "light") return 0
if (locate(/obj) in src.contents) return 0 //if there's already a piece
if (!usr.holding) return 0
if (!usr.holding.ForwardCheck(src))
usr << "Only kinged pieces can be moved backward!"
return 0

var/distance = get_dist(usr.holding.loc,src) //distance between squares
if (distance > 2)
usr << "That square is too far away!"
return 0

if (distance == 1)
usr.holding.overlays.len = 0
usr.holding.loc = src
usr.holding = null
NewTurn()
return 1

//This code will only be reached if the distance is 2
var/obj/piece/jumped = usr.holding.CanJump(src) //CanJump() can return piece
if (!jumped) //if there's no piece to jump...
usr << "You may only jump over enemy pieces."
return 0

else usr.Capture(jumped)
usr.holding.loc = src
if (!usr.holding.CanJumpAgain()) //if it can't jump again, end turn
usr.holding.overlays.len = 0
usr.holding = null
NewTurn()

We need to be able to see how far away this square the player wants to land on (src) is from the square of the piece the player is "holding" (usr.holding). We use a built-in proc, get_dist, for that. If the distance is greater than 2, the square's too far away for any legal move. If the distance is 1, there's our regular move; we return from the proc after the move is made. The rest of the code will only be reached if the distance is exactly 2: the right distance for jumping. We create a variable, "jumped," to hold whatever CanJump() returns. If there are no pieces that can be jumped by moving to src, CanJump() won't return anything, and we'll return 0 from Click(), meaning the player's attempt to move won't be successful. But if CanJump() returns a piece, we'll capture it.

Since that would be a successful move, that's when we finally move the player's piece (usr.holding) by changing its loc. Then we'll see if there are any other jumps the piece is capable of making. If there aren't, we remove the "selected" circle icon from the piece, and end the player's turn.

We just called some procs we haven't written yet. Let's write them. We need a piece/CanJump(), a piece/CanJumpAgain(), and a mob/Capture(). Above the piece's ForwardCheck() proc, put:

            CanJump(turf/square/endsquare)
var/turf/square/midsquare
midsquare = get_step(usr.holding,get_dir(usr.holding,endsquare))
var/obj/O = locate(/obj) in midsquare
if (!O) return 0 //if there's no piece in the middle
if (O.icon_state != usr.color) return O //return piece that can be captured

CanJumpAgain()
for (var/turf/square/T in view(src,2)) //go through all squares within 2
if (abs(src.x - T.x) != 2) continue
if (abs(src.y - T.y) != 2) continue
if (locate(/obj) in T) continue //if it has a piece on it, skip
if (!src.ForwardCheck(T)) continue //if it'd move backward, skip
if (src.CanJump(T)) return 1 //if the piece could jump onto that square

return 0 //if the piece can't make any jumps, return 0

In CanJump(), we create a variable called "midsquare" to hold the value that the built-in proc get_step() will return. Get_step() takes as arguments a starting location and a direction; we use the player's piece as the starting location, and to get the direction it wants to head, we use the get_dir() proc, which takes as its arguments two locations to calculate the direction between. The value that "midsquare" ends up holding is the square that's one step away from the piece in the direction it wants to go—in short, the square the piece is jumping over.

Next we create another variable, O, to hold an obj/piece if one can be found in midsquare with the locate() proc. If there's no O, no piece to be jumped over, we return 0. If there is an O, and it belongs to the enemy, we return it so the caller proc can use it on the lines that say "jumped = usr.holding.CanJump(src)" and " usr.Capture(jumped)." To recap all that code, we've enabled to computer to see if there's a piece on the square being jumped over, to make sure it's an enemy piece, and to return a reference to it so the piece can be captured.

In the CanJumpAgain() proc, we look at all the squares that are 2 away diagonally, and check each to see if we could get there by jumping another piece. To do that, we go through all the squares in view() of the piece, within a range of 2. Since that will return squares that are only 1 away and light-colored squares too, we ignore those squares that aren't exactly 2 squares away diagonally, using a neat math trick. By checking the difference between the piece's x and the square's x, and also comparing their y coordinates, we can say, "If the square isn't reached by going exactly 2 steps horizontally and 2 steps vertically, move on to the next." We check that difference in coordinates with the abs() proc, which returns the absolute value of what we put in. In this case that's one x coordinate minus the other, or one y minus the other. (If it's been a long time since math class, recall that absolute value is essentially the positive version of any number.) So if the piece is at 5,3 and the square we're checking is at 7,5, the subtraction will return -2,-2. Abs() converts this to 2,2. And only if the square is 2,2 away will the next line be reached to see if there's already a piece on it. If there is, we try the next square in view.

Using "continue" tells the computer to forget about the object that it's on and move to the next one in the loop. If the square the computer is currently looking at passes all those checks—if it's never skipped—we use the CanJump() proc to see if the player's piece could actually make a jump to this square.

All of this allows us to decide, once returned to square/Click(), whether to end the player's turn or not. It seemed so much simpler when you played checkers in real life and just "knew" whether your turn was over! This is a telling glimpse at just how differently people and computers see the world.

We still need a mob/Capture() proc. Below mob/say(), tabbed to the same level as "verb," put:

    proc
Capture(obj/thepiece)
usr << "You've captured a [thepiece.icon_state] piece!"
oview(usr) << "[usr] has captured a [thepiece.icon_state] piece!"
del (thepiece)

This proc may be self-explanatory for you. When we call Capture(), we pass in the captured piece, which was being held in the "jumped" variable in square/Click(). We send one message to the usr and another to everyone outside the usr; then we delete the jumped piece.

 

[To be concluded later this week!]
Another good article, but also don't forget all of the requirements! Jumping is mandatory whenever possible, so if one piece can jump, you can't opt for a non-jumping move, and you must continue to jump as long as a jumping piece lands in another jump-worthy position. :)
Good catch, Hiead -- or it would have been if it was missing from tonight's part II! :)