Rusty days
As I mentioned in the previous post, the addiction to board games I have been struggling with during these last months had collided with my years long interest, and sometimes obsession, with programming. That was due to me having found mechanically complicated and generally engrossing strategy board games, and that some users in the BGG forums had created software to run those board games as well as their opponents. Those companion apps just tell you which pieces of the opponent to move, but otherwise let the tactile experience of playing those board games intact, with the difference that now you just have to focus on your own actions. That's very important in games such as those that might take five minutes to plan out your turn, and you don't want the experience to be bogged down by following the simulated opponent's logic. I have more than enough trying to follow my own logic.
The COIN series of games from publisher GMT are maybe the most mechanically complicated and involved board games in the market. Famously designed, originally at least, by an ex-CIA analyst, they feature, usually, four asymmetric factions trying to get ahead in a web of complicated alliances and enmities set during a historical quagmire. For example in their most simple game, "Cuba Libre", that simulates the communist revolution and takeover in Cuba, has you playing as either the government trying to resist the communist horde, or the communists led by Castro, or a student group trying to oppose both the government and the communists, or the American mafia trying to stay in favor of whoever could win, so they can remain in business. Each has its own victory conditions, and at times you might favor a faction over the remainder for purely tactical reasons, just to betray them later. Each turn you draw a card from the "event" deck, which acts as a propagandistic maneuver that can play in your favor if you push for it, but whether to exploit that historical moment or pass (because you can't play the following card, which you can always see, if you play the current one) is where a significant amount of its strategy lies.

Pictured: some of "Fire in the Lake"'s cards.
In addition you can also do many different actions for each faction, such as rallying support, marching through the map, attacking some positions, going underground, recruiting, etc. As I play board games almost exclusively by myself (I don't like being around people, to put it mildly), I would have to command one of those factions and then follow the very complicated flowcharts of each of those simulated opponents in order for them to act. These games are marvelously designed and tremendously engrossing, but having to check each condition of the flowchart and then execute their chosen actions feels too much like work, and takes you out of the fun and involvement of just dealing with your own problems and opportunities. That has a solution, albeit a complicated, time-consuming one: just program your own command line software that can run their logic.
Curt Sellmer did wonders with his software adaptation of Labyrinth (with both its expansions) as well as Colonial Twilight (that simulates the Algerian revolt against the French). In my case, I've never been as interested by a programming language as I've been with Rust since I found out about it; Rust is a blazing fast, memory safe and reliable programming language the likes of which, it seems, had never existed. Amongst programmers it was known that if you wanted the kind of speed that you really need for programming complicated simulations or artificial intelligence stuff (including games), you needed to learn C++, an old and convoluted dinosaur that despite its closeness to machine language, it doesn't save you from inadvertently creating memory holes that will end up popping up as almost unsolvable bugs. Microsoft mentioned how they were moving significant parts of their codebase to Rust, because 70% of the software patches they send out have to do with memory leaks or issues with memory management in general. Rust stops all that; you don't have to allocate the memory yourself, but there's no need, because you do need to establish ownership and lifetimes for all those variables and structures for which the compiler cannot find the obvious declaration (it can deduce most lifetimes, but those it can't more often than not indicate a design problem).
This last few weeks I've been programming like a madman to translate GMT's "Fire in the Lake", about the Vietnam war, into a Rust program. The code is openly available at its GitHub page. I've already cleared most hurdles related to how you need to change your programming mindset regarding Rust's safety model. Some of my habits were related to programming in a garbage collecting language, and they are more often than not impracticable in Rust's environment. For example:
-The biggest hole I fell in had to do, of all things, with a structure that held the parameters, many, that had to be sent to the struct that handles and delegates the decision making of all the factions. It had to be sent the number of the card in play, the faction that had to decide, the full map, all the tracked variables (such as resources, victory markers, etc.), the available forces of all the factions, etc. In any other program you would gather all those parameters into its own class, something like DecideParameters (for the "decide" function), but Rust has a problem with that: you can only ever hold on to a particular mutable reference once. To pass it to some other structure, the structure that held on to that reference has to drop it somehow. The parameter class required plenty of mutable references in its constructor, and it would be doling them out to whoever asked for them. Even though you could program that safely by just making the "decide" function take those references and work with them, you can't really prove that those references held and distributed by the parameter gathering structure wouldn't end up holding a reference to something that had passed away. You can't prove it either in a garbage collected language. The fact that we were doing this is an example of the recklessness that other languages allow; Rust isn't being fastidious by disallowing you to do that: it's just making sure your program can't create those bugs. That means, however, that you are more likely to move on gradually towards a functional style of programming: create structs just when there's some field that really needs to be tracked, and work with functions otherwise. Rust's memory model has no issue with you passing mutable references through functions, as long as they don't stay there somehow, and they can't stay in functions. This limitation does mean, though, that you will end up with parameter lists of potentially 7-10 entries. For example, the only point of entry to the decision making process, the function "decide", looks like this:
fn decide(&self, active_card: u8, current_eligible: Factions, map: &Map, track: &Track, _available_forces: &AvailableForces) -> Decision
-My biggest previous issue with Rust when I looked into it back in 2018 was that you couldn't create collections or in general "group" different structures, even if they implemented the same trait (Rust's more complicated version of other language's interfaces). It's one of the main principles of code quality that you need to program to interfaces instead of to implementations to avoid code coupling, and particularly to isolate volatile and non-deterministic subsystems. That Rust would complain if you were to create a collection of spaces, for example, in which the space in question could be a interface to an implementation of a province, of a city or of a line of communication (as is the case in "Fire in the Lake"), was a tremendous problem without a reliable solution; there's an unsafe way that Rust sort of allows, by creating a dynamic link, but that is 10x slower, handling those references is messy and annoying, and it defeats the purpose of programming in Rust to begin with. However, the community came to the rescue, and there's a fantastic library called enum_dispatch that uses the richer Rust enums to allow a sort of "type identification and grouping" as well as keeping the implemented traits working. It looks like this:
extern crate enum_dispatch;
use self::enum_dispatch::enum_dispatch;
#[enum_dispatch]
pub trait Space {
(...)
}
#[enum_dispatch(Space)]
#[derive(Debug)]
pub enum Spaces {
City,
Province,
LineOfCommunication,
}
When instantiating the structs you just have to call the "into" method in order to "transform" them into the enum-like form, but that's all. Otherwise they work as you would expect in any other language.
-In general I've had trouble avoiding some redundancy or close repetition that in other languages you would solve through duck typing or generics. Generics in Rust is a prickly area that I won't dare touch until I'm comfortable enough with the integration tests to make sure there are no regressions.
-Amongst the biggest priorities when attempting to codify a system is figuring out how to isolate the volatile, non-deterministic elements. This game has random input from the dice throws, but much worse is that you have players that are bots and are controlled by complicated flowcharts, but those factions can also be controlled by a human being. At first I considered creating two "paths" through the main system depending on whether a player was a human entering commands with the keyboard or it was a bot, but I found a more elegant solution: to the system, all players behave like humans. The bot's decisions and actions are translated to typed-like commands such as "event", "rally", "operation", or the numerous names of spaces, or the amount of forces it wants to move around. The system that processes those commands and executes them just carries with it a list of commands in an order it can understand, and it doesn't know, nor has any need to know, whether those commands were produced by a human being or a robot. So far I have proved three turns of the playbook that comes with the game, and that's a big deal given how complicated these games are and how many different things the factions can do. For example, this is the code that tests the second turn. I just input the player dummies for the test in the same slots as I would input a regular bot or a human being, and the system doesn't care. Awkwardly, though, I had to store the definitions of the test doubles in the main crate, because num_dispatch doesn't allow the implementations of a trait to be in an extern crate. It just bothers me because of general code quality reasons.
-I'm reading through Vladimir Khorikov's "Unit Testing: Principles, Practices, and Patterns", which seems to be a modern bible on test driven design. I've always been a fanatic of testing first, but I realized as well that while you do need to create a test first, you shouldn't be creating dumb stuff such as whether you can instantiate a struct or access its members: just incorporate whatever struct you create as part of an integration test that proves a system necessity. Through proving the first three turns of the plabook I created many structures and functions, and their behaviors are locked in place, for the most part, through participating in those tests.
I have those familiar itches that make me want to open Visual Studio Code and keep coding as soon as I get home. These obsessions are my particular version of a venereal disease. I hope they last until this small project ends, which shouldn't be that far away.
EDIT: All this talk of testing a program reminds me of one of the biggest legends of gaming, and also the most complicated game ever created, "Dwarf Fortress", that has been going strong for fourteen years and that is gearing up for a Steam release with an updated interface and graphics. Created by a mathematician, he learned programming through writing the game in its initial states, and he readily admitted that his code was probably disastrously structured. I always wished to take a peek at that mountain of code, although it also terrifies me. That game kept piling up features without being able to solve huge bugs that it had introduced years ago, and likely because they cannot be solved at this stage. I doubt their codebase includes any test, so the only safeguard against regressions is whether or not the code compiles when you make a change. That makes me shudder. Still, I wish they would use the Steam money, a significant part of it at least, to just get a bunch of systems analists and coders to rebuild the code from scratch. It would be a thing of beauty.
The COIN series of games from publisher GMT are maybe the most mechanically complicated and involved board games in the market. Famously designed, originally at least, by an ex-CIA analyst, they feature, usually, four asymmetric factions trying to get ahead in a web of complicated alliances and enmities set during a historical quagmire. For example in their most simple game, "Cuba Libre", that simulates the communist revolution and takeover in Cuba, has you playing as either the government trying to resist the communist horde, or the communists led by Castro, or a student group trying to oppose both the government and the communists, or the American mafia trying to stay in favor of whoever could win, so they can remain in business. Each has its own victory conditions, and at times you might favor a faction over the remainder for purely tactical reasons, just to betray them later. Each turn you draw a card from the "event" deck, which acts as a propagandistic maneuver that can play in your favor if you push for it, but whether to exploit that historical moment or pass (because you can't play the following card, which you can always see, if you play the current one) is where a significant amount of its strategy lies.

Pictured: some of "Fire in the Lake"'s cards.
In addition you can also do many different actions for each faction, such as rallying support, marching through the map, attacking some positions, going underground, recruiting, etc. As I play board games almost exclusively by myself (I don't like being around people, to put it mildly), I would have to command one of those factions and then follow the very complicated flowcharts of each of those simulated opponents in order for them to act. These games are marvelously designed and tremendously engrossing, but having to check each condition of the flowchart and then execute their chosen actions feels too much like work, and takes you out of the fun and involvement of just dealing with your own problems and opportunities. That has a solution, albeit a complicated, time-consuming one: just program your own command line software that can run their logic.
Curt Sellmer did wonders with his software adaptation of Labyrinth (with both its expansions) as well as Colonial Twilight (that simulates the Algerian revolt against the French). In my case, I've never been as interested by a programming language as I've been with Rust since I found out about it; Rust is a blazing fast, memory safe and reliable programming language the likes of which, it seems, had never existed. Amongst programmers it was known that if you wanted the kind of speed that you really need for programming complicated simulations or artificial intelligence stuff (including games), you needed to learn C++, an old and convoluted dinosaur that despite its closeness to machine language, it doesn't save you from inadvertently creating memory holes that will end up popping up as almost unsolvable bugs. Microsoft mentioned how they were moving significant parts of their codebase to Rust, because 70% of the software patches they send out have to do with memory leaks or issues with memory management in general. Rust stops all that; you don't have to allocate the memory yourself, but there's no need, because you do need to establish ownership and lifetimes for all those variables and structures for which the compiler cannot find the obvious declaration (it can deduce most lifetimes, but those it can't more often than not indicate a design problem).
This last few weeks I've been programming like a madman to translate GMT's "Fire in the Lake", about the Vietnam war, into a Rust program. The code is openly available at its GitHub page. I've already cleared most hurdles related to how you need to change your programming mindset regarding Rust's safety model. Some of my habits were related to programming in a garbage collecting language, and they are more often than not impracticable in Rust's environment. For example:
-The biggest hole I fell in had to do, of all things, with a structure that held the parameters, many, that had to be sent to the struct that handles and delegates the decision making of all the factions. It had to be sent the number of the card in play, the faction that had to decide, the full map, all the tracked variables (such as resources, victory markers, etc.), the available forces of all the factions, etc. In any other program you would gather all those parameters into its own class, something like DecideParameters (for the "decide" function), but Rust has a problem with that: you can only ever hold on to a particular mutable reference once. To pass it to some other structure, the structure that held on to that reference has to drop it somehow. The parameter class required plenty of mutable references in its constructor, and it would be doling them out to whoever asked for them. Even though you could program that safely by just making the "decide" function take those references and work with them, you can't really prove that those references held and distributed by the parameter gathering structure wouldn't end up holding a reference to something that had passed away. You can't prove it either in a garbage collected language. The fact that we were doing this is an example of the recklessness that other languages allow; Rust isn't being fastidious by disallowing you to do that: it's just making sure your program can't create those bugs. That means, however, that you are more likely to move on gradually towards a functional style of programming: create structs just when there's some field that really needs to be tracked, and work with functions otherwise. Rust's memory model has no issue with you passing mutable references through functions, as long as they don't stay there somehow, and they can't stay in functions. This limitation does mean, though, that you will end up with parameter lists of potentially 7-10 entries. For example, the only point of entry to the decision making process, the function "decide", looks like this:
fn decide(&self, active_card: u8, current_eligible: Factions, map: &Map, track: &Track, _available_forces: &AvailableForces) -> Decision
-My biggest previous issue with Rust when I looked into it back in 2018 was that you couldn't create collections or in general "group" different structures, even if they implemented the same trait (Rust's more complicated version of other language's interfaces). It's one of the main principles of code quality that you need to program to interfaces instead of to implementations to avoid code coupling, and particularly to isolate volatile and non-deterministic subsystems. That Rust would complain if you were to create a collection of spaces, for example, in which the space in question could be a interface to an implementation of a province, of a city or of a line of communication (as is the case in "Fire in the Lake"), was a tremendous problem without a reliable solution; there's an unsafe way that Rust sort of allows, by creating a dynamic link, but that is 10x slower, handling those references is messy and annoying, and it defeats the purpose of programming in Rust to begin with. However, the community came to the rescue, and there's a fantastic library called enum_dispatch that uses the richer Rust enums to allow a sort of "type identification and grouping" as well as keeping the implemented traits working. It looks like this:
extern crate enum_dispatch;
use self::enum_dispatch::enum_dispatch;
#[enum_dispatch]
pub trait Space {
(...)
}
#[enum_dispatch(Space)]
#[derive(Debug)]
pub enum Spaces {
City,
Province,
LineOfCommunication,
}
When instantiating the structs you just have to call the "into" method in order to "transform" them into the enum-like form, but that's all. Otherwise they work as you would expect in any other language.
-In general I've had trouble avoiding some redundancy or close repetition that in other languages you would solve through duck typing or generics. Generics in Rust is a prickly area that I won't dare touch until I'm comfortable enough with the integration tests to make sure there are no regressions.
-Amongst the biggest priorities when attempting to codify a system is figuring out how to isolate the volatile, non-deterministic elements. This game has random input from the dice throws, but much worse is that you have players that are bots and are controlled by complicated flowcharts, but those factions can also be controlled by a human being. At first I considered creating two "paths" through the main system depending on whether a player was a human entering commands with the keyboard or it was a bot, but I found a more elegant solution: to the system, all players behave like humans. The bot's decisions and actions are translated to typed-like commands such as "event", "rally", "operation", or the numerous names of spaces, or the amount of forces it wants to move around. The system that processes those commands and executes them just carries with it a list of commands in an order it can understand, and it doesn't know, nor has any need to know, whether those commands were produced by a human being or a robot. So far I have proved three turns of the playbook that comes with the game, and that's a big deal given how complicated these games are and how many different things the factions can do. For example, this is the code that tests the second turn. I just input the player dummies for the test in the same slots as I would input a regular bot or a human being, and the system doesn't care. Awkwardly, though, I had to store the definitions of the test doubles in the main crate, because num_dispatch doesn't allow the implementations of a trait to be in an extern crate. It just bothers me because of general code quality reasons.
-I'm reading through Vladimir Khorikov's "Unit Testing: Principles, Practices, and Patterns", which seems to be a modern bible on test driven design. I've always been a fanatic of testing first, but I realized as well that while you do need to create a test first, you shouldn't be creating dumb stuff such as whether you can instantiate a struct or access its members: just incorporate whatever struct you create as part of an integration test that proves a system necessity. Through proving the first three turns of the plabook I created many structures and functions, and their behaviors are locked in place, for the most part, through participating in those tests.
I have those familiar itches that make me want to open Visual Studio Code and keep coding as soon as I get home. These obsessions are my particular version of a venereal disease. I hope they last until this small project ends, which shouldn't be that far away.
EDIT: All this talk of testing a program reminds me of one of the biggest legends of gaming, and also the most complicated game ever created, "Dwarf Fortress", that has been going strong for fourteen years and that is gearing up for a Steam release with an updated interface and graphics. Created by a mathematician, he learned programming through writing the game in its initial states, and he readily admitted that his code was probably disastrously structured. I always wished to take a peek at that mountain of code, although it also terrifies me. That game kept piling up features without being able to solve huge bugs that it had introduced years ago, and likely because they cannot be solved at this stage. I doubt their codebase includes any test, so the only safeguard against regressions is whether or not the code compiles when you make a change. That makes me shudder. Still, I wish they would use the Steam money, a significant part of it at least, to just get a bunch of systems analists and coders to rebuild the code from scratch. It would be a thing of beauty.
Published on August 10, 2020 02:22
•
Tags:
board-games, code-quality, programming, rust
No comments have been added yet.