Today we continue the tour of the “code base” of my suite of “assistive programs” for players and Judges of Strategic Primer, which I began last month with a tour of the “controller” component. This post looks at the “model” on which the various “views” rely.
As I said in my introduction to last month’s post, this tour is primarily aimed at anyone who might like to contribute code or otherwise help in the development of these programs. But players may find it interesting or useful. And you can follow along in the repository on BitBucket.
The model has been in a lot of flux of late. Until quite recently there was the “back-end model” representing the game-world itself (maps, tiles, etc.—more on that later), and the “front-end model” designed for the map viewer’s use, which some other “views” (including command-line drivers I covered last month) awkwardly used and some utterly ignored. But over the last few weeks, I’ve done a lot of refactoring and other improvements, so that each of the two major “views” so far is backed by its own model designed for it, as are some of the command-line drivers.
I’ve done my best to gather all the “model” code under the
model package, which is divided into several subpackages.
The “misc” subpackage, which I only created quite recently, contains two interfaces for “driver models” (model objects designed for views to be built upon) to implement, and two abstract classes that implement the cumbersome or tricky yet generic bits of those interfaces so that specific drivers’ models don’t have to include the code over and over. The first interface,
IDriverModel, which I expect every model “driver model” will implement, calls for a model to have a map-view object (more about that below) that can be changed (so that an entire view can share a “driver model” object without having to recreate the whole thing to load a new map), to know the filename from which it was loaded (making a “save” rather than just “save as” menu item possible, another recent improvement), and to know the dimensions of the map. The interface also extends the PropertyChangeSource interface from my
util package, meaning that a driver model has to keep track of other objects that want to be notified of changes to its properties.
The abstract class in this sub-package implementing this
AbstractDriver, handles the boring details: managing
PropertyChangeListeners, updating its state and notifying listeners when the map is changed, and so on.
The other interface in this sub-package,
IMultiMapModel, and the
AbstractMultiMapModel class implementing its details, are extensions of the interface and abstract class I just described that add the notion of a collection of “subordinate” maps, which both the exploration driver and the “subset” checking driver, among others, need and had previously managed on their own.
There are two sets of things in this package. The first is the classes implementing what I now call “the old encounter model,” and the second is the driver model used by the recently-developed exploration interface. I recently moved the first set into the
The “old encounter model” involved loading a set of “encounter tables” from file, then using them to determine what an explorer might encounter at any given spot. There are several kinds of tables, and each one in any given place can either give a result or say, “The result will be something from such-and-such table.” Each kind of table determined its result by a different method: either always returning the same thing (a
ConstantTable), determining which “quadrant” of the map the current location fell in and always returning the same thing in any given quadrant (a
QuadrantTable), randomly determining the result (a
RandomTable), basing the result on the current tile’s terrain (a
TerrainTable), or using the “event” number that tiles used to have under the “old old encounter model” to come up with what that “old old model” used to produce (a
LegacyTable). There’s also an
ExplorationRunner class that uses the tables to produce the results that model called for.
All of that, however, is essentially obsolete; we leave it in the tree only because it’s useful for populating an empty map with lots of quasi-reasonable data.
The remaining contents of this package are an interface for a driver model that an exploration “app” would use, and a class implementing that interface. This
IExplorationModel interface, which extends the “multi-map” model I mentioned in the previous section, provides several “helper” methods: to list all the players who are included in all the maps, to list all of a specified player’s units, to move a unit in all of the maps, and to find a fixture’s location.
This is the one package in the whole project, I think, in greatest need of reorganization—there’s simply too much here. And this is with two sets of classes pulled into subpackages already. But because no such refactoring is immediately obvious, I’ll leave it as is for now, and just take you through the package in a somewhat orderly fashion.
Let’s start with two interfaces:
IMapNG. The former is what both “maps” and “map views” implement, allowing code that doesn’t care about “map view”-specific features to handle either case. (More on that in a moment.) It’s sort of a legacy of the transition from just handling “maps” everywhere to primarily dealing with “map views”, but since I prefer to program to interfaces when I can I leave it in. A map, by the interface, allows the user to get the map’s dimensions and map version, add a player (a perhaps-unnecessary concession to the demands of the XML deserialization process), get the collection of players, get the collection of tiles, and get the tile at a specified point. And by the interfaces it inherits from, it must be comparable to another instance of the interface, and an implementation must be able to tell whether another instance is or is not a “pure subset” of itself.
IMapNG is an interface that nothing uses yet. I’ve come to the conclusion that the old
IMap interface is both too tightly coupled to its implementation and subtly inefficient. For example, to find out the terrain type at a given location, with the old interface, you have to extract the Tile object “at” that location—which I now consider an implementation detail—and ask it for its terrain type. This new interface (which, as I said, nothing uses yet, and is only implemented by a fairly trivial “adapter” wrapper around an
IMap implementation) knows the dimensions and version of the map, as well as the current turn and the current player (two bits of information that in the previous paradigm distinguish a map view from a simple map), and lets the caller iterate over the players and locations in the map and query it about the terrain and other things at a given location. Unlike the interface I hope it will eventually replace, it doesn’t expose any details about how it’s implemented.
Whenever these interfaces, or anything else, talk about a location, they nearly always mean a
Point, an immutable row-column pair.
Points are produced (and cached) by the
PointFactory class, which includes a singleton instance but allows the use of separately-constructed instances (largely for testing purposes).
Both sets of interfaces require their implementers to know about the map’s dimensions and map version; these are encapsulated in a
MapDimensions class, which consists of the number of rows in the map, the number of columns in the map, and the format version it claims to be. It is immutable—I prefer most objects to be.
One of the things that a map is responsible for keeping track of is a collection of the players the map refers to. Each
Player object knows its ID number (used to refer to the player when things he or she owns are serialized to disk) and name, and is (at present) immutable. In the current implementation (i.e. the proper implementation of
IMapNG might go a different direction)
Player objects are collected in a
PlayerCollection, which allows callers to iterate over all the players in the map or get a
Player given its ID number (returning a blank
Player rather than null if there’s no such
Player in the collection already), and can tell whether another
PlayerCollection is a strict subset of it (containing no players it doesn’t).
Another interface that, like
IMapNG, I’ve put in the repository but not written an implementation for or used anywhere is
Changeset. I have several ideas that changesets would fairly neatly solve: sending players just the new information for their maps; allowing users to make changes to a map and undo and redo them; storing multiple turns’ maps in one file rather than having one copy of the map in my records for every turn that has gone by; allowing players to (if they wish) send me part of their orders in a form I can open in the map viewer, run on the main map, and export results from; and so on. But implementing the basic functionality that would make all of these features possible has proved intractable so far, so the
Changeset interface sits in this package with no references to it anywhere else.
I’ve alluded already to the
Subsettable interface, though not by name. An object that is “
Subsettable of something” must contain a method that tells whether a given instance of that something is a “strict subset” of that object—containing no members that it does not. In some cases I bend that a bit, so that players’ maps with text notations from me to the player, places where an explorer saw traces of an animal, or a cache of treasure an explorer picked up (removing it from the main map) still appear to be “subsets” of the main map. But once those few exceptions are in place, I’ve found this to be tremendously useful at times. By the way, most implementations of
Subsettable are deliberately “fail-slow,” since they take a stream to write a list of extraneous members (players, tiles, tile-fixtures, etc.) on; it’s better to run the subset-testing driver once and fix all the problems than to have to run it many times because it quits after the first difference “for efficiency.”
The last interface I should mention at this point is the
XMLWritable interface. It’s empty, merely a marker now, but every object that can be read from or written to my XML format “implements” it. The name is an artifact of the first XML I/O framework, in which each model class was responsible for producing its own XML representation.
Now, coming back to maps, and specifically to the two classes implementing the
IMap interface. The first, and original, is
SPMap (named that rather than
Map to avoid clashing with the standard interface for mappings from one kind of object to another, which I use extensively). An
SPMap knows its dimensions and has a collection of players and a collection of tiles … and that’s about it as far as its state goes. It implements the interface, and does little else.
The other implementation, which is used far more widely now, is
MapView. A map view is a view of a map; as such, it contains an
SPMap object, to which it delegates most of the work of implementing the
IMap interface. But beyond that, it knows the current turn and the current player, both of which can be changed by a caller. (The current player is actually handled by the
PlayerCollection, but this is the logical place to expose it in the API.)
The most fundamental thing about a map (until I convert to the
IMapNG interface) is that it’s a collection of tiles. The complexity of this is modeled using a
TileCollection class, which lets callers add new tiles (replacing old ones at the same location), get the tile at a specified
Point, and iterate over its contents. (That last, I should mention, is accomplished by making its iterator produce
Tiles; a Tile, as you’ll see, doesn’t know its location, while the
TileCollection can easily be queried to get the
Tile at the
Point if needed.)
And now we come to
Tiles. While, as I said above, a
Tile is really an implementation detail, in the current paradigm the class is fairly central. Each
Tile knows its terrain type (I only quite recently got rid of the code that required it to also know its location) and has a collection of
TileFixtures (more about which anon), which can be added or removed individually. Rivers are represented as a specific kind of
Tile has some extra methods for managing them, and they complicate the
removeFixture methods somewhat. The bulk of the class’s complexity (which my static analysis tools are always all but screaming about), however, comes from its implementation of
Subsettable: in addition to the
TileFixtures that aren’t supposed to be counted in subset calculations, there are some that a naive implementation would choke on because they aren’t “equal” from Java’s point of view, but the subset calculation should investigate further. (For example, if a fortress on both
Tiles has an extra unit in the purported subset
Tile, this should be reported as an extra unit in the fortress, not an extra fortress on the
I mentioned rivers. In the code, they’re implemented as an enumerated type, so any given river is either a
Lake or one of the four cardinal directions, and any of those five “directions” can be combined on any given
“Fixtures” are, from the perspective of a
Tile at least, dealt with primarily using several interfaces. The first is
IFixture, which is a super-interface for
TileFixtures and some other things (which I’ll get to later) that aren’t supposed to be on a
Tile directly. It specifies that any fixture should have an ID number (though some fudge that a little bit at the moment) and should be able to tell whether another fixture is equal except for its ID number (since normally the first thing to check in an equality function is whether the ID number is the same).
TileFixture, a fixture that a
Tile can directly contain, has to have one additional method: a method to get the fixture’s “Z-value.” This, and the specification that all
TileFixtures have to be
Comparable to one another, let the view pick one fixture to show as the “top” fixture on the tile through some rational algorithm rather than at random. This is really “model-view mixing,” but I couldn’t see any other good yet easy way to do it. Another case of “model-view mixing” that comes in with most
TileFixtures is the
HasImage interface, which requires an object to know the filename of an image (i.e. an icon) that represents it. I hope to soon rename that method to make it indicate a default image so that some fixture instances can specify a unique icon image but fall back to the “default” image for their class if it’s not found. There’s also the
HasPortrait interface, which requires an object to know the filename of a larger “portrait” image representing it, but nothing uses that interface yet.
Three more interfaces in this package that several
TileFixtures implement are
TerrainFixture, a marker interface for fixtures that alter the effective terrain of the tile (such as forests and mountains);
IEvent, for fixtures, dating back to my first attempt at adding things to explore for to the map, that have a difficulty level (“DC”) for finding them and an “exploration result text” to display when they’re found; and
HasName, marking objects that have a “kind” and a “name” respectively (mostly for use in the search UI).
Other than code testing these classes and others in sub-packages, that’s all the classes in this package directly. But there are a lot of classes under the
fixtures sub-package, which we’ll turn to next.
fixtures sub-package is itself divided up into several sub-packages, which we’ll go through in turn, but also directly contains a few classes and interfaces.
The first class is
RiverFixture. This is how we now implement tiles having rivers. It uses an
EnumSet from the Java Collections Framework to manage most of the details, but it implements the fixture API and lets its creator pass in as many River objects (or none) as he or she wishes to the constructor.
The next class is
Ground. It represents the kind of rock beneath a tile; as such, it has a “kind”, and can be exposed or not. Because every single tile has at least one, they don’t have a unique ID yet (though I should fix that).
And the last class here is
TextFixture. This is how I convey things that no other fixture can represent (and, sometimes, things that would mess up subset calculations if I used another fixture, like “there was a unit here in such-and-such turn”). It contains its text and an optional turn number (the turn this specific note was added to the map). It also doesn’t have a unique ID, but I don’t think that matters much.
Also in this package is the
UnitMember interface, which is a marker sub-interface of
IFixture that everything that can be part of a unit must “implement.”
There are five
TerrainFixtures in this package. The two obvious ones are
Forest, which knows what kind of forest it is and can optionally be “rows of trees” rather than an ordinary forest, and
Mountain, which has no state at at all. In the previous version of the map format both of those were tile terrain types, not things on a tile. Because of how common they are, no forest or mountain has a unique ID yet, which is again something I should correct one of these days.
The other kinds of
Hill, each instance of which does have a unique ID number but has no other state.
The interface which all (or perhaps nearly all) fixtures in this package share is
HarvestableFixture, a (so far merely) marker interface indicating something that can be mined, harvested, or searched to gather something (usually some resource).
We’ll start with some of the most obvious: a
Mine. It knows what kind of thing is being or was once mined there, and its status (using an enumerated type designed for towns, which I’ll describe in more detail in the next subpackage), as well as its unique ID number. Along the same lines, there are
StoneDeposit; both implement
IEvent and so have a numeric difficulty for finding them. A mineral vein knows what kind of mineral it is (an arbitrary text property nowadays, but the
MineralKind enumerated type remains for now because a few places elsewhere in the code-base need a list of possible kinds of minerals) and can be exposed or not; for now a stone deposit can only be one of a few (enumerated) kinds of stone, and is always exposed.
Next, turning from mining to harvesting: the
Meadow represents both fields and meadows. A
Meadow knows what kind of thing is being grown in it, whether it’s a field or a meadow, whether it’ cultivated or not, its status (using a custom enumerated type, for rotating between seeding, growing, bearing, and fallow), and its custom ID number. Similarly, a
Grove represents a grove or an orchard, and knows which of those it is, whether it’s under human cultivation, what kind of tree it contains, and its ID number. More simply, a
Shrub merely knows what kind of shrub it is in addition to its ID number.
CacheFixture is used for both hidden treasures and small caches of vegetables (and, I think, mushrooms). It knows what kind of cache it is and what it contains.
The last two are less obvious, since they’re here because they can be searched by explorers and archaeologists to find hidden things and artifacts of the past. The first is the
Battlefield class, which reports to the user that “There are signs of a long-ago battle here,” and the second is the
Cave class, representing “extensive caves beneath this tile.” Neither has any state (yet) beyond the numeric difficulty (as they both implement
IEvent) and an ID number.
The nearly-defining interface here is
ITownFixture has a name, a status, and a size. The status and size are both enumerated types: the
TownStatus enum allows towns to be “active”, burned-out, or ruined, while the
TownSize describes them as being small, medium-size, or large.
One class that implements this interface on its own, rather than being part of the set, is
Village. (This is because villages were added well after “events” stopped being the primary way of thinking about exploration results.) All villages are small, but each knows its status, name and ID number.
All the other implementations, though, inherit from
AbstractTown, an abstract superclass that handles the details that would otherwise be duplicated among the three subclasses. It uses yet another enum,
TownKind, to make its
toString implementation subclass-specific, and additionally knows the town’s size, status, and name. Each of the three subclasses,
Town, manages its own numeric difficulty and ID number, but leaves the details I described to the superclass.
This package also contains the important
Fortress class. Eventually I want to unify it with
Fortification, but that’s far in the future for now. A fortress knows its name and which player owns it, as well as its ID number, and contains any number of units. That list can be adjusted by adding and removing units at runtime. The
Fortress class is also
Subsettable, as I mentioned above.
And the nearly-defining interface here is
MobileFixture, a marker interface extending
TileFixture and indicating fixtures that should be (eventually) able to move from place to place in the ordinary course of events. (One player, with a philosophy of mobility, has moved a fortress before, but I had to do that by hand.) Most also implement
I won’t go into the details of most of the classes here; they’re fairly straightforward implementations, well described by their names and the list of interfaces they implement, so I’ll just list them:
Unit class, on the other hand, is more complicated, and we’ll look at it in some detail. A
Unit knows its name and “kind,” the player that owns it, and its ID number. And it can have any number of “unit members,” which must implement the
UnitMember marker interface I mentioned above.
There’s one other class here that doesn’t quite fit the mold:
SimpleMovement. It’s a data class, encapsulating knowledge about movement costs associated with various terrain types. (In a very simplistic model, of course; eventually, each unit’s movement cost should depend on that unit’s type as well as on the terrain.) I put it here for lack of any better idea; perhaps it belongs in
When I introduced the
UnitMember interface, I said that not all
TileFixtures; in fact, the most common
UnitMember is a
Worker, which is not. A
Worker knows its name and race (human, dwarf, etc.), its ID number, and its levels in various Jobs … which brings us to the next package.
There are only two classes in this sub-sub-package:
Job object represents a worker’s experience level in a Job, which is sort of like a class in a standard tabletop RPG; a
Worker object can contain any number of
Job objects. The
Job object knows the name of the Job and how many levels the worker has in it. It also contains any number of
Skill class represents experience and training more granularly. Each instance contains the name of the skill, the worker’s level in the skill, and how many hours of experience or training the worker has undergone since last gaining a level in the skill. There’s also a method to add hours of experience, which in addition to the number of hours takes a “condition” parameter. If that parameter, which is usually a randomly generated number between 0 and 100, is less than the number of hours the worker has accumulated in the skill since last “leveling” the skill, the worker gains a level in the skill and the number of hours resets to zero.
And now we turn away from the
model.map package hierarchy, to look at the driver models underlying the drivers you’ll most likely deal with.
The map viewer is the flagship application of this suite; this package contains the model apparatus on which it is built. We’ll start with the “driver model.”
IViewerModel interface specifies the API on which the map viewer runs. A viewer-model has a “selected tile” (or, rather, as of quite recently a “selected point”), which can be set by a method; it allows a caller to get a tile from the map given its location; it has a notion of the visible dimensions of the map, which can be changed by a caller (and more about that in a bit); and it has a “zoom level,” which can be queried, increased, decreased, and reset to the default. The
ViewerModel class is, as you might well expect, a fairly close implementation of this interface; its only method not specified by the interface is one to clear the selected point, resetting it to (-1, -1). It also fires
PropertyChangeEvents when the visible dimensions change, when the zoom level changes, and when the selected point changes (and in the latter case, two such events, one for the “point” property and one for the “tile” property).
VisibleDimensions class is used to represent what subset of the map is actually visible to the user at the moment—both how many rows and columns are visible, and which ones, are essential to know for determining which tile a mouse-click is selecting, for example. So a
VisibleDimensions object is an immutable encapsulation of the minimum and maximum row and column at any given time; new instances are created and substituted in as needed.
TileViewSize class is used by the view to determine how big each tile should be drawn; this used to be a constant integer determined by the map version (version-2 map tiles need to be drawn bigger than version-1 tiles), but now takes the current zoom level and the current map version as parameters and returns a properly scaled result.
The map viewer uses instances
PointIterator class in the search feature. It produces every
Point that the map could contain in order (either across-then-down or down-then-across, and either forwards or backwards), optionally starting from the currently selected tile. There’s also a fairly extensive set of unit tests of the
PointIterator, since I was not at all certain my implementation was correct.
FixtureComparator class is used to sort fixtures on a tile so that “the most important” one is drawn “on top.”
The remaining classes in this package have to do with the “tile details panel” (which we’ll get to in the next post in this series, next month), which, among other things, lets users see a list of the fixtures on the selected tile and drag them to the list of fixtures in another tile in another map window. The
FixtureListModel is a list-model backing the visible list, updating it when a new tile is selected. The
FixtureListDropListener handles drag-and-drop related events, trying to prevent fixtures from being dropped on the list they were dragged from. Two classes,
CurriedFixtureTransferable, wrap a single
TileFixture or a list of
TileFixtures (respectively) in the form that Java’s drag-and-drop API requires. And the
TileTypeFixture class, an implementation of
TileFixture, allows the list of fixtures to additionally show the tile’s terrain type, which also allows the user to copy that using drag and drop.
This is the last package in the model, and it’s fairly recent, since I only started the worker-management UI in the last couple of months.
The only method the
IWorkerModel interface adds to the
IDriverModel interface is one to get all units in the map belonging to a player, and the
WorkerModel class doesn’t add any either. Most of the logic of the worker-advancement application is handled in the GUI, or in its components’ model objects, which I’ll get to next.
Beyond that driver-model object, this package also contains list models for each of the major lists in the advancement GUI. The
UnitListModel updates itself whenever the user selects a new player by asking the
WorkerModel object for that player’s units; the
UnitMemberListModel lists the members in the currently-selected unit and updates itself whenever a new one is chosen; the
JobsListModel updates itself whenever a new member of the current unit is selected, clearing itself if the selected member isn’t a
Worker and showing the worker’s
Jobs if it is; and the
SkillListModel similarly maintains the list of
Skills in the currently-selected
So there you have it. Next month we’ll wrap this series up with a tour of the view, explaining how the parts you see when you run the program are put together, and how they work.