No worries, though. This is the kind of hangup where I'll come back over the weekend and instantly see what needs doing. All I need do is let the idea sit untouched for a few days, and I'll think of something. As I said previously, this is not the first attempt at the triangle maker, it's just the first successful one after a few months: so what's another day or two?
In the meantime I'm going to explain how the map renderer itself works.
First, the World Generator program creates, as its primary output, three files which contain data about my game world. One file contains info on towns, another is for roads, and the third and largest contains data about the hexes themselves -- including their sub-triangles.
Each generator output file corresponds to a parser I've written in Haskell. Now what is a parser? Well, it's a type of program which consumes ordinary text and attempts to arrange it into a structured format for further manipulation. Initially, World Generator output contains chunks like "Elevation 0.133533" and "Climate Mediterranean" and "[Coord (49, -65, 16),Coord (48, -65, 17),Coord (48, -66, 18)]". However, plain old text like this isn't useful to a computer; we have to pre-specify that, for example, "Elevation 0.133533" refers to a type of data, with "Elevation" being the data constructor and "0.133533" being the value. If the string were malformed due to an error -- say that "Elevation" was spelled "Evelation" -- the parser would fail to parse, i.e. be unable to assign the structure I've specified to the text string.
Here's the structure I'm using to store information about a sub-triangle (referred to as simply a "Sub" here):
The first definition states that SubInfo is a 3-tuple. I parse that 3-tuple, and then immediately convert it into a correspondence between a Direction (definition not shown) and a Sub, specified on the next line, since I need to be able to look up that data by naming a given Direction.
Next, here's the parser which verifies input data and converts it into the above SubInfo structure.
Note that parseSubInfo is a parser which it itself composed of smaller parsers, as I've indicated with arrows. For example, parseQuality is defined as validating (and then throwing away) the string "Quality", and then doing the same with some spaces. Then it tries to validate either the word "Civilized" or the word "Wild." If successful, it converts that word into the appropriate data type (a Civilized or a Wild -- no longer a string, but a value.)
Higher up in the program I use parseSubInfo itself as a component in an even larger parser, which slurps up all the data for a single hex. This technique of combining small parsers into larger ones is extremely intuitive and straightforward; the functions here are usually referred to as parser combinators, because of their combining nature. (Moving forward, unless I say otherwise, when I say "parser" I mean the final, primary parser in each program, which may be made of smaller components.)
So: one parser for roads, one for towns, one for hexes. We use the parsers to structure the generator output into useful shapes in memory, and then pass that now-useful data into the main body of World Renderer. This program does the actual work of drawing each layer of map data into the final image. Such work includes:
- calculating the pixel coordinates at which a given hex should be drawn, based on its hex coordinates
- coloring each hex based on climate, or moisture, or elevation, or whatever else I specify from the data (thus enabling me to recreate the entire map with a new color scheme, whenever necessary)
- drawing in the roads between the locations of each town
- drawing the little town-name indicators themselves
The important part to take away is that most of what I'm doing is defining different ways to draw different data. If I added a new kind of info to each hex, all I'd have to do is specify how it should be represented as a shape or color, and I'd be ready to render a freshly improved map. I can also specify exactly which hexagons I want to render, which lets me zoom in on chunks of the world, as has been the case with the maps I've posted recently.
Here's one function from the renderer program. It calculates the output pixel coordinates for a given hexagon, based on its cubic coordinates.
And here's the primary function (one below the top-level function, which calls the parsers and then feeds their results into this function.) I think my naming conventions make it clear what is going on: we draw the hexes, then we draw the roads on top of them, then we write the town names on top of that. Note that we don't need to draw the town names before the roads, since although they haven't been rendered yet, we still have their locations in memory as part of the road info. Also note that the definition for "filtered" is how I restrict the render to a certain section of the whole ("wanted" is defined elsewhere.)
I give the renderer executable program a filename and it draws the map into that file. And that's all there is to it!
For the record, the last step is the one I'm stuck in for the civilized/wild triangle data. I'm having difficulty specifying exactly the right commands to extract triangle data for each hex and color them appropriately. Everything's parsed and ready; the renderer just needs its new drawing commands. Now you have an idea of how close I am to nailing this!
Please speak up if I've piqued your interest or if something's unclear.