This is the third installment of our Angry Rationals game. I hope that even if you are not particularly interested in game programming this exercise helps you understand some of the more advanced things one can accomplish using Mathematica dynamic feature plus graphical abilities.
In part 1 I defined the game structure and in part 2 I added the first level but at this point the game is still very amateurish. Today’s goals are to:
- polish the look and feel of the game by adding more villans, backgrounds, and sound.
- introduce a new level with more difficulty and more coding complexity
- consider how the added complexity may call for different coding techniques
Let’s start with the background image for the game. Working with images in notebooks is easy because you can just copy and paste the image and assign to a variable (Fig 1).
The next step is getting the background into the game. This can be done using Inset. The inset image should be behind all graphics so we place it in the Prolog option of Graphics which cause it to be rendered before any other graphics object. Fig 2 shows how to do this. You should read the documentation for Inset for the details but basically you map a point on the image to a point on the graphic and specify a size for the resulting image.
For sound, Mathematica provides a primitives Sound, SoundNote, and EmitSound. Again, I am not going to spend a lot of time explaining these now. There is a full chapter on Music and Sound in Mathematica Cookbook written by a domain expert, John Kiehl, but the Mathematica documentation is extensive as well. As a test I created a CDF that emits a sound when two objects collide.
Manipulate[
If[EuclideanDistance[p1, {0, 0}] < 0.1, Quiet@EmitSound[Sound[SoundNote["Snare"]]]]; Graphics[ { Text[Style["x", 28], p1], Text[Style["y", 28], {0.0, 0.0}] }, PlotRange -> {{-1, 1}, {-1, 1}}
]
,
{p1, {-1, -1}, {1, 1}},
SaveDefinitions -> True
]
NOTE: The juxtaposition of Quiet and EmitSound above may look discordant but they do different things. Quiet suppresses any messages that may be generated by EmitSound.
Now that we have some of the preliminaries out of the way we can get back to the game. So far our game has been simple but even it is simplicity there is quite a few variables and logic for managing the game state. As we introduce more game elements it is clear this can quickly get out of hand so we need to take a step back and think like a software engineer.
The two techniques that tend to result in elegant Mathematica programs are Functional and Rule-Based programming. I will use both. My first goal is to encapsulate the state of the game’s villans (i.e. the irrationals). The state of a villan partly static (e.g. its name stays constant), partly dynamic in relation to the current state( e.g. a sound should be emitted just at the point of collision) and partly a function of prior states (e.g. once a villan is struck it should remain in the struck state). This implies a villan can be represented by a function that has memory of the past. Such a function is called a closure. But what should the function return? How about a set of rules that specify the present state of the villan. Let me describe what I mean step by step.
I want the present state of a villan to be a set of rules. For example:
villan1 =
{
name->"π",
hit -> False,
fontw -> Bold,
color -> Green,
size -> 32,
pos -> {0,1},
transform -> Identity,
sound -> Sound[SoundNote[None]]]
}
This states that currently villan1 has name π, has not be hit, is displaying as Bold and Green at a particular position and is neither transformed (e.g. not Rotated) nor emitting a sound. But this is the instantaneous representation of the villan. What we really want is to represent the villan as a function of the games state and history. The way to do that is to make the villan into a function (henceforth called the villan function).
villan1 = Function[{projPos},
{
name->"pi",
hit -> collision[{0,1},projPos],
fontw -> Bold,
color -> If[collision[{0,1},projPos],Pink,Green],
size -> 32,
pos -> {0,1},
transform -> If[collision[{0,1},projPos],
Rotate[#1,30 Degrees]&,Identity]
sound -> If[collision[{0,1},projPos],
Sound[SoundNote["Snare"]],
Sound[SoundNote[None]]]
}
This now says that the rules for hit, color and transform and sound are a function of the projectiles position (i.e. a function of whether this particular villan is involved in a collision with the rational projectile).
But this is still not correct. Because, some of the rules are not simply a function of the projectile being hit but rather a function of ever being hit. That is to say, we need memory of the past. Now we can store this memory of the past externally and pass it in as another function argument but that complicates matters because now we need to maintain a bit of extra state for each villan separate from the villan. What we want is the villan itself to keep its own state. What we want is a closure. Now Mathematica does not explicitly provide closures but it is fairly easy to emulate them by taking advantage of the feature of Module that guarantees variables scoped within the Module are unique. We associate the module’s variables with the function by using a villan constructor (makeVillan). In addition, we use Options on the villan constructor to capture the default values. Keep in mind that makeVillan is distinct from the villan function (its return value).
Options[makeVillan] = {fontw -> Bold, color -> Green, size -> IrrSZ};
makeVillan[aName_, aPos_, OptionsPattern[]] :=
Module[{c1 = False},
Function[{projPos, reset},
With[{c2 = collision[aPos, projPos]},
c1 = c1 || c2;
c1 = If[reset, False, c1];
{
hit -> c1,
name -> nm,
fontw -> OptionValue[fontw],
color -> If[c1, Pink, OptionValue[color]],
size -> OptionValue[size],
pos -> aPos,
transform -> If[c1,
Rotate[#1, 30 Degree] &,
Identity],
sound -> If[c2,
Sound[SoundNote["Snare"]]],
Sound[SoundNote[None]]]
}
]
]
]
This is relatively little code but it concentrates many Mathematica concepts so let’s review it line by line.
Options[makeVillan] = {fontw -> Bold, color -> Green, size -> IrrSZ};
This establish the defaults for a villan constructor. It says villans should be Bold and Green and have a size IrrSZ (which is a constant defined elsewhere.
makeVillan[aName_, aPos_, OptionsPattern[]] :=
This says that to make a villan we provide a name and its position as well as additional options which can override the defaults.
Module[{c1 = False},
This says the makeVillan rule has a lexically scoped variable c1 whose initial value is False. This in itself is unremarkable except in relation to what comes next. For now, I'll say that c1 means "has this villan ever been involved in a collision".
Function[{projPos, reset},
The value produced by makeVillan is a Function (i.e. we are returning a Function) and that function takes a projPos and another argument called reset (more on that later).
With[{c2 = collision[aPos, projPos]},
Here we use With inside of the function to define a constant c2 which is a Boolean that states “is the villan currently hit”.
c1 = c1 || c2;
c1 = If[reset, False, c1];
These lines say, that it is true that the villan has been hit if it was hit before or is currently hit except if we are reseting its previous state of being hit. Being able to reset the state is important to starting a level over.
{
hit -> c1,
name -> nm,
fontw -> OptionValue[fontw],
color -> If[c1, Pink, OptionValue[color]],
size -> OptionValue[size],
pos -> aPos,
transform -> If[c1, Rotate[#1, 30 Degree] &, Identity],
sound -> If[c2, Sound[SoundNote["Snare"]]],Sound[SoundNote[None]]]
}
These lines provide the return value of the villan function and as we alluded before, the value is a set of rules. But note that these rules reference the Module variable c1. But c1‘s scope is not active during the invocation of the villan function; it is active only during the invocation of the constructor (makeVillan) hence we can see a bit of hanky-panky is going on here. What I am doing is stealing c1 and relying on the fact that internally Mathematica has replaced c1 with a name that is unique (see documentation for Module if this is unclear). Hence, our villan function acts as a closure over some unique global variable that has taken c1‘s place (again, this is all under the covers). Since Mathematica guarantees the Kernel will have no other such variable, we are safe. In essence, we dynamically allocated a bit of storage for a villan function to remember the past. You should note that the rule for sound use c2 rather than c1 because a sound is only a function of the instaneous state of a collision not the past state.
I have introduced a lot of concepts but the payoff, as you will see, is that our next game level will be almost trivial to implement. In this level we will have 3 villans positioned along points of a Sine wave. The player will need to set the amplitude and frequency of the wave such that the rational projectile (3/4) collides will all 3 villans.
Below is the code for the new level. This includes the code that we explained above and the Manipulate for the actual game play. Note how the Manipulate is now devoid of game state management and control logic except for replacement rules and the controls. Most of the code in the Manipulate is simple option settings for the Graphics and the Manipulate itself.
With[{COLLIDETHRS = 0.25, IrrSZ = 48},
collision =
Function[{p1, p2}, EuclideanDistance[p1, p2] < COLLIDETHRS];
villanCls = transform@Text[Style[name, fontw, color, size], pos];
Options[makeVillan] = {fontw -> Bold, color -> Green,
size -> IrrSZ};
makeVillan[nm_, p_, OptionsPattern[]] :=
Module[{c1 = False},
Function[{projPos, reset},
With[{c2 = collision[p, projPos]},
c1 = c1 || c2;
c1 = If[reset, False, c1];
{
hit -> c1,
name -> nm,
fontw -> OptionValue[fontw],
color -> If[c1, Pink, OptionValue[color]],
size -> OptionValue[size],
pos -> p,
transform -> If[c1,
Rotate[#1, 30 Degree] &,
Identity],
sound -> If[c2,
Quiet@EmitSound[Sound[SoundNote["Snare"]]]]
}
]
]
]
]
With[{PiX1 = 2, PiY1 = 1.5, PiX2 = 10, PiY2 = 1.5, PiSZ = 28, EX1 = 6,
EY1 = -1.5},
villans = {makeVillan["\[Pi]", {PiX1, PiY1}],
makeVillan["\[ExponentialE]", {EX1, EY1}],
villan3 = makeVillan["\[Pi]", {PiX2, PiY2}] };
]
With[{TSTART = 0.2, TEND = 12.0},
Manipulate[
DynamicModule[{villans, pos, reset, path},
reset = t <= TSTART;
pos = {t, f[t, p1, p2]};
villans = {villan1[pos, reset], villan2[pos, reset],
villan3[pos, reset]};
sound /. villans;
path = Table[{t, f[t, p1, p2]}, {t, TSTART, t, 0.1}];
Graphics[
{
Line[path],
Text[Style[3/4, Bold, Red, 24], pos],
villanCls /. villans
},
Axes -> True,
PlotRange -> {{0, 11}, {-3.5, 3.5}},
PlotRangePadding -> 0.35,
ImagePadding -> {{0, 0}, {0, 0}},
Prolog -> Inset[background, Center, Center, 13.0]]]
,
{t, TSTART, TEND, Trigger},
{p1, 0.0, 2.0},
{p2, -1, 1},
FrameLabel -> Style["p1 Sin[p2 t]", 18],
TrackedSymbols :> {t}
]
]
Here is what the level looks like (this CDF has embedding issues. I am told this will improve in the next release of Mathematica)
Conclusion
This installment covered a lot of ground but I hope gave you some food for thought. It is possible to create Manipulates that encompass a lot of logic without drowning in the logic as long as you think carefully how to encapsulate the logic by leveraging the best features of Mathematica. The techniques covered here are also discussed extensively in my book (plus a lot more!).
This will be my last post on Angry Rationals for a while but I plan on continuing to work on the game as I have time and I will provide the final notebook for you to download to play and tweak when it is ready.


