how does your language... make a map?
- From: "Colin MacIntyre" <colin@xxxxxxxxxxxxxxxx>
- Date: Fri, 24 Mar 2006 00:21:43 +0800
Hi all,
In today's coding session I developed some useful tools for converting
between arrays. Sitting back and looking at it, I noticed that in only a few
lines of code it demonstrated a lot of what Forth is really all about, and
I'd like to share it with you. Also, I've heard a lot of good things about
FreePascal, BlitzMax, Java, Python etc. and I'd appreciate getting a taste
for how these (and any others) accomplish fundamental game-making tasks.
Would you be interested in submitting your language's version, or perhaps
your personal take on your language's version, as a kind of joint learning
exercise?
I'll start with:
- How to make basic 2-D ASCII maps in Forth -
This first post is for beginners, where I'll try to explain things in a
detailed way. Experts may want to wait for the next one. Monospace font is
recommended as there are some diagrams.
Like other languages, Forth code is often written in an editor, and then
"include"ed in the interpreter for testing. For these examples, I am working
directly at the interpreter. You will notice a number in brackets under the
code. Since Forth uses a Last-in-First-out implicit stack, or as some call
it, a "push-down" stack, this is how I will describe the stack depth, and
the values on the stack at that point. In the interpreter, the word ".s"
does the same thing. After each example, it is assumed that I will clear the
stack.
Okay, enough jibber-jabber! The first thing I want to do is choose the size.
Let's say we want a map 60 characters down by 80 characters across. As a
place to store these integers, I have a choice between variables, constants,
or values. What's the difference between these? Well, to define variables
I'd have to type:
variable rows | define a variable named rows
60 rows ! | the number 60, in rows, "store" it
variable cols
80 cols !
(0)
and then to get the values from it:
rows @ | read as "rows fetch"
(1) 60
cols @
(2) 60 80
but right now this is too much work for two little numbers I don't need to
modify, only replace. On the other hand, constants should only be used for
things that don't change, so that leaves us with values.
60 value rows
80 value cols
(0)
To fetch, I simply type the name of the value, a la:
rows
(1) 60
I change my mind, 60 rows is too big, so I just go:
40 to rows
(0)
Next, I want to create an array that will in the future contain all the
ASCII characters that need to be stored and printed to the screen. In Forth
there is no word like "array", so I must define my own. I want this to be a
special array, one that accepts two values, rows and columns, and multiplies
them together to create a static-sized structure (say that 5 times fast).
: c-array ( n n --) create dup , * allot ;
Whoa, what the heck is all that? Well, let's start at the beginning. As we
know, Forth uses an integrated interpreter to parse, or understand, what you
are typing. The nature of the interpreter is trivial, caveman simple. Let's
say I type in:
80 20 +
| | |
| | |___ word, yes. go to the dictionary and do what it says to do (add
2 numbers together)
| |__ word, no. number, yes. move along...
|_ is this a word in the dictionary? no. a number? yes. move to the next
non-space.
Failing these two checks, it will error out with something like, "stack
underflow"
So back to our array, the word ":" tells the interpreter to start making a
new word. More specifically, it tells it to stop interpreting and enter
compile-mode, create a header in the dictionary, and call it whatever name
comes next.
But what does "create" do? Aptly enough, this allows us to create a data
structure in memory. A simple example is the definition for the word
"variable"
: variable create 0 , ;
| | | | | |_ stop making a new word, switch back to
interpreting
| | | | |_ place whatever's on the stack at the next
available address.
| | | |_ put the number zero on the stack.
| | |_ create a header at the next available memory address in
the dictionary, and
| | | name it whatever is next
| |_ call it "variable"
|_ stop interpreting, start making a new word.
So when we type:
variable rows
the word "create" inside the word "variable" pauses to look for and store a
name, in this case "rows", and then continues on by storing a zero next to
it. That's why all variables in Forth are initialized to zero.
Back, back to our array!
: c-array ( n n --) create dup , * allot ;
You may wonder about this:
( n n --)
It is simply a comment, a picture of the stack for our convenience, telling
us how to use this word. Everything before "--" is what should come before
the word on the stack, and everything after "--" is what the word produces.
In this case, "c-array" expects two integers on the stack, as if we typed:
rows cols c-array ...
but as the stack diagram shows, nothing will come out. It eats the whole
stack. What happened? Let's look at the rest of the definition,
dup ,
"dup" duplicates, or makes a copy of the value on top of the stack, and as
we saw before, "," will then place that value in the very next memory
address. So what is on top of the stack? In the above example, "cols" is, or
more accurately, it's value, 80, is. "cols" value is a special number as it
determines how many columns, or how wide, each of the rows are on our map.
More on that later.
Next, we have two more words,
* allot
which multiplies two numbers together, the product of which is the memory
space "allot"ted to our data structure.
That's it! ";" closes the deal, and we now have a word we can use and re-use
to make arrays. We have also extended the language, temporarily until we
close the interpreter session, or permanently if we take the time to insert
it into the language source and re-compile. Let's make our first map called
Terrain.
rows cols c-array Terrain
80 40 * = 3200 cells for us to populate with ASCII. Now this isn't much good
unless we have a way to point at each of those byte addresses in memory and
store a character there, so let's make another word that, given x and y,
will get us an address in a 2-D array.
: xy ( x y a -- a') swap over c@ * + + 1+ ;
after which we'll type:
10 15 Terrain xy
_ Comment Legend __
|x - column # |
|y - row # |
|a - memory address |
|a'- new address |
|___________________|
Alright, this one will take a bit of explanation. Let's see what's on the
stack at the point the interpreter enters the word "xy"
(3) 10 15 4232373
Remember, the stack is always read from left to right. Obviously, that big
number on top of the stack (TOS) must be the beginning memory address for
our Terrain array.
swap
simply swaps the top two items on the stack, giving us:
(3) 10 4232373 15
over
does two things. First it dup's, or copies, the /second/ item, now 4232373,
and then leaps it over top of the first item to place it TOS.
(4) 10 4232373 15 4232373
Now we have a copy of the address, ready and waiting for us to fetch a value
from it. What is that value going to be? Remember "cols" and how it was
important enough to store in the very definition of our word "c-array",
right next to the header? And remember "@" from the variable section? Well
"c@" is the same, except it only fetches one byte of data, which is all we
need. So c@ is going to eat the address, and spit out the number 80 stored
there, which is the number of columns we specified for our Terrain array. Of
course we know that a 2-D array isn't really 2-D in memory, it's just one
long string of data. But we can simulate 2-D by multiplying by the number of
columns, the /offset/ we need to go to any area of the map. Now let's look
at the stack, and then the rest of "xy" in a diagram:
(4) 10 4232373 15 80
: xy swap over c@ * + + 1+ ;
| | | |
| | | |_ increment by one.
| | |_ add the col# (10)
| |_ add the starting address of Terrain to that result
|_ multiply cols (80) by the row# (15) of the map square
(array address)
| we want.
We want to increment by one at the end because the first address in our
array isn't 0,0 on the map, it's our offset number (80). Now we have a new
address, the map square at coordinate 10,15 in Terrain. Let's store an ASCII
character there:
'# swap c! | "swap" because "c!" expects the address, not the
character, on TOS ( c a --)
Now if we printed our map, it'd be empty except for one little dungeon wall.
We'll close out this session by creating two more identically-sized arrays,
one for creatures and the player to move around in, and one for colour
information. We'll also make a variable, and another useful word.
rows cols c-array Play
rows cols c-array Colour
variable currentmap
: map-size rows cols * ;
Later I'll get to the tools I mentioned, which we can use to convert between
these arrays and really kick our map into gear.
Colin
*** Free account sponsored by SecureIX.com ***
*** Encrypt your Internet usage with a free VPN account from http://www.SecureIX.com ***
.
- Follow-Ups:
- Re: how does your language... make a map?
- From: Colin MacIntyre
- Re: how does your language... make a map?
- From: Martin Read
- Re: how does your language... make a map?
- From: stremler
- Re: how does your language... make a map?
- From: Chris Morris
- Re: how does your language... make a map?
- From: Timofei Shatrov
- Re: how does your language... make a map?
- From: Slash
- Re: how does your language... make a map?
- From: Radomir 'The Sheep' Dopieralski
- Re: how does your language... make a map?
- Prev by Date: Re: What to do with Gigabytes...
- Next by Date: Re: What to do with Gigabytes...
- Previous by thread: Annoncing LeTrain 1.3
- Next by thread: Re: how does your language... make a map?
- Index(es):
Relevant Pages
|