Core Game Logic ↩
When I worked on the breakout game, it was nice to keep some of the core logic in a pure Java package of code. As noted by Tim Cain, keeping engines and games seperate is a nice thing; it lets you unit test easily, forces a clear seperation of responsibility, and if we were targetting multiple platforms, potentially makes ports easier. I'm only targetting Macs, Windows, and Linux since LibGDX does that hard work for me. But slot machines sound straightforward to implement, so I think we can bang out the core logic in one section before moving into the UI stuff. 1
I considered switching things up 2, but in the end, decided that I'd just use LibGDX and Java for this game. So, as per usual, I booted up GDX liftoff and created a project.
While going through the dialog and selecting the options, I decided that rather than using freetype for loading fonts, I'd just make everything sprites this time. While this is generally not a good idea, if I don't use the freetype extension, I technically can target the HTML output for LibGDX, and I'm curious about how well that works. So, we'll see if by the end of this I get another game on the website or not.
The main thought I had while thinking "a slot machine should be fun to make" was about easing functions to make the reels of the machine spin and slow down as they land on disappointment or dopamine. However, upon further thought, I realized that the tricky part of this is going to be the actual win rate! So, before putting any figurative pen to paper, I popped over to the wikipedia page on slot machines to see if there was any information about that sort of thing.
Slot machines are typically programmed to pay out as winnings 0% to 99% of the money that is wagered by players. This is known as the "theoretical payout percentage" or RTP, "return to player". The minimum theoretical payout percentage varies among jurisdictions and is typically established by law or regulation. For example, the minimum payout in Nevada is 75%, in New Jersey 83%, and in Mississippi 80%. The winning patterns on slot machines – the amounts they pay and the frequencies of those payouts – are carefully selected to yield a certain fraction of the money paid to the "house" (the operator of the slot machine) while returning the rest to the players during play.
This makes sense, the house has to make money back and amortizing it over all players so that a large chunk are happy and making money, while still having a sort of 'tax' from losers to ensure profits are made is just smart business. Mathy business. Anyway, so how does the casino or machine manufacturers make sure they hit that payout?
The table of probabilities for a specific machine is called the Probability and Accounting Report or PAR sheet, also PARS commonly understood as Paytable and Reel Strips. Mathematician Michael Shackleford revealed the PARS for one commercial slot machine, an original International Gaming Technology Red White and Blue machine. … There are 13 possible payouts ranging from 1:1 to 2,400:1. The 1:1 payout comes every 8 plays. The 5:1 payout comes every 33 plays, whereas the 2:1 payout comes every 600 plays. Most players assume the likelihood increases proportionate to the payout.
The one mid-size payout that is designed to give the player a thrill is the 80:1 payout. It is programmed to occur an average of once every 219 plays. The 80:1 payout is high enough to create excitement, but not high enough that it makes it likely that the player will take their winnings and abandon the game. More than likely the player began the game with at least 80 times his bet (for instance there are 80 quarters in $20).
In contrast the 150:1 payout occurs only on average of once every 6,241 plays. The highest payout of 2,400:1 occurs only on average of once every 643 = 262,144 plays since the machine has 64 virtual stops. The player who continues to feed the machine is likely to have several mid-size payouts, but unlikely to have a large payout
It's interesting to read about how the various probabilities and payouts come together to help screw over a player while still being "fair" in the larger sense of things. The marriage of psychology and math is a dangerous one it seems. But more importantly, we can infer from this that we could do something like a big look up table for winning combinations and select them at the desired rate, or we could try to find one of these PAR sheets online and attempt to understand how to potentially implement it.
A quick search for the aforementioned Red, White, and Blue PAR sheet landed me on this website. And man, that makes everything crystalize really nicely! We've got 3 reels, 64 numbers for each, they map to a table of results, and then we check a payout table with the results and tada, money!
This ALSO means that it should be VERY easy to test things work as expected because the core of our game mechanics is actually just two look up tables. My guess is that this is going to just be a good chunk of data entry upfront, and then we'll spend most of our time ensuring that the UI components end up spinning and landing on the right symbol.
As far as symbols go, the website with the useful tables doesn't actually have a picture of what one of these machines looks like. And so, I didn't quite understand what it meant by "blank" in the table until I looked here and saw this picture of the machine:
If you observe the bottom, underneath the part that says "this machine accepts...", there's a [BLANK] symbol along the bottom. So, that answers that. I don't know about you, but when I read a "blank" in the table I was imagining just an empty spot in the reel and thought that that would look kind of weird. Anyway, what is odd though is that the picture of the above machine has a "wild" symbol, and the lookup tables on the other site don't mention this at all, but I suppose they don't HAVE to mention it, if "any red" just means a red seven, the red bar or a wild symbol then that's that I guess.
It does raise the question of how we handle that in code though. And I think the easiest thing to do is to just ignore wilds entirely. I can see the calculations and how things would work with the lookup table, and it'd be fun to make our own, but I want a clear simple paved path for our first foray into this to avoid any unneccesary implementation pain. So, for the first go at this, I'm going to use the look up tables defined by the wizard of odds as is.
Getting our code base started, I'm going to define the 7 possible symbol names in a look up table. You might ask why the extra layer of indirection here, but trust me, it will all make sense later, promise. For now, let's define our mapping in plain simple Java:
public class SymbolNameMap {
private final String blank;
private final String oneBar;
private final String twoBar;
private final String threeBar;
private final String blueSeven;
private final String whiteSeven;
private final String redSeven;
public SymbolNameMap(
String blank,
String one_bar, // Don't mind me, rust conventions are nice sometimes
String two_bar,
String three_bar,
String blue_seven,
String white_seven,
String red_seven
) {
this.blank = blank;
this.oneBar = one_bar;
this.twoBar = two_bar;
this.threeBar = three_bar;
this.blueSeven = blue_seven;
this.whiteSeven = white_seven;
this.redSeven = red_seven;
}
// getters for each below...
...
}
Then, I'm going to go ahead and define the three Reels as their own subclasses that will load the symbols in according to the look up tables from the wizards website. The parent class is simple:
public abstract class Reel {
protected String[] symbols;
protected int idx;
public Reel(SymbolNameMap symbolNames) {
symbols = new String[64];
idx = 0;
loadSymbolsToReel(symbolNames);
}
public int spin() {
idx = MathUtils.random(0, symbols.length - 1);
return idx;
}
protected abstract void loadSymbolsToReel(SymbolNameMap symbolNames);
}
We'll add more onto this base class in a little bit, but for now this is all we need. Taking the giant table from the website and copying it into sublime text, I'm not left with the question of turning it into a CSV or something similar. I do want to load arbitrary reel configuration at some point, but while we're getting stood up and a prototype working, we'll just hard code things. To do that, by the power of multi cursors, I can turn the text from the table into code:
And, by the power of multi cursors AND arithmetic, we can correct the human form of 1 based indices to the computer powered 0 based:
And just like that, the definition of the loadSymbolsToReel for the first
reel is completed: 3
public class FirstReel extends Reel {
protected String[] symbols;
public FirstReel(SymbolNameMap symbolNames) {
super(symbolNames);
}
protected void loadSymbolsToReel(SymbolNameMap symbolNames) {
symbols[0] = symbolNames.getTwoBar();
symbols[1] = symbolNames.getTwoBar();
symbols[2] = symbolNames.getTwoBar();
symbols[3] = symbolNames.getBlank();
symbols[4] = symbolNames.getBlank();
symbols[5] = symbolNames.getThreeBar();
symbols[6] = symbolNames.getThreeBar();
symbols[7] = symbolNames.getBlank();
symbols[8] = symbolNames.getBlank();
symbols[9] = symbolNames.getBlank();
symbols[10] = symbolNames.getWhiteSeven();
symbols[11] = symbolNames.getWhiteSeven();
symbols[12] = symbolNames.getWhiteSeven();
symbols[13] = symbolNames.getWhiteSeven();
symbols[14] = symbolNames.getWhiteSeven();
symbols[15] = symbolNames.getWhiteSeven();
symbols[16] = symbolNames.getBlank();
symbols[17] = symbolNames.getBlank();
symbols[18] = symbolNames.getBlank();
symbols[19] = symbolNames.getOneBar();
symbols[20] = symbolNames.getOneBar();
symbols[21] = symbolNames.getOneBar();
symbols[22] = symbolNames.getBlank();
symbols[23] = symbolNames.getBlank();
symbols[24] = symbolNames.getBlank();
symbols[25] = symbolNames.getBlueSeven();
symbols[26] = symbolNames.getBlueSeven();
symbols[27] = symbolNames.getBlueSeven();
symbols[28] = symbolNames.getBlueSeven();
symbols[29] = symbolNames.getBlueSeven();
symbols[30] = symbolNames.getBlueSeven();
symbols[31] = symbolNames.getBlank();
symbols[32] = symbolNames.getBlank();
symbols[33] = symbolNames.getBlank();
symbols[34] = symbolNames.getTwoBar();
symbols[35] = symbolNames.getTwoBar();
symbols[36] = symbolNames.getBlank();
symbols[37] = symbolNames.getBlank();
symbols[38] = symbolNames.getThreeBar();
symbols[39] = symbolNames.getBlank();
symbols[40] = symbolNames.getBlank();
symbols[41] = symbolNames.getBlank();
symbols[42] = symbolNames.getBlank();
symbols[43] = symbolNames.getBlank();
symbols[44] = symbolNames.getRedSeven();
symbols[45] = symbolNames.getBlank();
symbols[46] = symbolNames.getBlank();
symbols[47] = symbolNames.getBlank();
symbols[48] = symbolNames.getBlank();
symbols[49] = symbolNames.getBlank();
symbols[50] = symbolNames.getThreeBar();
symbols[51] = symbolNames.getThreeBar();
symbols[52] = symbolNames.getThreeBar();
symbols[53] = symbolNames.getBlank();
symbols[54] = symbolNames.getBlank();
symbols[55] = symbolNames.getTwoBar();
symbols[56] = symbolNames.getTwoBar();
symbols[57] = symbolNames.getBlank();
symbols[58] = symbolNames.getBlank();
symbols[59] = symbolNames.getOneBar();
symbols[60] = symbolNames.getOneBar();
symbols[61] = symbolNames.getOneBar();
symbols[62] = symbolNames.getBlank();
symbols[63] = symbolNames.getBlank();
}
}
We can declare the the other two reels as subclasses too. You might be screaming at me "just make one class and a loader already pleeeaase", but I'm going to do things the "dumb" way that matches reality first before we do any sort of refactoring or generalizing. We're in the prototyping phase, and unlike all of our day jobs, the risk of a prototype going into production is low since the deadline is controlled by us!
For reference. Here's the 2nd reel:
public class SecondReel extends Reel {
public SecondReel(SymbolNameMap symbolNames) {
super(symbolNames);
}
@Override
protected void loadSymbolsToReel(SymbolNameMap symbolNames) {
symbols[0] = symbolNames.getTwoBar();
symbols[1] = symbolNames.getTwoBar();
symbols[2] = symbolNames.getBlank();
symbols[3] = symbolNames.getBlank();
symbols[4] = symbolNames.getThreeBar();
symbols[5] = symbolNames.getThreeBar();
symbols[6] = symbolNames.getBlank();
symbols[7] = symbolNames.getBlank();
symbols[8] = symbolNames.getBlank();
symbols[9] = symbolNames.getWhiteSeven();
symbols[10] = symbolNames.getBlank();
symbols[11] = symbolNames.getBlank();
symbols[12] = symbolNames.getBlank();
symbols[13] = symbolNames.getOneBar();
symbols[14] = symbolNames.getOneBar();
symbols[15] = symbolNames.getOneBar();
symbols[16] = symbolNames.getOneBar();
symbols[17] = symbolNames.getBlank();
symbols[18] = symbolNames.getBlank();
symbols[19] = symbolNames.getBlank();
symbols[20] = symbolNames.getBlueSeven();
symbols[21] = symbolNames.getBlueSeven();
symbols[22] = symbolNames.getBlueSeven();
symbols[23] = symbolNames.getBlueSeven();
symbols[24] = symbolNames.getBlueSeven();
symbols[25] = symbolNames.getBlueSeven();
symbols[26] = symbolNames.getBlueSeven();
symbols[27] = symbolNames.getBlank();
symbols[28] = symbolNames.getBlank();
symbols[29] = symbolNames.getBlank();
symbols[30] = symbolNames.getTwoBar();
symbols[31] = symbolNames.getTwoBar();
symbols[32] = symbolNames.getBlank();
symbols[33] = symbolNames.getBlank();
symbols[34] = symbolNames.getThreeBar();
symbols[35] = symbolNames.getThreeBar();
symbols[36] = symbolNames.getBlank();
symbols[37] = symbolNames.getBlank();
symbols[38] = symbolNames.getBlank();
symbols[39] = symbolNames.getBlank();
symbols[40] = symbolNames.getBlank();
symbols[41] = symbolNames.getRedSeven();
symbols[42] = symbolNames.getRedSeven();
symbols[43] = symbolNames.getRedSeven();
symbols[44] = symbolNames.getBlank();
symbols[45] = symbolNames.getBlank();
symbols[46] = symbolNames.getBlank();
symbols[47] = symbolNames.getBlank();
symbols[48] = symbolNames.getBlank();
symbols[49] = symbolNames.getThreeBar();
symbols[50] = symbolNames.getThreeBar();
symbols[51] = symbolNames.getThreeBar();
symbols[52] = symbolNames.getBlank();
symbols[53] = symbolNames.getBlank();
symbols[54] = symbolNames.getTwoBar();
symbols[55] = symbolNames.getTwoBar();
symbols[56] = symbolNames.getBlank();
symbols[57] = symbolNames.getBlank();
symbols[58] = symbolNames.getOneBar();
symbols[59] = symbolNames.getOneBar();
symbols[60] = symbolNames.getOneBar();
symbols[61] = symbolNames.getOneBar();
symbols[62] = symbolNames.getBlank();
symbols[63] = symbolNames.getBlank();
}
}
And the third reel:
public class ThirdReel extends Reel {
public ThirdReel(SymbolNameMap symbolNames) {
super(symbolNames);
}
@Override
protected void loadSymbolsToReel(SymbolNameMap symbolNames) {
symbols[0] = symbolNames.getTwoBar();
symbols[1] = symbolNames.getTwoBar();
symbols[2] = symbolNames.getTwoBar();
symbols[3] = symbolNames.getBlank();
symbols[4] = symbolNames.getBlank();
symbols[5] = symbolNames.getThreeBar();
symbols[6] = symbolNames.getBlank();
symbols[7] = symbolNames.getBlank();
symbols[8] = symbolNames.getBlank();
symbols[9] = symbolNames.getWhiteSeven();
symbols[10] = symbolNames.getWhiteSeven();
symbols[11] = symbolNames.getWhiteSeven();
symbols[12] = symbolNames.getWhiteSeven();
symbols[13] = symbolNames.getWhiteSeven();
symbols[14] = symbolNames.getWhiteSeven();
symbols[15] = symbolNames.getWhiteSeven();
symbols[16] = symbolNames.getBlank();
symbols[17] = symbolNames.getBlank();
symbols[18] = symbolNames.getBlank();
symbols[19] = symbolNames.getOneBar();
symbols[20] = symbolNames.getOneBar();
symbols[21] = symbolNames.getOneBar();
symbols[22] = symbolNames.getOneBar();
symbols[23] = symbolNames.getOneBar();
symbols[24] = symbolNames.getBlank();
symbols[25] = symbolNames.getBlank();
symbols[26] = symbolNames.getBlank();
symbols[27] = symbolNames.getBlueSeven();
symbols[28] = symbolNames.getBlank();
symbols[29] = symbolNames.getBlank();
symbols[30] = symbolNames.getBlank();
symbols[31] = symbolNames.getTwoBar();
symbols[32] = symbolNames.getTwoBar();
symbols[33] = symbolNames.getTwoBar();
symbols[34] = symbolNames.getBlank();
symbols[35] = symbolNames.getBlank();
symbols[36] = symbolNames.getThreeBar();
symbols[37] = symbolNames.getBlank();
symbols[38] = symbolNames.getBlank();
symbols[39] = symbolNames.getBlank();
symbols[40] = symbolNames.getBlank();
symbols[41] = symbolNames.getBlank();
symbols[42] = symbolNames.getRedSeven();
symbols[43] = symbolNames.getBlank();
symbols[44] = symbolNames.getBlank();
symbols[45] = symbolNames.getBlank();
symbols[46] = symbolNames.getBlank();
symbols[47] = symbolNames.getBlank();
symbols[48] = symbolNames.getThreeBar();
symbols[49] = symbolNames.getThreeBar();
symbols[50] = symbolNames.getThreeBar();
symbols[51] = symbolNames.getBlank();
symbols[52] = symbolNames.getBlank();
symbols[53] = symbolNames.getTwoBar();
symbols[54] = symbolNames.getTwoBar();
symbols[55] = symbolNames.getTwoBar();
symbols[56] = symbolNames.getBlank();
symbols[57] = symbolNames.getBlank();
symbols[58] = symbolNames.getOneBar();
symbols[59] = symbolNames.getOneBar();
symbols[60] = symbolNames.getOneBar();
symbols[61] = symbolNames.getOneBar();
symbols[62] = symbolNames.getBlank();
symbols[63] = symbolNames.getBlank();
}
}
K, so we've got our reels defined, which means we can define our machine now. Let's start figuring out the shape of the machine class. We'll probably poke, prod, and adjust as we go, but to start, let's just think about what the machine has to do and make some methods and whatnot. First off, it obviously has three reels:
public class SlotMachine {
private Reel first;
private Reel second;
private Reel third;
And, we'll need to know what index of the reel is being displayed currently, so I think I'll try to do that via tracking the centermost index
private int[] currentReelIndicies;
private int[] spunReelIndicices;
And initially, we'll just set both of those to whatever the reel has for its index on initialization (which is 0):
public SlotMachine(SymbolNameMap symbolNameMap) {
first = new FirstReel(symbolNameMap);
second = new SecondReel(symbolNameMap);
third = new ThirdReel(symbolNameMap);
currentReelIndicies = new int[]{
first.getIdx(),
second.getIdx(),
third.getIdx()
};
spunReelIndicices = new int[]{
first.getIdx(),
second.getIdx(),
third.getIdx()
};
}
The two things we need to have the slot machine do that immediately
come to mind is spin and payout. Spinning
is the easy one, we've defined a spin method on the reels, and so we
call each of those and just keep track of bookkeeping:
public void spin() {
int firstIdx = first.spin();
int secondIdx = second.spin();
int thirdIdx = third.spin();
spunReelIndicices[0] = firstIdx;
spunReelIndicices[1] = secondIdx;
spunReelIndicices[2] = thirdIdx;
}
The payout method though, we can't complete yet. Well, besides the bookkeeping itself that we planned to do:
public int payout() {
// Transfer the target to the current for bookkeeping
currentReelIndicies[0] = spunReelIndicices[0];
currentReelIndicies[1] = spunReelIndicices[1];
currentReelIndicies[2] = spunReelIndicices[2];
// Payout table to come :)
return 0;
}
}
So now, we need to talk about the payout look up table! Let me copy over a few of these as an example for us to talk about:
| Win | 1 Coin | 2 Coins | 3 Coins |
|---|---|---|---|
| Red 7, white 7, blue 7 | 2400 | 4800 | 10000 |
| Red 7, red 7, red 7 | 1199 | 2400 | 5000 |
| White 7, white 7, white 7 | 200 | 400 | 600 |
| … | |||
One of the interesting things about slot machines is that you can put in more than one coin at a time and it influences the payout. In the case of this machine, you can enter up to 3 and it can cause a higher payout. The picture I found of the machine indicates this is just a multiplier, not something where the reason the payout is higher is due to multiple lines all matching. Which I think makes our life easier.
If you look at the Tales of Symphonia slot machine that inspired this idea in the first place, you can see that there are multiple paylines! Five actually:
It'd be neat to figure out how to deal with the diagonals, top, bottom, and center paylines. But as I said above, and that I'll keep repeating for your AND my focus, we're going to stick to simple for starters. So for now, one center payline only.
So, we basically have a mapping from 3 symbols to 3 potential payouts. Besides the top two payouts, all the other ones are the single coin payout times three. It's only the top two that are different. Though, the table and the image I found have a big difference. The one in the picture only has up to TWO coins! So, should we use the wizard table of 3? Or the two payout tables of the example? Or just simplify it even further and only have one?
How about we have one, but make it easily swappable as needed? In other words, let's make a seperate class for the payout table! Then we can pass it in at runtime as needed for however many coins one's betting with. We get a class with one focused payout table, and the potential to have 3 different ones. The best of both worlds!
public class PayoutTable {
private final SymbolNameMap symbolNameMap;
private Map<String, Integer> payoutTable;
public PayoutTable(SymbolNameMap symbolNameMap) {
this.symbolNameMap = symbolNameMap;
loadPayoutTable(symbolNameMap);
}
public int payoutFor(String firstSymbol, String secondSymbol, String thirdSymbol) {
String key = keyFor(firstSymbol, secondSymbol, thirdSymbol);
return payoutTable.getOrDefault(key, 0);
}
protected void loadPayoutTable(SymbolNameMap symbolNameMap) {
Map<String, Integer> payoutTable = new Hashtable<>();
payoutTable.put(
keyFor(
symbolNameMap.getRedSeven(),
symbolNameMap.getWhiteSeven(),
symbolNameMap.getBlueSeven()
), 2400
);
// TODO data entry
this.payoutTable = payoutTable;
}
protected String keyFor(String reelOne, String reelTwo, String reelThree) {
return reelOne + reelTwo + reelThree;
}
}
But wait, we have a problem! Or at least, we have a potential annoyance. The data entry isn't just the symbols, we've got things like "any 3 sevens" or "any 3 reds", all inside of the payout table. Sounds like strings and a hash map won't be able to cover everything we're going to need here. At least, not unless I'm willing to sit and type out a bunch of permutations.
I'm not. We can define some helper methods for these instead. Their signatures can look like this if we locate said helpers into the symbol table class:
public boolean isRed(String symbol) {
...
}
public boolean isBlue(String symbol) {
...
}
public boolean isWhite(String symbol) {
...
}
public boolean isBar(String symbol) {
...
}
public boolean isSeven(String symbol) {
...
}
Then it's just a matter of writing up some inclusion lists against the incoming symbols. Nothing too fancy. But if we were using a language like scala or rust we could make some union types and do pattern matching. But, we're using Java! So none of that. I could declare a new type or some enums or something fun like that, but… Simple! We are trying to keep things simple! 4
Also I said inclusion lists because I've had a semi-recent habit of doing things
like [1,2,3].includes(foo) in javascript rather than fallthrough switches,
but really, we're just going to toss some boolean logic in and some equals
public boolean isRed(String symbol) {
return getRedSeven().equals(symbol) ||
getOneBar().equals(symbol);
}
public boolean isBlue(String symbol) {
return getBlueSeven().equals(symbol) ||
getThreeBar().equals(symbol);
}
public boolean isWhite(String symbol) {
// Blank is NOT white, otherwise the payout of blankblankblank would make no sense
return getWhiteSeven().equals(symbol) ||
getTwoBar().equals(symbol);
}
public boolean isBar(String symbol) {
return getOneBar().equals(symbol) ||
getTwoBar().equals(symbol) ||
getThreeBar().equals(symbol);
}
public boolean isSeven(String symbol) {
return getRedSeven().equals(symbol) ||
getWhiteSeven().equals(symbol) ||
getBlueSeven().equals(symbol);
}
Now, you might be asking yourself: But if we're going to do this, then why did we bother creating a hash map in the first place? My friend. Let me introduce you to a magical, magical thing:
That's right. There's a handy dandy computeIfAbsent function!
So, we can code in the simple payouts that are tied to very specific combinations as
stored keys, and then for the ones that are more category driven, we can insert them
via this computation function on the fly. I suppose this means that over time we'll
end up with more memory used as people play, and that could be a problem if someone
played the game for a really, really, long time. But no ones gonna do that!
Anyway, let's finish creating the paytable for the fixed values that don't pay out based on the category of the symbol or its color:
protected void loadPayoutTable(SymbolNameMap symbolNameMap) {
Map<String, Integer> payoutTable = new Hashtable<>();
payoutTable.put(
keyFor(
symbolNameMap.getRedSeven(),
symbolNameMap.getWhiteSeven(),
symbolNameMap.getBlueSeven()
), 2400
);
payoutTable.put(
keyFor(
symbolNameMap.getRedSeven(),
symbolNameMap.getRedSeven(),
symbolNameMap.getRedSeven()
), 1200
);
payoutTable.put(
keyFor(
symbolNameMap.getWhiteSeven(),
symbolNameMap.getWhiteSeven(),
symbolNameMap.getWhiteSeven()
), 200
);
payoutTable.put(
keyFor(
symbolNameMap.getBlueSeven(),
symbolNameMap.getBlueSeven(),
symbolNameMap.getBlueSeven()
), 150
);
payoutTable.put(
keyFor(
symbolNameMap.getOneBar(),
symbolNameMap.getTwoBar(),
symbolNameMap.getThreeBar()
), 50
);
payoutTable.put(
keyFor(
symbolNameMap.getThreeBar(),
symbolNameMap.getThreeBar(),
symbolNameMap.getThreeBar()
), 40
);
payoutTable.put(
keyFor(
symbolNameMap.getTwoBar(),
symbolNameMap.getTwoBar(),
symbolNameMap.getTwoBar()
), 25
);
payoutTable.put(
keyFor(
symbolNameMap.getOneBar(),
symbolNameMap.getOneBar(),
symbolNameMap.getOneBar()
), 10
);
payoutTable.put(
keyFor(
symbolNameMap.getBlank(),
symbolNameMap.getBlank(),
symbolNameMap.getBlank()
), 1
);
this.payoutTable = payoutTable;
}
Then, we can compute the few category rules on the fly using the key for the map and…
private Integer checkCategories(String s) {
symbolNameMap.isBlue(s) // Ah, that's a problem, we need all 3 key parts!
return 0;
}
Well drat. You know, the fact that our key to our table isn't a tuple that we can
pull the parts out of is biting us now. I suppose I could define a proper type for
the key rather than just using a string. We'd implement equals and hashCode
and it'd all be tons of lovely fun. Or. Or we could just compute 'em and
store 'em without the fancy pants function. Or we could define a closure.
But eh, let's just keep it simple, safe, and s… uh… something else
that begins with s that makes sense. S stands for stop judging me, I got excited about
using the fun compute function. Anyway, here's the code:
public int payoutFor(String firstSymbol, String secondSymbol, String thirdSymbol) {
String key = keyFor(firstSymbol, secondSymbol, thirdSymbol);
if (payoutTable.containsKey(key)) {
return payoutTable.get(key);
}
String[] row = new String[]{firstSymbol, secondSymbol, thirdSymbol};
boolean allSevens = Arrays.stream(row).allMatch(symbolNameMap::isSeven);
if (allSevens) {
payoutTable.put(key, 80);
return payoutTable.get(key);
}
boolean redWhiteAndBlue = symbolNameMap.isRed(firstSymbol) &&
symbolNameMap.isWhite(secondSymbol) &&
symbolNameMap.isBlue(thirdSymbol);
if (redWhiteAndBlue) {
payoutTable.put(key, 20);
return payoutTable.get(key);
}
boolean allBars = Arrays.stream(row).allMatch(symbolNameMap::isBar);
if (allBars) {
payoutTable.put(key, 5);
return payoutTable.get(key);
}
boolean allReds = Arrays.stream(row).allMatch(symbolNameMap::isRed);
if (allReds) {
payoutTable.put(key, 2);
return payoutTable.get(key);
}
boolean allWhite = Arrays.stream(row).allMatch(symbolNameMap::isWhite);
if (allWhite) {
payoutTable.put(key, 2);
return payoutTable.get(key);
}
boolean allBlue = Arrays.stream(row).allMatch(symbolNameMap::isBlue);
if (allBlue) {
payoutTable.put(key, 2);
return payoutTable.get(key);
}
payoutTable.put(key, 0);
return payoutTable.get(key);
}
The only thing that I think is worth calling out in this code is the fact that,
while put will return an integer, it returns what was stored
previously in that key in the map. So, it's just a bunch of null
values in there, otherwise we could return payoutTable.put our way
to less code. But that's just how it is. Write Java? Be verbose. Enjoy it!
Oh, the other thing is taste. I could write return 0 at the
end there. I'm not going to though. Why, why oh why do you hate wasting cpu?
cries the people who condemn the use of goto and claim one must metal program or lest
ye be a quiche eater. 5 But
I prefer my code to be consistent. I return from the .get in every point
in this file, I'm going to finish with it. I trust that the compiler is going to
optimize it for me, I can't imagine it's not smart enough to do that. And, if it
doesn't and I got to optimize the hell out of this, then I'm only touching it if the
flame graphs of war direct my sword to its throat.
Anywhoodle, metaphorical demon fighting aside, We have a payout table! So, we can
update out payout function in the SlotMachine class! So
this
public int payout() {
// Transfer the target to the current for bookkeeping
currentReelIndicies[0] = spunReelIndicices[0];
currentReelIndicies[1] = spunReelIndicices[1];
currentReelIndicies[2] = spunReelIndicices[2];
// Payout table to come :)
return 0;
}
becomes
public int payout() {
// Transfer the target to the current for bookkeeping
currentReelIndicies[0] = spunReelIndicices[0];
currentReelIndicies[1] = spunReelIndicices[1];
currentReelIndicies[2] = spunReelIndicices[2];
return payoutTable.payoutFor(
first.getCurrentSymbol(),
second.getCurrentSymbol(),
third.getCurrentSymbol()
);
}
Nothing surprising there, we made the helper to use it after all! And now we've got a whole bunch of lovely core classes that we faithfully believe will definitely work first try. You probably know where this is going.
Yup that's right we need to test! Let's see if we can birth a few solid unit tests to ensure that all of the little pieces we care about actually work as expected. Though, before we can deliver the goods there, we need to actually set up the project for tests.
You might be tempted to try to use the intelliji generators and the add to lib type dialogs that exist:
But, well.
It doesn't really work. So, rather than toss jars into the lib folder and pray 6 we can follow the steps that I've done in past blog posts to add in JUnit and Jupiter as needed to get things going. For reference, it's two steps in this particular version of LibGDX! Step 1: update the root build file with a new configure block:
configure(":core") {
apply plugin: 'java-library'
repositories {
mavenCentral()
}
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
sourceSets.test.java.srcDirs = [ "test/" ]
}
And step 2, update the core/build.gradle file with
repositories {
mavenCentral()
}
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testImplementation('org.junit.jupiter:junit-jupiter-params:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
I didn't delete anything from the two build files by the way, just added them in as new stand alone blocks. But once that's done, the test that does nothing runs without throwing up an error:
Which means we can add in some test cases and supporting code! Since we basically
copied a table to get our results, we'll want to make our test cases powered by a
table too. Or, well, something like that. We'll use the @ParameterizedTest
annotation to make it easy to define a whole bunch of tests in one fell swoop! To
do that though, we need to define what an individual test case is:
static class RowAndPayout {
private final String r1;
private final String r2;
private final String r3;
private final int win;
public RowAndPayout(String r1, String r2, String r3, int win) {
this.r1 = r1;
this.r2 = r2;
this.r3 = r3;
this.win = win;
}
public static RowAndPayout caseOf(String s1, String s2, String s3, int winnings) {
return new RowAndPayout(s1, s2, s3, winnings);
}
public String toString() {
return r1 + "|" + r2 + "|" + r3 + " = " + win;
}
}
The only thing of note (besides my poor variable naming) is that we've got a static method intended to save a lttle bit of syntactical noise. Which is to say, that we can define a list of test cases in a really easy way AND it will be readable:
static Stream<RowAndPayout> payoutExpectations() {
return Stream.of(
RowAndPayout.caseOf("7r", "7w", "7b", 2400),
RowAndPayout.caseOf("7r", "7r", "7r", 1200),
RowAndPayout.caseOf("7w", "7w", "7w", 200),
RowAndPayout.caseOf("7b", "7b", "7b", 150),
RowAndPayout.caseOf("7b", "7r", "7w", 80),
RowAndPayout.caseOf("7w", "7r", "7w", 80),
RowAndPayout.caseOf("br1", "bw2", "bb3", 50),
RowAndPayout.caseOf("bb3", "bb3", "bb3", 40),
RowAndPayout.caseOf("bw2", "bw2", "bw2", 25),
RowAndPayout.caseOf("br1", "7w", "bb3", 20),
RowAndPayout.caseOf("br1", "br1", "br1", 10),
RowAndPayout.caseOf("br1", "bb3", "br1", 5),
RowAndPayout.caseOf("bb3", "bb3", "br1", 5),
RowAndPayout.caseOf("7r", "br1", "br1", 2),
RowAndPayout.caseOf("7w", "bw2", "7w", 2),
RowAndPayout.caseOf("7b", "bb3", "7b", 2),
RowAndPayout.caseOf("_", "_", "_", 1),
RowAndPayout.caseOf("_", "7r", "_", 0),
RowAndPayout.caseOf("_", "7r", "bw2", 0)
);
}
But what's with all the strings you might say? What do they MEAN!? Well, they're just notation shorthand for the colors, shapes, and what have you we've already described base on the picture we saw. Within the test class, I'm going to define those shapes in our symbol map:
PayoutTable payoutTable;
@org.junit.jupiter.api.BeforeEach
void setUp() {
payoutTable = new PayoutTable(new SymbolNameMap(
"_", // blank
"br1", // 1 bar (red)
"bw2", // 2 bars (white)
"bb3", // 3 bars (blue)
"7b", // Blue seven
"7w", // White seven
"7r" // Red seven
));
}
I'm making up my own shorthand here because I wanted it to be quick to write out while looking at my other monitor and using the payout reference table to create cases. There are a lot more cases than this obviously, but a smattering of values should work ok. Though, this would actually be a decent use case for property based testing or fuzz testing where we let random values get generated that must match a given criteria…
Ahem. Let's put that aside (it would be fun though), the last piece of the puzzle is the actual test itself! We've got the test cases ready, we've got the table created, and so, our test using our test case class and friends is:
@ParameterizedTest
@MethodSource("payoutExpectations")
void payoutForTest(RowAndPayout rowAndPayout) {
int amountWon = payoutTable.payoutFor(rowAndPayout.r1, rowAndPayout.r2, rowAndPayout.r3);
assertEquals(rowAndPayout.win, amountWon, rowAndPayout.toString());
}
Really short, huh. That's kind of intentional, the test case is super simple because all the "heavy" setup work is already done via the helpers and whatnot. It wouldn't be hard to define a new test case each time, filling up the class with method after method. But that's not really needed, parameterized tests are a wonderful thing since as long as you can define your test case and its requirements within a few pieces of data passed to your test, you're good to go. And in fact,
We're good to go! The payout table is accurate and the category checking is working! So, what else should we test in our core logic? Well, I could probably whip up a test for the payout, it's using RNG, but we can probably fix a seed, and if not, at the very least, we know we start the initial reels at 0 0 0 everytime, so we could figure that out and make a test.
class SlotMachineTest {
SlotMachine slotMachine;
SymbolNameMap symbolNameMap;
@BeforeEach
void setUp() {
symbolNameMap = new SymbolNameMap(
"_",
"br1",
"bw2",
"bb3",
"7b",
"7w",
"7r"
);
slotMachine = new SlotMachine(symbolNameMap);
}
@Test
void payout() {
// Initial reels are at 2 bar 2 bar 2 bar
assertEquals(25, initialAll0Payout);
}
}
Annd if I run it
Cannot store to object array because "this.symbols" is null
java.lang.NullPointerException: Cannot store to object array because "this.symbols" is null
at spare.peetseater.games.slots.core.FirstReel.loadSymbolsToReel(FirstReel.java:11)
at spare.peetseater.games.slots.core.Reel.<init>(Reel.java:12)
at spare.peetseater.games.slots.core.FirstReel.<init>(FirstReel.java:7)
at spare.peetseater.games.slots.core.SlotMachine.<init>(SlotMachine.java:15)
at spare.peetseater.games.slots.core.SlotMachineTest.setUp(SlotMachineTest.java:24)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
Hm? Wait, what. What did I… Oh. OH, wow. You know, this feels like a classic mistake.
If the invocation occurs during initialization of a parent class but invokes an implementation in a subclass, the subclass implementation might refer to fields in the subclass that have not yet been initialized. This can cause unexpected behavior and commonly causes null pointer exceptions.
src: oracle blog
And while it feels like a classic mistake, I got curious here. I found that quote on a blog post from oracle. Where do they outline this in the language manual? Well, assuming you can find the damn thing, the information seems to be split up all over the place. For example,
calling non-final methods during instance initialization can cause problems
src: Java Tutorial on initializing instance members
Yes, no shit. The runtime exception clearly indicates that this has happened. But where did you caution me against such foolishness Oracle!? Where!!? Perhaps in the section about super types?
If a subclass constructor invokes a constructor of its superclass, either explicitly or implicitly, you might think that there will be a whole chain of constructors called, all the way back to the constructor of Object. In fact, this is the case. It is called constructor chaining, and you need to be aware of it when there is a long line of class descent.
src: Java Tutorial on the super keyword
Oh yes, indeed, we must be aware of such things. But why and how should we avoid common problems that we might see when doing such things oracle? Hm? Using a template method that's abstract feels like something I should be able to do anywhere. Explain to me, like the nice blog post or the numerous stackoverflow posts have, about this oddity please! You know where it doesn't mention that perhaps you shouldn't call an abstract method from a constructor? The page about abstract methods!
Perhaps I expect too much for the folks who release the documentation for the language they're stewards of to actually write common potential problems down somewhere easily findable. Then again, perhaps I ask too much of myself and have found my foot in my mouth, because the real problem that is causing this isn't so much that the super class has to finish calling its constructor before the subclass is free to reference its own fields, but rather, that the subclass accidentally has hidden the field itself and that's what's causing the actual problem.
This bug is pretty subtle if you want to go look back at our declarations of the Reel and its subclasses to see if you can spot it. Go ahead, I'll wait.
A hint? There's a difference between the first, second, and third reel's definitions.
Ready for the spoiler then?
public class FirstReel extends Reel {
-protected String[] symbols;
public FirstReel(SymbolNameMap symbolNames) {
super(symbolNames);
}
Yeah, I accidentally had the array declared within the subclass from when I was first hashing out a couple ideas before declaring the subclass. Wups! This is why we test things folks! Imagine if I had merrily chugged along to the next part then ran into the error after I had already left "core library" brain mode, and was now in "UI do thing" mode. Resetting all those neurons in my head!? The calamity! Thoughts, uprooted! Context? Lost! The ever shifting sands of time under my feet pulling my into a quicksand mire of pure lost productivity, oh no, say it isn't so!
Ok it wouldn't be that big of a deal in this particular case, but still. This is a class of problem that can peak out, and if you write just a whole bunch of library code and then shuffle over to some other system that's using it, then it's easy to mistakenly think that it wasn't YOUR code that caused the problem, but something in this new place you're tinkering in. That can lead to a lot of lost time and frustation. Anyway, the good news is that the test passes now.
So, what if I want to test a seeded random value? I did a quick search of "libgdx mathutils seed random" and found this stackoverflow post which outlines that apparently, you can't really rely on LibGDX to not call the random function itself internally, so if you're doing something expecting a specific sequence, it may behave a bit funny. But, I'm just calling random three times in a row, so I'm curious if this actually aligns to my own needs or not. Let's find out!
@Test
void spin() {
MathUtils.random.setSeed(1);
slotMachine.spin();
System.out.println(slotMachine.getFirstReel().getCurrentSymbol());
System.out.println(slotMachine.getSecondReel().getCurrentSymbol());
System.out.println(slotMachine.getThirdReel().getCurrentSymbol());
}
Now, I'm not asserting anything yet, so it's not really a test. The real test is running it twice to find out if it's worth bothering writing a test in the first place:
Ok, the squishing to get both of these side by side might make it a bit hard to see. And you might think I just took screenshots twice of the same run, but I promise, I ran it twice! To prove it, we can update our test to verify things
@Test
void spin() {
MathUtils.random.setSeed(1);
slotMachine.spin();
String spin1Reel1 = slotMachine.getFirstReel().getCurrentSymbol();
String spin1Reel2 = slotMachine.getSecondReel().getCurrentSymbol();
String spin1Reel3 = slotMachine.getThirdReel().getCurrentSymbol();
MathUtils.random.setSeed(1);
slotMachine.spin();
String spin2Reel1 = slotMachine.getFirstReel().getCurrentSymbol();
String spin2Reel2 = slotMachine.getSecondReel().getCurrentSymbol();
String spin2Reel3 = slotMachine.getThirdReel().getCurrentSymbol();
assertEquals(spin1Reel1, spin2Reel1);
assertEquals(spin1Reel2, spin2Reel2);
assertEquals(spin1Reel3, spin2Reel3);
}
So, in our case, because LibGDX isn't really doing anything else in between calls, it seems like seeding the random value does actually work as expected to rig the machine. Great. It's nice to know that our spinning code works as expected, and if I need to test some specific combo later on, we can see if the rigging works once the rest of libgdx is running or not.
Anyway. Besides the fact that we haven't implemented a "coin" feature here, the core logic is done. And by coin, I mean the whole, put two coins in and get out more money. For our game, I'm going to just do a simple multiplier on the payouts themselves for that. We're making simple slots after all, oh hey, maybe that should be the name of the game!
How do we spin? ↩
Besides the SAR sheets and all other fun statistical stuff that intrigued me about this project, the visual side of things that intrigues me is how the reels spin. There's definitely easing functions at work here I think, and it also begs the question of what the best way to model something like this is. So, before we deal with a title screen, selecting assets, or doing anything like that, I think we should figure out one of the core dopamine hits of a slot machine.
The spin! 7
I was in a meeting the other day and one of the designers present linked to animista, which is a pretty neat site for CSS animations. Obviously, we can't really use CSS here, but we can take the general ideas from it and look at different easing functions and then port them over. Specifically, the bounce in animation seems pretty good for our needs. I'll have to find my own formula for it, but it's nice to get an idea of what to search around for as far as "ease in" or "ease out" things.
Let's load up a texture into the screen and make it "spin" on a button press to test things out. The texture loading part is pretty simple, we need two things. The symbol textures:
and the machine to mask the edges a bit (more on this in a sec)
the symbol sheet above I've put together into one image for blogging convenience, but in the test code, we're just being quick and dirty and loading these textures up directly. I haven't even renamed the "First screen" class because we're in prototyping mode, which means we go with what works first in order to figure out the better option after.
public class FirstScreen implements Screen {
SpriteBatch batch;
OrthographicCamera camera;
Viewport viewport;
Texture[] textures = new Texture[6];
Texture machineMask;
@Override
public void show() {
batch = new SpriteBatch();
camera = new OrthographicCamera();
viewport = new FitViewport(1280, 720, camera);
camera.setToOrtho(false);
camera.update();
textures[0] = new Texture(Gdx.files.internal("RedSeven.png"));
textures[1] = new Texture(Gdx.files.internal("WhiteSeven.png"));
textures[2] = new Texture(Gdx.files.internal("BlueSeven.png"));
textures[3] = new Texture(Gdx.files.internal("OneBar.png"));
textures[4] = new Texture(Gdx.files.internal("TwoBar.png"));
textures[5] = new Texture(Gdx.files.internal("ThreeBar.png"));
machineMask = new Texture(Gdx.files.internal("MachineMaskMini.png"));
}
...
Now, about the "mask mini". You may already know what an image mask is, but just in case the term is unfamiliar. Thing about a pumpkin! When you carve a halloween pumpkin you don't (typically) leave the pumpkin parts in for the eyes and mouth, rather, you remove the pumpkin pieces from where you want the light to shine through to make the face. An image mask is the same thing, rather than draw our reels, we draw the machine around the reels and leave the slots open for the symbols to shine through. I'm probably overthinking my explanation here, but it should be pretty obvious even if I'm doing a poor job of it when you look at this image:
The actual mask has a transparent background, but I coloured it grey for you to make the gridlines show up better. As you can see, the grid is 32 by 18, or a 16:9 aspect ratio. So, given a 1280x720 window, each block is 40 pixels large. The reason I didn't make the grid just 16 by 9 is because each of the symbol textures are 400x400 pixels large, and so I wanted to map that to 4 units by 4 units while mapping out the layout. As to why I wanted that, make a grid of 16x9 pixels in paint or some similar program. Now try to create a mask that will have 3 open slots for symbols to appear in.
16x9 exactly doesn't really play well with this, but 32x18 does! It gives me the rough shape I want for our eventual slot machine, and so it makes for a good grid. That said, you might notice in the image above that things aren't quite lining up yet. That's because I was trying to do some math with aspect ratios and what have you myself that wasn't quite right. The proper way to set things up so that the math works out and I can think in the same units in both the pixel by pixel grid, and in my grid, is to set up a viewport like so instead:
@Override
public void show() {
...
viewport = new FitViewport(32, 18, camera);
...
}
@Override
public void render(float delta) {
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
for (int i = 0; i < 3; i++) {
float x = 8 + i * 6;
for (int j = 0; j < textures.length; j++) {
float y = 3 + j * 4;
batch.draw(textures[j], x, y, 4, 4);
}
}
batch.draw(machineMask, 0, 0, 32, 18);
batch.end();
if (Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) {
Gdx.app.exit();
}
}
Using 32x18, just like the minimask image, lets us figure out our placement by just counting the blocks. So, the x position of the symbols starts with an offset of 8 blocks, then each reel and divider is 6 apart from anchor to anchor point. 4 blocks high, and 4x4 for the actual texture width and height. Letting LibGDX's viewports figure out the math aligns things much much better: 8
So, now we can shift our thoughts over to the spinning problem. I was thinking about this on my way back from eating lunch today and decided that modular arithmetic and a "move towards" behavior of sorts should work. By this, I mean what I did when we wrote up the movement code for the tiles in the match 3 game. In essence, rather than setting a velocity and updating a vector on every game tick, we set a destination, then let interpolation track the current location and if we've hit the destination or not yet.
To do this, it's easiest to reify the movement itself into its own class. Otherwise we're going to be tracking a lot of variables that are going to clutter things up. In the match 3 game I had a moveable position, and while that was useful then, I think for the time being I'm going to just toss things into one class without seperating the movement out yet for now:
public class ReelSymbol {
float currentX, currentY, destinationX, destinationY = 0;
float accum, spinTime = 0;
float width, height = 4;
Texture texture;
public ReelSymbol(float sx, float sy, Texture texture, float width, float height) {
this.texture = texture;
this.currentX = this.destinationX = sx;
this.currentY = this.destinationY = sy;
this.width = width;
this.height = height;
}
...
}
Tracking the position and dimensions of a UI element and its texture is the main point of this class. As noted, we've got an accumulator value and a spin time counter. These two are used for our update method in order to track how far along we are to our destination
public void update(float delta) {
float episilon = 0.000000001f;
if (
nearlyEqual(currentX, destinationX, episilon) &&
nearlyEqual(currentY, destinationY, episilon)
) {
return;
}
accum += delta;
float alpha = MathUtils.clamp(accum / spinTime, 0, 1);
Vector2 a = new Vector2(currentX, currentY);
Vector2 newPosition = a.interpolate(
new Vector2(destinationX, destinationY), alpha, Interpolation.bounceIn
);
currentX = newPosition.x;
currentY = newPosition.y;
}
There's a bit more to talk about here beyond just the expected interpolation between
two values though. nearlyEqual? That's a method lifted from this lovely
blog post about comparing floats.
As some of you might know, floating point values aren't actually easily comparable. It's very
very possible that the computer can't actually represent the value you want with the expected
precision, because of this we can't just do currentX == destinationX. Why? Because
it's possible that will always be false due to the way the floats are being represented.
If we've already reached our destination, then we don't need to update any more. 9
public void setDestination(float x, float y, float spinTime) {
this.destinationX = x;
this.destinationY = y;
this.spinTime = spinTime;
}
When we set our destination, we need to define how long the spin time is going to be. I don't really like this very much, but since my main goal right now is getting the reel spinning, we'll revisit this momentarily. Drawing is also slightly different than what you might initially expect:
public void draw(SpriteBatch batch, float modulo) {
batch.draw(texture, currentX, currentY % modulo, width, height);
}
Our draw method takes a modulo! This is because as we interpolate to a destination, we need to wrap the number back around. The reel is a circle after all! Or, a cylinder I guess? Anyway, I'll draw a picture to illustrate all this, but lets get through the rest of the prototype code first. When I press the space bar, I'll set the destinations and get the reel spinning:
symbolsOnReels.clear();
for (int i = 0; i < 3; i++) {
float x = 8 + i * 6;
for (int j = 0; j < textures.length; j++) {
float y = 3 + j * 4;
symbolsOnReels.add(new ReelSymbol(x, y, textures[j], 4 , 4));
maxYForSpinning = y - 3;
}
}
for (ReelSymbol symbolTexture : symbolsOnReels) {
symbolTexture.setDestination(
symbolTexture.getCurrentX(),
symbolTexture.getCurrentY() + maxYForSpinning * 5,
10f
);
}
Alright, so that first loop is the initial setup just like we did before, but this time
we're tracking a maxYForSpinning value. If you think about keeping something
within a boundary and looping with modulo, you typically think about the screen size, like
the blue box in this picture:
But, we want to keep things in the spinning reel space, not neccesarily the screen space. Sure, we'll only show something if its in the red box, because we only have space to show 3 symbols at a time to the player. But the wheel is spinning out of sight, and so when we wrap the numbers around, we need to be wrapping it around the size of the reel, not the screen or the window to the reel. Makes sense?
Now, we're not actually using the real reel yet (hehe), because we're just looping over the list of textures that we have, mainly because I wanted to test that spinning animation. And so, test I did:
Wups. I forgot to clear the screen! Fixing that up with a quick call to the screen utils in our drawing function for the screen removes that issue though.
ScreenUtils.clear(Color.BLACK);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
for (ReelSymbol symbolTexture : symbolsOnReels) {
symbolTexture.update(delta);
symbolTexture.draw(batch, textures.length * 4);
}
batch.draw(machineMask, 0, 0, 32, 18);
batch.end();
Neat! That doesn't look bad, but it's also not quite right either. You might think that
the wrong thing is the modulo textures.length * 4. No. That's fine. Because
we're not using the actual reel classes yet, we only have one of each symbol and
that's tracked in that textures list, each symbol is 4 units wide, and so the
reel's overall size is indeed four times the length of its array.
No, the thing that's wrong is the fact that this piece of code can be changed and it has 0 affect on the spin
symbolTexture.setDestination(
symbolTexture.getCurrentX(),
symbolTexture.getCurrentY() + maxYForSpinning * 5, // number of loops
10f // <-- spin time
);
The 5 here I intended to control how many loops there are, and effectively
it controls how quickly the pieces move. Use a low number? They move slow. A large number?
You get that classic smearing effect. That's fine, but that spin time value? That's not
working at all! Look! It says 10! As in ten seconds! Look at the video above,
did that take 10 seconds?
No. Not at all! Something is wrong. But what?
The accumulator gets stuck because we early return! Well ok, so, maybe this is because of the bounce function, after all, when we bounce with an easing function we'll be near the value we want to be, then "bounce" away from it. So, to test that idea, what if we just use a linear approach instead to the number?
Hm?
This is after the reel has stopping spinning by the way. It's just sitting there. Not accumulating. What's wrong with you little accumulator!? The obvious potential problem is the early return, and if I remove it, of course the value increases. Forever. We need to decouple the update of the values counting up from whether or not the symbol has reached its destination or not. When things behave unexpectedly. Simplify. We've got a 2 variable problem, and just like math, solving for one unknown value is a lot easier than solving for two. So let's toss out the prototype and try again.
The first lego in our building block of fun is a simple accumulator. Devoid of time, space, and cares of the world of things like x, y, and velocities. It needs only to perform one simple function, count up:
public class TimedAccumulator {
private float target;
private float accum;
public TimedAccumulator() { this(0); }
public TimedAccumulator(float forSeconds) {
accum = 0;
this.target = forSeconds;
}
public void update(float delta) {
if (isDone()) {
accum = target;
return;
}
accum += delta;
}
public boolean isDone() {
return accum >= target;
}
/** Returns 0 - 1, 1 being 100% complete*/
public float getProgress() {
if (target == 0) return 1;
return accum / target;
}
public void reset() {
accum = 0;
}
public void setTarget(float newTarget) {
this.target = newTarget;
}
}
There's nothing that special about this, just wrapping up how we would track an accum
variable anywhere else and put it into an easy-to-reuse object. Now, let's continue this line of
thinking. The spinning we were doing and the slowing down and landing on a specific spot, both of
those can be seperated to make reasoning about the code easier. In both cases, we have a position,
and a way to update it. So, as an interface:
import com.badlogic.gdx.math.Vector2;
public interface PositionBehavior {
public Vector2 getPosition();
public void update(float delta);
}
and then, let's consider the vertical spin. It's pretty simple, but rather than using a
modulo to wrap, we just clamp things immediately down to the other end of the range.
Which means that it's on the caller to properly define the range that the y position
should exist within. I'll omit the getPosition from implementors of this
interface because it's just a return of the value. The rest is of interest though:
public class VerticallySpinningPosition implements PositionBehavior {
private final Vector2 position;
private final Vector2 range;
private final float speed;
/**
* @param range range [x,y]
* The position will wrap to x when position.x reaches y
* */
public VerticallySpinningPosition(Vector2 position, float speed, Vector2 range) {
this.position = position.cpy();
this.speed = speed;
this.range = range;
}
public void update(float delta) {
position.y += speed * delta;
if (position.y > range.y) {
position.y = range.x;
}
}
...
}
As you can see though, there's really nothing complicated about this code at all. Similar, there's nothing complicated about the code to stop something from spinning and have it land in the appropriate place after some time. It's just the destination code, but pulled out:
public class ArrivingAtPosition implements PositionBehavior {
private Vector2 current;
private final Vector2 end;
private final TimedAccumulator accumulator;
public ArrivingAtPosition(Vector2 start, Vector2 end, float withinTime) {
this.current = start.cpy();
this.end = end.cpy();
accumulator = new TimedAccumulator(withinTime);
}
public void update(float delta) {
accumulator.update(delta);
this.current.interpolate(end, accumulator.getProgress(), Interpolation.linear);
}
...
}
So, we can spin. we can move towards a position. What about the beginning of the session though? Or after the position has arrived where we told it to go? It doesn't move at all. And sure, I supose you could arrive at your own current position, but I think it'd be nice to have something seperate for that rather than a hacky-feeling re-use of something that isn't named for what it is:
<pre>
public class StationaryPosition implements PositionBehavior {
Vector2 position;
public StationaryPosition(float x, float y) {
position = new Vector2(x, y);
}
@Override
public Vector2 getPosition() {
return position;
}
@Override
public void update(float delta) {
// Do nothing.
}
}
Yup. Simple. With these four basic building blocks we can tweak our code from before
into something that actually works. The ReelSymbol can track a behavior,
and we don't need to track current or destination tuples. And so, it simplifies down
to just this:
public class ReelSymbol {
float width, height = 4;
Texture texture;
PositionBehavior positionBehavior;
public ReelSymbol(float x, float y, Texture texture, float width, float height) {
positionBehavior = new StationaryPosition(x, y);
this.texture = texture;
this.width = width;
this.height = height;
}
public void draw(SpriteBatch batch, float modulo) {
batch.draw(
texture,
positionBehavior.getPosition().x,
positionBehavior.getPosition().y % modulo,
width,
height
);
}
public void update(float delta) {
positionBehavior.update(delta);
}
public Vector2 getPosition() {
return positionBehavior.getPosition();
}
public void setPositionBehavior(PositionBehavior behavior) {
this.positionBehavior = behavior;
}
}
I'm not 100% happy with the modulo still worming its way into our draw method. But we're going to live with that for a little bit longer. Baby steps. For now though, you can see that a little indirection has simplified the reel symbol quite a bit. I want to wrap a reel into a class too, but for now, let's get the screen code compiling by replacing where it was calling the methods we've removed with our new behaviors. First up, to time the actual spin, we'll use the accumulator in the screen:
public class FirstScreen implements Screen {
Optional<TimedAccumulator> maybeSpin;
public FirstScreen(...) {
...
maybeSpin = Optional.empty();
}
public void render(float delta) {
...
maybeSpin.ifPresent((spin) -> {
spin.update(delta);
});
...
}
}
I don't especially like using null values if I can help it. While we could
represent this with a null, having to write code like if (spin != null && spin.isDone())
just doesn't feel good. When I use nulls I always feel their weight in my fingertips, ready to boil
over and cause pain if I'm not careful. They're just spiny and unpleasant to use. Optional
on the other hand, is an easy construct to use and pretty declarative of what it is, which I like.
Speaking of declaring, we need to actually set it to something when we press space.
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
maybeSpin = Optional.of(new TimedAccumulator(3f));
for (ReelSymbol symbolTexture : symbolsOnReels) {
symbolTexture.setPositionBehavior(
new VerticallySpinningPosition(
symbolTexture.getPosition(), // where!
4 * maxYForSpinning, // speed!
new Vector2(-1, maxYForSpinning) // range!
)
);
}
}
I'm arbitrarily choosing 3 seconds for testing purposes here. The max speed hasn't changed from our previous code though, and the range we'll restrict ourselves to includes a negative number. If you remember from the grid notes, the part above the reel is only 3 units large, but our symbols are 4 by 4. So if something loops around and we want to have it come into view (and not immediately be in the frame for each reel) then we have to shift things back a little bit.
Now, the reels can spin, but we need them to stop too. We've already got the spin accumulator running now, and so we can use its helper to define what to do when we've been spinning for an appropriate amount of time:
if (maybeSpin.isPresent() && maybeSpin.get().isDone()) {
Iterator<ReelSymbol> iter = symbolsOnReels.iterator();
for (int i = 0; i < 3; i++) {
float x = 8 + i * 6;
for (int j = 0; j < textures.length; j++) {
float y = 3 + j * 4;
ReelSymbol reelSymbol = iter.next();
reelSymbol.setPositionBehavior(
new ArrivingAtPosition(
reelSymbol.getPosition(), // from where you are
new Vector2(x, y), // arrive here!
1f // within x seconds
)
);
}
}
maybeSpin = Optional.empty();
}
Since we're just prototyping still, I'm not bother with the "real" positions of the reel symbols, and instead re-using the original placements of the test symbols to set where the spinning symbols should land on. This does have a bit of a funny side effect with the interpolation sending symbols up and down depending on where they were at the time. But, most importantly:
It actually spins for how long we tell it too! There is one silly thing going on though. Can you spot it?
That's right, some of the symbols are drunk! Or rather, when we tell something to take on the behavior of "arriving at destination X", we don't tell it to keep incrementing for us and we'll modulo you later, no, we just tell the vector to start moving towards where we want it to be. And so, you end up with some symbols going up, some down, and all of it looking a bit messy.
It's just not very reellike I suppose. But, it did solve our problem with the timing not being respected. So that's that, we've got the spinning animation going, it's time to move onto the next big question for our game!
How do we stop? ↩
Maybe a strange question, but as you can see in that last capture, when we move the symbols to their final destination they take the most direct path to their stopping point. They don't loop around first so that they don't go up when the reel's spinning direction is down, or vice versa. This is a defect I suppose from our cutting things up into seperate components. And while I think it's sort of fun looking sometimes, it doesn't really feel right for the symbols to be able to play musical chairs with their proper positions does it.
But then again, "proper position", isn't even properly defined yet now is it? We've just been looping the texture list itself, not using anything related to our actual reels yet! So, I think as we solve the problem of "arriving", we should do so within the context that it will actually be used, so rather than poke, prod, and perturb the first screen class some more, let's define a class to handle an individual reel.
public class ReelColumn {
private final Reel reel;
private final Vector2 position;
private final Vector2 windowSize;
private final ReelSymbol[] symbols;
public ReelColumn(
Reel reel,
Vector2 position,
Vector2 windowSize,
Map<String, Texture> symbolToTextureMap
) {
this.reel = reel;
this.position = position;
this.windowSize = windowSize;
this.symbols = new ReelSymbol[reel.getSize()];
float symbolSize = 4;
for (int i = 0; i < reel.getSize(); i++) {
symbols[i] = new ReelSymbol(
position.x, position.y + i * symbolSize, // position
symbolToTextureMap.get(reel.getSymbol(i)),// texture
symbolSize, symbolSize // width x height
);
}
}
...
}
I don't know if I like ReelColumn very much, but I dislike the idea of naming this just Reel
and having to do fully qualified names more. So, putting the name aside, you can see we've got a position which is pretty
standard. But rather than a "size", we've got a windowSize parameter that will define how large the visible
window is to the reel. So, if I want to show 3 symbols, that's a 4 by 12 window.
The first parameter, reel is going to be a reference to one of our slot machines three reels, and
the last parameter is just a simple mapping from the name of the symbol to its texture. I'm not 100% sure if I
care for that part yet, but we'll see about tweaking that later if it becomes a bother. For now, the constructor
simply sets up every single reel item into a long list that extends past the size of our game window, but which is
restricted to the size of the full reel (and will loop when we hit that).
This window size is kind of nice, because it makes our drawing method a lot more efficient:
public void draw(SpriteBatch batch) {
float height = symbols[0].getHeight();
float modulo = symbols.length * height;
for (ReelSymbol reelSymbol : symbols) {
Vector2 symbolLocation = reelSymbol.getPosition();
if (
position.y < symbolLocation.y + height &&
symbolLocation.y < position.y + windowSize.y
) {
reelSymbol.draw(batch, modulo);
}
}
}
Since we've defined the window size, it makes it easy to only call the draw method for the symbol if the symbol is within the visible window. This also means that we don't actually need to pass in that pesky modulo anymore. But, since I haven't removed the other code yet, we'll leave it in until we're ready to toss it out. Static images aren't too exciting, so let's transfer that spin related code into the column:
public void startSpinningReel() {
float maxYForSpinning= symbols.length * symbols[0].getHeight();
float symbolSpinSpeed = 24;
for (ReelSymbol symbol : symbols) {
symbol.setPositionBehavior(
new VerticallySpinningPosition(
symbol.getPosition(),
symbolSpinSpeed,
new Vector2(-1, maxYForSpinning)
)
);
}
}
public void stopReel() {
for (ReelSymbol symbol : symbols) {
symbol.setPositionBehavior(
new StationaryPosition(
symbol.getPosition().x, symbol.getPosition().y
)
);
}
}
Of course, the symbols won't spin unless we update them each frame. But that's just a simple method for our reel to pass along the delta as you'd expect it to:
public void update(float delta) {
for (ReelSymbol symbol : symbols) {
symbol.update(delta);
}
}
Now, before I start ripping things out and replacing it with our new class. Let's make sure it's working. I'll leave my existing reels in their spot and just toss our debug one on the side. In our setup code for the screen, I'll throw a bit of messy code in to get that setup:
SymbolNameMap symbolMap = new SymbolNameMap(
"blank",
"one_bar",
"two_bar",
"three_bar",
"blue_seven",
"white_seven",
"red_seven"
);
slotMachine = new SlotMachine(symbolMap);
...
textures[0] = new Texture(Gdx.files.internal("RedSeven.png"));
...
textures[6] = new Texture(Gdx.files.internal("Blank.png"));
symbolNameToTexture = new Hashtable<>();
symbolNameToTexture.put(symbolMap.getRedSeven(), textures[0]);
...
symbolNameToTexture.put(symbolMap.getBlank(), textures[6]);
testReel = new ReelColumn(
slotMachine.getFirstReel(),
new Vector2(1, 3),
new Vector2(4, 12),
symbolNameToTexture
);
Then, wire up the spinning behavior to take affect when the other reels are triggered within the render method for the first screen:
testReel.update(delta);
testReel.draw(batch);
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
...
testReel.startSpinningReel();
}
if (maybeSpin.isPresent() && maybeSpin.get().isDone()) {
testReel.stopReel();
...
}
And hows it look?
Ah. I'm glad we're fiddling and experimenting. It's helped us come to an important realization! The 64 reel isn't meant to be the visible reel, it's just the odds! I mean, sure, blanks are a thing on a slot machine, but the user shouldn't see how many blanks there are padding and packing around all the other symbols! That'd make them realize how riggered the game is!
If we want this to feel more like a proper slot machine, we need to hide all these blanks away and make it feel like there's a good chance of winning, even if there's not. I'm thinking we could take some inspiration from run-length encodings to solve both of our problems at once. The two problems being the many blanks and symbols that come from the reel list being used as is, as well as the awkward movement of the symbols on the reel moving against the spin direction to get to their destination.
If you're unfamiliar, a run length encoding is a neat trick that can be
used to compress data if its expected that there are many repeated symbols
in a row. Rather than representing a list of six nines like this for example:
[9,9,9,9,9,9], you could install encode it as 69 and
when decoding the data, you read the pair of bytes that make up those numbers
and then construct that list of six nines as you go. Handy, but inefficient if you have
stuff like [1,2,3] where you'd have to represent it as 11,12,13
and you'd take up more space than just the plain data itself.
How does this concept help us? It doesn't! The reverse of it does though! We've got
a whole bunch of data like bar,bar,bar,blank,blank,blueseven... and we
need to transform it into a form that displays nicely to the user. In essence, I want
to read the reel and create an iterator that yields that next non-duplicative symbol
based on the reel probability! You'll see how this fixes our other issue gracefully
in just a moment. Let's get some code going first, I'm excited!
If we apply this idea to the first reel, then we'd end up with the ordering of:
TwoBar Blank ThreeBar Blank WhiteSeven Blank OneBar Blank BlueSeven Blank TwoBar Blank ThreeBar Blank RedSeven Blank ThreeBar Blank TwoBar Blank OneBar Blank
I'm not sure if this holds for the other two reels, but it's kind of nice how compressing the data from the PAR sheet results in a blank between each symbol. Anyway, condensing this is super easy and our iterator is super simple:
public class ReelIterator {
private final Reel reel;
private final List<String> symbolsLoop;
private int nextIdx;
public ReelIterator(Reel reel) {
this.reel = reel;
assert(reel.getSize() != 0);
this.symbolsLoop = new LinkedList<String>();
this.nextIdx = 0;
String symbol = null;
for (int i = 0; i < reel.getSize(); i++) {
String nextSymbol = reel.getSymbol(i);
if (!nextSymbol.equals(symbol)) {
symbol = nextSymbol;
symbolsLoop.add(nextSymbol);
}
}
}
public String peek() {
return symbolsLoop.get(nextIdx);
}
public String next() {
String symbol = symbolsLoop.get(nextIdx);
nextIdx++;
if (nextIdx >= symbolsLoop.size()) {
nextIdx = 0;
}
return symbol;
}
}
Pretty simple right? We can prove it works with a quick unit test as well.
Looping over the iterator twice will result in us making a list that has
duplicative members half the size of itself away. Or, put more mathematically,
i == i + size of compressed list if we pull the full list twice:
class ReelIteratorTest {
private SymbolNameMap symbolNameMap;
private List<String> expectedCondensedReel;
@org.junit.jupiter.api.BeforeEach
void setUp() {
symbolNameMap = new SymbolNameMap(
"_",
"br1",
"bw2",
"bb3",
"7b",
"7w",
"7r"
);
expectedCondensedReel = new ArrayList<>();
... that list from above added in ...
expectedCondensedReel.add(symbolNameMap.getBlank());
}
@Test
public void testCircularNature() {
Reel reel = new FirstReel(symbolNameMap);
ReelIterator iter = new ReelIterator(reel);
int condensedSize = expectedCondensedReel.size();
String[] iteratedTwiceResult = new String[condensedSize * 2];
for (int i = 0; i < condensedSize * 2; i++) {
iteratedTwiceResult[i] = iter.next();
}
for (int i = 0; i < condensedSize; i++) {
assertSame(
iteratedTwiceResult[i],
iteratedTwiceResult[i + condensedSize],
String.format(
"Expected %s to be %s",
iteratedTwiceResult[i],
iteratedTwiceResult[i + condensedSize]
)
);
}
}
}
And if I want to be comprehensive, then we can also add in a test that confirms that peeking at the next symbol doesn't advance the internal cursor along.
@Test
public void peakingDoesNotAdvanceInternalCursor() {
Reel reel = new FirstReel(symbolNameMap);
ReelIterator iter = new ReelIterator(reel);
String nextPeak = iter.peek();
String expectedPeak = expectedCondensedReel.get(0);
assertSame(nextPeak, expectedPeak);
String secondPeak = iter.peek();
expectedPeak = expectedCondensedReel.get(0);
String notExpected = expectedCondensedReel.get(1);
assertSame(secondPeak, expectedPeak);
assertNotEquals(notExpected, secondPeak);
assertNotEquals(notExpected, expectedPeak);
}
Both of these tests happily pass for us. Which means we can move along to the integration step.
Now, why does this iterator solve our problem of pieces moving in the opposite direction
as the reel spin? Simple. They can't move in the wrong direction if they don't exist yet. So let's
refactor the ReelColumn a bit. 10
public class ReelColumn {
private final Reel reel;
private final Vector2 position;
private final Vector2 windowSize;
- private final ReelSymbol[] symbols;
+ private final Deque<ReelSymbol> symbols;
+ private final Map<String, Texture> symbolToTextureMap;
+ private final ReelIterator reelSymbolsToCome;
+ private final float reelSize;
+ private boolean isSpinning;
public ReelColumn(
Reel reel,
Vector2 position,
Vector2 windowSize,
Map<String, Texture> symbolToTextureMap
) {
this.reel = reel;
+ this.reelSymbolsToCome = new ReelIterator(this.reel);
+ this.reelSize = 4;
this.position = position;
this.windowSize = windowSize;
- this.symbols = new ReelSymbol[reel.getSize()];
- float reelSize = 4;
- for (int i = 0; i < reel.getSize(); i++) {
- symbols[i] = new ReelSymbol(
- position.x, position.y + i * reelSize,
- symbolToTextureMap.get(reel.getSymbol(i)),
- reelSize, reelSize
+ this.isSpinning = false;
+ this.symbols = new ArrayDeque<>();
+ this.symbolToTextureMap = symbolToTextureMap;
+ for (int i = 0; i < 4; i++) {
+ addNextSymbolToReel(position.y - reelSize + i * reelSize);
+ }
+ }
I did a little bit of constant clean up, moving the reelSize to be a class level constant
we can use elsewhere. The main thing, besides obviously using the ReelIterator, is that
we aren't generating 64 symbols all at once anymore. Instead, we've only got 4 symbols defined for the
reel at any point in time at the start of the game. When we need a new symbol, we'll ask the generator
we made to help us out. Helping us out will mostly take the form of our new helper method:
public ReelSymbol addNextSymbolToReel(float startingYPosition) {
String symbol = reelSymbolsToCome.next();
ReelSymbol nextReelSymbol = new ReelSymbol(
position.x, startingYPosition,
symbolToTextureMap.get(symbol),
reelSize, reelSize
);
if (isSpinning) {
int speed = 24;
nextReelSymbol.setPositionBehavior(
new MovingAtVelocity(nextReelSymbol.getPosition(), speed)
);
}
symbols.add(nextReelSymbol);
return nextReelSymbol;
}
While I was good about cleaning up that reelSize constant, I've still got the speed
sitting in a local variable. I'll clean that up in a bit, but for now you can see that this code
isn't that different from the code we removed from the constructor. It does however,
take whether we're currently spinning into account in order to set the behavior of the symbol's
position.
And if you're keeping track, this behavior is new! I initially used our vertical looping behavior I
defined before, but found that it was giving me a little bit of trouble since it clamped the edges
and caused the logic I'm about to show you to misbehave a bit. You can probably guess what the
MovingAtVelocity behavior does, but just in case, here's its update function:
public void update(float delta) {
position.y += speed * delta;
}
Yeah. Nothing really worth mentioning there. It's unbounded and symbols can raise themselves to the moon
and back if they'd like to try. Or at least, they would, but our update function for
the ReelColumn is now doing quite a bit more logic than before. As a reminder, it was
just doing a simple loop over the symbols and calling update on each one before.
Now, it's got two main blocks. The first of which, is what it was before, plus a conditional:
public void update(float delta) {
if (!isSpinning) {
for (ReelSymbol reelSymbol : symbols) {
reelSymbol.update(delta);
}
return;
}
...
But, if we ARE spinning. Then we need to generate a symbol if we've just pushed one out of view:
Iterator<ReelSymbol> iter = symbols.descendingIterator();
float minY = Float.MAX_VALUE;
for (Iterator<ReelSymbol> it = iter; it.hasNext(); ) {
ReelSymbol symbol = it.next();
minY = Math.min(minY, symbol.getPosition().y);
symbol.update(delta);
if (symbol.getPosition().y >= position.y + windowSize.y) {
it.remove();
}
}
if (symbols.size() < 4) {
ReelSymbol newSymbol = addNextSymbolToReel(minY - reelSize);
newSymbol.update(delta);
}
}
I will say that the descendingIterator is very intentional. We're
using a Deque
so that we have access to either side of the queue for ease of use. Descending iterators
start at the last element, and iterate forward. Very useful for something we'll do
in a bit. It doesn't matter that much for the sake of this loop, but what does
matter is that if we have less than 4 symbols, we'll spawn a new one one underneath the
bottommost symbol that will then raise up into view.
Since everything is aligned nicely and our UI is pretty much based around the reel size, this results in a pretty seemless experience overall. Though I suppose if I wanted to be completely general and allow for more than 3 symbols to have a spot to land in, I'd need to use the window size of the reel to compute how many symbols are the minimum required. But I'm never planning on having more than 3 show at a time, so I'll be happily ignorning that bit of hardcoding for now.
Since the movement behavior has changed to no longer require clamping down, the
startSpinningReel method also gets a small update. It has to set our
flag value, and also give that initial shove to the reel.
public void startSpinningReel() {
- float maxYForSpinning= symbols.length * symbols[0].getHeight();
- float symbolSpinSpeed = 24;
+ isSpinning = true;
+ float speed = 24;
for (ReelSymbol symbol : symbols) {
symbol.setPositionBehavior(
- new VerticallySpinningPosition(
- symbol.getPosition(),
- symbolSpinSpeed,
- new Vector2(-1, maxYForSpinning)
- )
+ new MovingAtVelocity(symbol.getPosition(), speed)
);
}
}
What it doesn't have to do any more is compute a maximum y to loop a symbol around at
anymore. The update code update (hehe) is now reaping the symbols that
drift too close to the sun and so that's not neccesary anymore. Once again, I'm hardcoding
that speed value, and I'll move it, promise. For now though, the code has become a bit
simpler which is nice! One other piece of code got simpler too:
public void draw(SpriteBatch batch) {
- float height = symbols[0].getHeight();
- float modulo = symbols.length * height;
+ float modulo = symbols.size() * reelSize;
for (ReelSymbol reelSymbol : symbols) {
- Vector2 symbolLocation = reelSymbol.getPosition();
- if (position.y < symbolLocation.y + height && symbolLocation.y < position.y + windowSize.y) {
- reelSymbol.draw(batch, modulo);
- }
+ reelSymbol.draw(batch, modulo);
}
}
Since we're tracking reelSize classwide, I don't have to do an awkward
get height thing anymore. Also, since we don't have a giant list of symbols to
manage, we don't need any of the conditional logic for drawing anymore. I'm pretty
sure I don't even need the modulo argument to draw anymore for the symbol
itself, but let's wrap up the reel logic before we go galavanting on a refactoring
crusade.
The biggest change is the focus of our section here! How do we stop the reel at the right place? Well, we can toss out our old test code that just set the symbols to stationary where they were. Instead, we can take advantage of the fact that we can generate however many symbols we need to, effectively queueing up the symbols we need in order to make the middle row land on the desired outcome. There's 3 steps to this, so let's take it one bit at a time.
public void stopReel() {
isSpinning = false;
float currentBottommostY = Float.MAX_VALUE;
Iterator<ReelSymbol> iter = symbols.iterator();
for (Iterator<ReelSymbol> it = iter; it.hasNext(); ) {
ReelSymbol symbol = it.next();
currentBottommostY = Math.min(currentBottommostY, symbol.getPosition().y);
}
...
Firstly, we need to know where the current bottom symbol is. It could be mid-spin, it could be right at the bottom, newly spawned in, either way, that's the "roof" as it were. Every symbol we need to queue up will go underneath it, and therefore, we need that bottommost Y value computed.
String symbolToLandOn = reel.getCurrentSymbol();
String symbolAdded;
do {
symbolAdded = reelSymbolsToCome.peek();
currentBottommostY -= reelSize;
addNextSymbolToReel(currentBottommostY);
} while (!symbolToLandOn.equals(symbolAdded));
// The last Element added to the symbols is the winner for the reel this spin.
// Since we want it to land in the middle. Add one more.
currentBottommostY -= reelSize;
addNextSymbolToReel(currentBottommostY);
Next up, which symbol do we actually want to land on? The reel can tell
us that with a quick call, but then in order to produce it we need to hit the generator
until we've found it. Given that the symbols are generated based on the reel, there's
no risk of an infinite loop here, and since it's easier to always bubble up the symbol
from below than to try to consider the current symbols being shown to the player, we
can simplify the logic here with a do while loop.
As the comment notes, the desired symbol for the reel should land in the middle of the
machine, so we need to generate the blanks on either side of it by calling the generator
one additional time after we find the symbol we care about. Then, the last step is where
the use of the descendingIterator is important like I alluded to a moment ago.
Since the iterator starts at the last element added, we know we want that one to
land at the start of the visible window on the machine for this reel. So that's 0!
Therefore, every symbol after that should be located one symbol above the next. To get the
reel to land smoothly on those positions, we can set the time we're desiring to be a function
of how many symbols we've now queued up underneath the visible area. I left time declared as
1f and as a seperate variable so that we can tweak it as desired once we try to
make the gamefeel better. Beyond that, the code is simple. We know the lowest reel position
and can just count up in the typical for loop way:
iter = symbols.descendingIterator();
int i = 0;
float time = 1f;
float stopReelWithinSeconds = time * symbols.size();
for (Iterator<ReelSymbol> it = iter; it.hasNext(); i++) {
float stoppingPosition = position.y + i * reelSize;
ReelSymbol symbol = iter.next();
symbol.setPositionBehavior(
new ArrivingAtPosition(
symbol.getPosition(),
new Vector2(position.x, stoppingPosition),
stopReelWithinSeconds
)
);
}
Though I suppose the for loop's signature might look a tad strange. I've got a mix of an iterator
being used as the condition and intialized bit, and then the i++ happening where you
would expect it to be. I could move that into the loop body, but even if it's a little funny looking,
I think it's fine. Amusingly different even.
So. With all that work, what does the test reel on the side of the machine look like now? Spacey is how I'd describe it:
But hey, look how smoothly it stops on that middle row! And yes, if I log out what the desired spin was and what the generator was doing, you can see it's doing exactly what we've written up!
Not bad right? Now all we have to do is replace the 3 middle reels with these real ones backed by the slot machine itself and we'll be one step closer to a functioning slot machine! This is mostly deleting code, basically everything that had to do with looping those reel symbols before gets replaced by a simple call to update, draw, or similar. For example, when we stop the spinner timer:
if (maybeSpin.isPresent() && maybeSpin.get().isDone()) {
- testReel.stopReel();
- Iterator<ReelSymbol> iter = symbolsOnReels.iterator();
- for (int i = 0; i < 3; i++) {
- float x = 8 + i * 6;
- for (int j = 0; j < textures.length; j++) {
- float y = 3 + j * 4;
- ReelSymbol reelSymbol = iter.next();
- reelSymbol.setPositionBehavior(
- new ArrivingAtPosition(
- reelSymbol.getPosition(),
- new Vector2(x, y),
- 1f
- )
- );
- }
- }
+ firstReel.stopReel();
+ secondReel.stopReel();
+ thirdReel.stopReel();
maybeSpin = Optional.empty();
+ int payout = slotMachine.payout();
+ Gdx.app.log("PAYOUT", "$" + payout);
}
It's just a matter of calling stop now! It's nice to throw out those hard coded UI values in the double for loop. Though, we can't escape them entirely. The constructor code still needs to position our reels underneath the mask in the right place. Though it is easier to follow since again, no more for loop:
-symbolsOnReels = new LinkedList<>(); -for (int i = 0; i < 3; i++) { - float x = 8 + i * 6; - for (int j = 0; j < textures.length; j++) { - float y = 3 + j * 4; - symbolsOnReels.add(new ReelSymbol(x, y, textures[j], 4, 4)); - maxYForSpinning = y - 3; - } -} +Vector2 reelWindowSize = new Vector2(4, 12); +float reelBottomLeftY = 3; +firstReel = new ReelColumn( + slotMachine.getFirstReel(), + new Vector2(8, reelBottomLeftY), + reelWindowSize, + symbolNameToTexture +); +secondReel = new ReelColumn( + slotMachine.getSecondReel(), + new Vector2(14, reelBottomLeftY), + reelWindowSize, + symbolNameToTexture +); +thirdReel = new ReelColumn( + slotMachine.getThirdReel(), + new Vector2(20, reelBottomLeftY), + reelWindowSize, + symbolNameToTexture +);
I won't bother noting the draw or update changes, the symbolsOnReels field
has been removed from the class. So anything and everything that was interacting with that
is now gone and we've just got delegated calls out to the ReelColumn. Simple
and easy code. Which is good! Because if we look at our newly spinning spinner, it certainly
invokes a few thoughts about improving it. The good news though, is that it does in fact
work!
Un-aligning those reels at the start would probably make them feel better. Not to mention I'm not entirely sold on the blanks being as large as the other symbols, though that would certainly make the math more difficult to deal with. We could always deal with that by just giving the blanks an actual symbol rather than just a transparent texture. Then the machine would feel a little less empty. But, that's all polish we can do later. For now, we have something much more fundamentally important to accomplish!
How do we bet? ↩
If you're playing a slot machine, you're gambling. And gambling is about tossing your money up in the air and praying more comes down on your head than you threw up. Or, something like that anyway. My point is that we can't actually place a bet yet, and that's kind of fundamentally important to the whole game. This shouldn't actually take too much work to implement I think, we basically need some buttons, some fields to track the inputs, and some display code.
But, before we do that, let's move the code related to the reels over to their own class.
It's all sitting in FirstScreen right now. Which is basically our staging area
for ideas and testing. Since we've got the three reels spinning and working as desired, moving
it out of the way to clear the plate should make handling the buttons and such a bit simpler
because we won't have the other code in the way to distract us.
I'll call the class ReelsPanel since it's really just focused on the three column
panel that spins for the reels. It's not the entire slot machine itself, so we'll save that name
for the final aggregation of all the various parts. For now, we'll just do a mechanical extraction
of the code from the testing area. Since none of that code is changing anything besides location,
I'll be omitting most of the details beyond some skeleton notes:
public class ReelsPanel {
private SlotMachine slotMachine;
private ReelColumn firstReel;
private ReelColumn secondReel;
private ReelColumn thirdReel;
Texture machineMask;
Optional<TimedAccumulator> maybeSpin;
public ReelsPanel(
SlotMachine slotMachine,
Map<String, Texture> symbolNameToTexture,
Texture machineMask
) {
... all the setup code for the reel columns
... and initial spin accumulator Option
}
public void update(float delta) {
... all the calls to update
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
... start spinning code
}
if (maybeSpin.isPresent() && maybeSpin.get().isDone()) {
... stop spinning code
}
}
public void draw(SpriteBatch batch) {
firstReel.draw(batch);
secondReel.draw(batch);
thirdReel.draw(batch);
batch.draw(machineMask, 0, 0, 32, 18);
}
}
One thing I do want to call attention to is that we've pushed the Gdx.input
handling code into here. This is partially from neccesity of the mechanical refactoring
we just did. But also, this also suggests some helper methods for us if we want to stop
that. If we create some helper methods to call, then we can lift the input handling up
to the calling class. Or, if we don't want to do that, we could make the code a little
bit more robust by creating an input handler that takes the ReelsPanel as
an argument that would handle calling the helper methods for us.
Both options are better than directly calling the Gdx global input handler I think. But, before we do that, let me just finish up the refactor by changing the calling code in our prototype screen to use the new class. Since most of those class level fields were only needed from the reels panel, we can nix quite a few:
public class FirstScreen implements Screen {
OrthographicCamera camera;
Viewport viewport;
Map<String, Texture> symbolNameToTexture;
- Texture machineMask;
- Optional<TimedAccumulator> maybeSpin;
SlotMachine slotMachine;
- private ReelColumn firstReel;
- private ReelColumn secondReel;
- private ReelColumn thirdReel;
+ private ReelsPanel reelsPanel;
...
The machine mask becomes a local variable on the initial setup done by the
show method.
@Override
public void show() {
... camera setup, symbol name definitions as before ...
Texture machineMask = new Texture(Gdx.files.internal("MachineMaskMini.png"));
reelsPanel = new ReelsPanel(
slotMachine,
symbolNameToTexture,
machineMask
);
}
And the render code becomes drastically simpler since now all almost all
the processing lives in the ReelsPanel class:
@Override
public void render(float delta) {
ScreenUtils.clear(Color.BLACK);
camera.update();
reelsPanel.update(delta);
batch.setProjectionMatrix(camera.combined);
batch.begin();
reelsPanel.draw(batch);
batch.end();
if (Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) {
Gdx.app.exit();
}
}
Just like in other instances of this across our games, when we define a draw method for
a UI component class like this, we'll always begin and end the batch around it11.
And as I noted before, the input handling has pretty much all disappeared into the ReelsPanel.
While I am already distracting us from the whole "make betting work" thing, and
I should probably get back to that. Lifting up the code into helpers and calling it here
is trivial work if we don't create an input handler. So if extract the body of the if
statement we had in the panel to a helper named startSpinning then we can
update the screen code to handle that like so:
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
reelsPanel.startSpinning();
}
Easy. And for balance, I'll also create a stopSpinning method. This won't
be called from the screen, but rather internally since it depends on the spin accumulator
reaching its target value. I still think it feels nice to have both a start and stop
helper method though because as a developer if you see "start" you expect to see "stop"
too!
Ahem. Code aesthetics aside. Let's get back to the point now that we've finished our refactoring. How do we bet!? Heck, do we have a game over mechanic where the user loses all their money? This is a toy game of sorts, so it's not like someone's going to micro transact their way in here, but also there's not really much of a game to just saying "spin the wheel and see what you get"… actually, well, I suppose pachinko is exactly that. Hm. Let's set that aside, dopamine machine needs buttons I say!
If we look the tales of symphonia slot machine, I see something I like.
Bet one, bet max, spin. Within the game this increases your bet by one each time you press the button, or gives you a maximum bet amount depending on the machine. There's the penny machine with a range of 1 - 5, a 10 - 50, and of course, a 100 - 500 machine. I don't think we need to offer more than one "machine" for our game. We can just do something similar and simple to enjoy things. Let's roll with the simple 1 to 5 range for our betting range and construct the three buttons to control the amount. 12
So let's whip up a couple button assets real quick. I'm thinking that our layout could be something like this. We'll make the bet buttons the same size, but the spin button will be larger so that we can slap a big "SPIN" call out on it.
As you can see, I want to put the buttons on the left side of the machine rather than on the bottom. There's not really a ton of space underneath the reels, so I don't think we can copy the symphonia or the RWB machine we've based our probabilities on. But that's okay, we're making our game! So, to the left it goes!
If we do a bit of math based on the gridlines shown above, then our bet buttons will be 300px tall and 500px wide. In the game world space that will just translate over to 3x5, and then the spin button will be 5x5:
As you can see, I'm an artistic genius. 13
I'm trying to maintain the "red white and blue" theme, and that sort of newgrounds, armor game, hand-made flash game sort of feel to this. Or I'm lazy. You decide! The spin button will be a square and look like this:
I re-iterate. Genius. 14
Tossing these into place to render is pretty easy to do. Just adding the textures as class wide fields will let us get a quick feel for if the sizes feel good or not:
public void show() {
...
bet1Btn = new Texture(Gdx.files.internal("Bet1.png"));
betMaxBtn = new Texture(Gdx.files.internal("BetMax.png"));
spinBtn = new Texture(Gdx.files.internal("spin.png"));
...
}
public void render(float delta) {
... within the batch.begin area ...
batch.draw(bet1Btn, 1, 14, 5, 3);
batch.draw(betMaxBtn, 1, 10, 5, 3);
batch.draw(spinBtn, 1, 4, 5, 5);
...
}
And this results in something that, actually, looks kind of okay I think! Layout-wise.
We can use that remaining area at the bottom to display the actual chosen bet amount
I think. Now, I could declare a BitmapFont, and so long as it's
not a free type loaded font, it would work as expected in both HTML and Java targets.
But I think a sterile bitmap font won't match with our "style" here. So, let's do
something a bit more fun.
In order to display how much the current bet is, we'll need a way to show the digits 1 through 5. To display the results when the user is actually betting, spinning, and paying out money to the user, we'll want the rest of the digits too. So, what if we write up a little sprite sheet? Then we can split the texture up into ten parts:
Texture allDigits = new Texture(Gdx.files.internal("digits.png"));
TextureRegion[][] allRowsAndColumns = TextureRegion.split(allDigits, 300, 200);
digits = new TextureRegion[10]; // class level field
int d = 0;
for (int i = 0; i < allRowsAndColumns.length; i++) {
for (int j = 0; j < allRowsAndColumns[i].length; j++) {
digits[d] = allRowsAndColumns[i][j];
d++;
}
}
Which we can then render underneath the spin button. For the sake of showing you all the digits, I'm just rendering them one at a time here for you to see, rather than including the image itself here. You might sense that I am continuing to follow the theme we've established:
Though I admit, the red is QUITE red. But hey. This is (now) an ethical slot machine, therefore it's trying to make sure you don't play for too long. Obviously. That's why the UI is so eye blisteringly good. Definitely. This has nothing to do with laziness or lack of artistic talent.
Ahem. So, if I've got a sprite sheet for numbers, it'd be nice to make it easy to display I think. And thus, let's make a nice little utility, shall we? Now, I first started thinking about the classic problem of "How do I turn 123 into 1, 2, and 3?" the math way. We could modulo the "place" of the digit by 10 times how far it is from the 0th place, but we'd have to contend with subtracting out the accumulating values a bit in order to only take the value in the given place.
Or we could be dumber about it and not have to think about math at all because we're working in a high level language that can make this easy in one simple trick.
Just turn it into a string:
public class NumberRenderer {
private final TextureRegion[] digits;
public NumberRenderer(Texture digitSpriteSheet) {
TextureRegion[][] allRowsAndColumns = TextureRegion.split(
digitSpriteSheet, // texture to split
digitSpriteSheet.getWidth(), // into x wide
digitSpriteSheet.getHeight()/10 // y tall individual textures
);
digits = new TextureRegion[10];
int d = 0;
for (int i = 0; i < allRowsAndColumns.length; i++) {
for (int j = 0; j < allRowsAndColumns[i].length; j++) {
digits[d] = allRowsAndColumns[i][j];
d++;
}
}
}
public void draw(SpriteBatch batch, int number, float x, float y) {
// No negatives. Ever. Crash if we see one.
assert (number >= 0);
char[] characters = String.valueOf(number).toCharArray();
int i = 0;
for (char c : characters) {
int digit = c - 48;
// % 2 because spacing on our font digits is WIDE
// but I don't want to change the width of the tile
// since I like it centered.
batch.draw(digits[digit], x + (float) (i * 3) /2, y, 3, 2);
i++;
}
}
}
Using this to temporarily draw out a large number, you can see this works pretty well:
numberRenderer.draw(batch, 1234567890, 8, 1);
We're a fourth of the way to victory! Now we need to make the buttons actually work. This isn't that difficult, as I've made quite a few button classes over the course of these game projects. Like the toggle buttons and friends in the match 3 post. They all follow a pretty basic structure and state transition. The tricky part is how close to the "sun" that is LibGDX's internals do we bind these buttons. Setting up a way to replace the plain textures for the buttons with a wrapper is easy:
public class ClickableButton {
private final Texture btnTexture;
private PositionBehavior positionBehavior;
private final float width;
private final float height;
public ClickableButton(Texture btnTexture, float x, float y, float width, float height) {
this.btnTexture = btnTexture;
this.positionBehavior = new StationaryPosition(x, y);
this.width = width;
this.height = height;
}
public void draw(SpriteBatch batch) {
Vector2 p = positionBehavior.getPosition();
batch.draw(btnTexture, p.x, p.y, width, height);
}
public void update(float delta) {
positionBehavior.update(delta);
}
}
But how do we want to setup the event handling? Do we want the class to inherit the appropriate
InputProcessor and then
have the scene setup a multiplexer? Do we want to use the observer pattern to broadcast when a
click has happened back out from the button somewhere else? Do we want to have a list of listeners,
or do we want the inverse and poll for the state of the button. So many choices to think about.
Let's do something new. Let us PLEX. Okay that makes no sense, but that's only because I paused writing to do some coding real quick to explore. Buttons are one of those things that I find hard to write in small parts, you kind of need to get it hooked up to get a feel for how it is to use. Like, if you don't like the way you start to register, delegate, and use the button then you can end up with something you really don't like.
That said, buttons and the observer pattern go together like ham and cheese. And as per usual, the secret to making them work is the interface:
public interface ButtonSubscriber {
public void onClick(ClickableButton clickableButton);
}
Any sort of thing that cares about buttons is going to be a subscriber. For the time being,
we really only expect a click. No drags, holds, or anything of that sort. Nope, only clicks.
The ClickableButton will pass itself to its subscriber so that it can figure
out anything it might need to. For example, if you had 1 subscriber for many buttons, you
might need to check which one was clicked.
To track this we'll modify the ClickableButton to track its subscribers.
public class ClickableButton {
...
private final List<ButtonSubscriber> subscribers;
public ClickableButton(...) {
...
this.subscribers = new LinkedList<>();
}
public void addSubscriber(ButtonSubscriber buttonSubscriber) {
subscribers.add(buttonSubscriber);
}
...
}
You might be asking yourself, cool an add method. We talked about balance before, so there should be a remove one too.
Well, yes. There should be. And if I had one, I'd probably call it in the dispose
method of the code that's handling the buttons. But, this is a one screen game. The user shuts off the
game when they're done with it, and as far as I know, we're not going to be calling the show
method that sets up everything more than once anyway. If we were targetting mobile devices where a user
may swap back and forth between applications often and we have to be careful about state, then sure, we
should probably do that.
We're targeting the desktop and a stretch goal of Web via HTML.
So a button has a list of subscribers, but in order for up to have that mean anything we need to have a way to trigger them. A while ago I saw a John Carmack tweet about how you should trigger a click event on the down click of a button if you want a reactive feeling UI, and many games and interfaces perferm the event on the release of a button instead. I agree with this idea. But also, on a computer game like this, let's start things super simple by sending it on the release. 15
public void down() {
// TODO!
}
public void up() {
for (ButtonSubscriber subscriber : subscribers) {
subscriber.onClick(this);
}
}
Now, we can declare all these buttons somewhere, like the prototype FirstScreen
class, but how would we know when to actually call these methods at all? Obviously we could
use Gdx.input.* but that code is going to bloat up real fast since we've got a
few buttons to show. Let's make things simple by creating a helpful method to tell if a given
point is within the button (obstacle collision! But easy!)
public boolean isPointInside(float x, float y) {
Vector2 p = positionBehavior.getPosition();
boolean inX = p.x <= x && x <= p.x + width;
boolean inY = p.y <= y && y <= p.y + height;
return inX && inY;
}
We've written and used this code many times before, and so you might know what's coming next. But you're wrong! Ok, well, probably not, but I did come up with a new way of doing the input processing that, at the moment, I think is kind of nice. Behold, the PLEX!
public class ButtonMultiplexer implements InputProcessor {
private final Camera camera;
List<ClickableButton> buttons;
/* Camera is required to convert screen coords to world space */
public ButtonMultiplexer(Camera camera) {
this.camera = camera;
buttons = new LinkedList<>();
}
public void addButton(ClickableButton button) {
buttons.add(button);
}
... empty interface methods for key handling ...
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (btn.isPointInside(worldXYZ.x, worldXYZ.y)) {
btn.down();
touched = true;
}
}
return touched;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (btn.isPointInside(worldXYZ.x, worldXYZ.y)) {
btn.up();
touched = true;
}
}
return touched;
}
public void update(float delta) {
for (ClickableButton button : buttons) {
button.update(delta);
}
}
}
You might be thinking, how is anything different than usual? And to that, the usage of our new class:
btnPlexer = new ButtonMultiplexer(camera); btnPlexer.addButton(bet1Btn); btnPlexer.addButton(betMaxBtn); btnPlexer.addButton(spinBtn); Gdx.input.setInputProcessor(btnPlexer);
Does look quite similar to how we setup button listeners during other games like the match 3 one
backButton.addButtonListener(this); bgmVolumeControl.addButtonListener(this); sfxVolumeControl.addButtonListener(this);
And just as the button muliplexer is an input processor, so too was the old MenuInputAdapter
from the other games:
menuInputAdapter = new MenuInputAdapter(viewport); menuInputAdapter.addSubscriber(backButton); menuInputAdapter.addSubscriber(difficultyToggle); menuInputAdapter.addSubscriber(bgmVolumeControl); menuInputAdapter.addSubscriber(sfxVolumeControl);
So nothing has really changed that. I just called it a different name really. But I still like the button multiplexer. I suppose when a pattern works, one just goes back to it! So, what about the actions each button takes? The match 3 game's configuration menu's code looked like this:
@Override
public void buttonClicked(MenuButton menuButton) {
if (menuButton == sfxVolumeControl) {
GameSettings.getInstance().setSfxVolume(sfxVolumeControl.getVolume());
}
if (menuButton == bgmVolumeControl) {
GameSettings.getInstance().setBgmVolume(bgmVolumeControl.getVolume());
match3Game.match3Assets.getBGM().setVolume(bgmVolumeControl.getVolume());
}
if (menuButton == difficultyToggle) {
GameDifficulty changeTo = GameDifficulty.NORMAL;
if (difficultyToggle.isToggled()) {
changeTo = GameDifficulty.EASY;
}
GameSettings.getInstance().setDifficult(changeTo);
}
if (menuButton == backButton) {
match3Game.closeOverlaidScene();
}
}
But for our new game's code? Well. I've just got a few closures at the moment for prototyping purposes:
bet1Btn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
bet +=1;
if (bet > 5) bet = 1;
}
});
betMaxBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
bet = 5;
}
});
spinBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
reelsPanel.startSpinning();
}
});
They're not that different. It's more just a matter of what delegates to what. I'll probably tweak things a bit, but this a decent enough prototype to let me make the system set the bet and spin which is basically almost the whole game! The concerns of these subscribers also suggest, at least somewhat, that we could probably encapsulate the bet amount into a small helper subscriber, and if we wanted to, we could probably have the reels panel subscribe to the button itself.
That refactoring can come later, because, after adding in btnPlexer.update(delta);
to the render method in the screen, all the buttons are now eagerly awaiting an update.
And the position behavior for each button is what gets called by the update method. This allows
us to make the buttons react to the clicks in an obvious way. I'd like to make the buttons depress
themselves on click, and it'd be fun to make them wiggle or float a bit when you hover over them.
But let's do clicks first. A quick test to make sure it works with some RNG will be good:
...
private final Vector2 initialPosition;
public ClickableButton(Texture btnTexture, float x, float y, float width, float height) {
this.initialPosition = new Vector2(x, y);
...
}
public void down() {
positionBehavior = new StationaryPosition(
initialPosition.x + MathUtils.random(),
initialPosition.y + MathUtils.random()
);
}
public void up() {
positionBehavior = new StationaryPosition(
initialPosition.x,
initialPosition.y
);
for (ButtonSubscriber subscriber : subscribers) {
subscriber.onClick(this);
}
}
Obviously this isn't final and isn't something we'd ship out. But its is kind of amusing:
Most importantly though, it shows that these buttons work! So. We'll leave this as is for now and polish them later. We have one more important piece of the "Game" to finish before we do. The only other question for what we should about these buttons is if we should move them to a management container of some kind, like we did for the reels panel. Just like before, it makes life easier to prototype when there's not extra stuff in front of us, so let's go ahead and move those buttons.
Two of the buttons are concerned with the bet amount, so that can definitely be contained within their new home. But the spin button is using the reel panels and makes me pause. I don't really want to pass the reel panels in as a constructor argument. It just feels wrong to pass in something for the sole purpose of calling it from within a hook from an internal event. It feels like it would be better to emit the event and then deal with it. So, would our interface that the screen is going to care about something about the bet and a hook for when the spin button is triggered or something?
Hm. It's a thought to chew on. This morning I was thinking about how to try to keep as much in pure java as possible, without framework code, including events and those kinds of things. It makes me wonder a little bit if our slot machine (the pure java code with the reels and such) should be the thing that emits all the events and then the UI code just has to get hooked in? Hm. Hm. Hmm.
I think perhaps if I can't quite cleanly cut out this part, perhaps we leave it where it is for the time being until the delineations materialize in front of me a bit better. So. Let's move right along. Like I said, we've got one more important piece of the game logic to get set up before we can polish and tweak.
How do we get paid? ↩
We've already taken care of the trouble of how to display numbers. So showing the accumulating results of our efforts isn't too bad. I guess we should start with a certain amount of money and then see how far the user can get. Game over can happen when you're out of money or kidneys to sell I guess. I'm joking, but that would kind of be a funny idea for a game. At least then game over would definitely be well defined.
Anyway, let's make a wallet! I'm going to add this to the core game logic package since it really has nothing to do with the ui or anything libgdx related at all.
package spare.peetseater.games.slots.core;
public class Wallet {
private int funds;
public Wallet(int initial) {
funds = initial;
}
public void awardAmount(int toAdd) {
funds += toAdd;
}
public boolean hasEnoughToBet(int amount) {
return funds >= amount;
}
public void subtractAmount(int amount) {
funds -= amount;
// Unlike real life. There is no debt here.
if (funds < 0) {
funds = 0;
}
}
public boolean isBroke() {
return funds <= 0;
}
public int getFunds() {
return funds;
}
}
For now we won't let the funds dip below 0, this house is generous enough that if you glitch past whatever guard rails we put in place for checking that you have enough money to bet, we won't try to break your kneecaps or some other mobster trope. For those checks, we can tweak and alter the existing button subscriber methods if we want to get something working quickly:
wallet = new Wallet(100);
bet1Btn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
if (!wallet.hasEnoughToBet(bet + 1)) {
return;
}
bet +=1;
if (bet > 5) bet = 1;
}
});
betMaxBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
if (!wallet.hasEnoughToBet(5)) {
bet = wallet.getFunds();
} else {
bet = 5;
}
}
});
This gives us our constraints easily enough. But we need to spend the money when we bet, and once the reels of fate stop spinning, award any money we win. Also... we should display this number! Otherwise how is the user supposed to know how much money they have? Well. I have an idea about that! First, the easy step:
numberRenderer.draw(batch, wallet.getFunds(), 25.5f, 0);
Drawing the amount over on the right hand side. I updated the mask to put a little grey block at the bottom of the screen so the text isn't just floating in thin air.
But now for the fun part! Let's make some coins.
I figure we can make the right hand side of the screen a bit more interesting by visually showing the money we have left, and that we're gaining, as a little pile of coins. Now, obviously we're not going to do a whole physics simuation or anything crazy like that over there, but it should make for an amusing thing to display to show a bunch of coins falling down from the sky as we win our bets and such.
We could loop over a certain amount and display the coins in a sterile, mechanically placed fashion and end up with something like this:
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 20; j++) {
batch.draw(coinTexture, 26 + i, j * 0.2f, 1, 0.2f);
}
}
numberRenderer.draw(batch, wallet.getFunds(), 25.5f, 0);
But that's no good. No, we can do better by making a little UI manager that places the coins randomly down for us. Before we go all in on the idea of falling coins from the sky, let's just get some coins sitting in place without any of the frills. I think the simplest way to think about this is that we should have stacks of coins. Then, we can generate a few positions where we'll place those stacks, and then randomly add coins to each one.
class CoinStack {
private final Texture coinTexture;
private float topPosition;
private final float coinHeight;
private final float x;
private final float y;
public CoinStack(Texture coinTexture, float x, float y, float coinHeight) {
this.x = x;
this.y = y;
this.topPosition = y;
this.coinHeight = coinHeight;
this.coinTexture = coinTexture;
}
public boolean hasCoins() {
return this.y != topPosition;
}
public void addCoin() {
this.topPosition += coinHeight;
}
public void subtractCoin() {
this.topPosition -= coinHeight;
if (this.topPosition < y) {
this.topPosition = y;
}
}
public void draw(SpriteBatch batch) {
for (float i = y; i < topPosition; i += coinHeight) {
batch.draw(coinTexture, x, i, 1, coinHeight);
}
}
}
The majority of this is boilerplate that shouldn't be too surprising. I think the concept
to understand here, is that for now, we'll represent the stack of coins by its maximum
height. This means that we're not actually tracking an individual coin entity, but just where
we should be stacking them up for display. Hence why hasCoins is a check against
the anchor point of the stack and the where the topmost coin should be drawn.
We can build on these stacks (ha) by creating a class that will set up those positions for us within a given range and then delegate down to each as needed.
public class CoinStacksDisplay {
private final Vector2 xRange;
private final Vector2 yRange;
private static final float coinHeight = 0.2f;
public List<CoinStack> stacks;
public CoinStacksDisplay(Texture coinTexture, Vector2 xRange, Vector2 yRange) {
this.xRange = xRange;
this.yRange = yRange;
this.stacks = new LinkedList<>();
float xStep = 0.5f;
assert(xRange.x < xRange.y);
assert(yRange.x < yRange.y);
for (float x = xRange.x; x < xRange.y; x+= xStep) {
float yStart = yRange.x; // we're just treating the vector2 as a tuple
stacks.add(new CoinStack(coinTexture, x, yStart, coinHeight));
}
assert(!stacks.isEmpty());
}
While we're working on this and the numbers are fresh, the should be a list of stacks
generated and made as expected. But it's a smart idea to guard against silliness from
future us, and so I've included a number of assert statements here to make
sure programmer error crashes the system instantly. With those invariants covered, we
can rely on randomness to do the operations for the stacks:
public void addCoins(int numberOfCoins) {
for (int i = 0; i < numberOfCoins; i++) {
CoinStack stack = stacks.get(MathUtils.random(0, stacks.size() - 1));
stack.addCoin();
}
}
public void removeCoins(int coinsToTake) {
for (int i = 0; i < coinsToTake; i++) {
// Note that this likely will get inefficient in the long term.
// We will deal with it another time.
List<CoinStack> stacksWithCoins = stacks
.stream()
.filter(CoinStack::hasCoins)
.collect(Collectors.toList());
if (stacksWithCoins.isEmpty()) {
return;
}
CoinStack stack = stacksWithCoins.get(MathUtils.random(0, stacksWithCoins.size() - 1));
stack.subtractCoin();
}
}
public void draw(SpriteBatch batch) {
for (CoinStack stack : stacks) {
stack.draw(batch);
}
}
}
We don't have to worry about any IndexOutOfBoundsExceptions since we're always
guaranteed to have at least 1 stack in our list. We don't have a guarantee that we won't run
out of coins, but since we only ever remove coins form stacks with coins on them, the only
real problem is we're not bothering to verify that a stack has enough coins to remove if we
were to try to remove the full amount at once. But we remove coins one at a time, and while
this does mean that for N coins we loop over the stacks N times, in practice we really don't
have huge numbers here. So I'm not overly concerns about performance hits here until we
actually see a problem.
Of course, we're not seeing a problem or uh, whatever the opposite of a problem that isn't a solution is… that is to say: we're not drawing any stacks yet. So let's go ahead and call our fat stacks of coins from the screen class:
public void show() {
...
wallet = new Wallet(100);
coinTexture = new Texture(Gdx.files.internal("gold-coin.png"));
coinStacks = new CoinStacksDisplay(coinTexture, new Vector2(25, 32), new Vector2(0, 18));
coinStacks.addCoins(wallet.getFunds());
...
spinBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
if (!wallet.hasEnoughToBet(bet)) {
// NO! TODO: Do some sort of interaction to show they can't
return;
}
wallet.subtractAmount(bet);
coinStacks.removeCoins(bet);
reelsPanel.startSpinning();
}
});
...
}
And with the addition of coinStacks.draw(batch); to the render method,
we can see:
That looks pretty good! And it's random each time so it won't ever feel stale. As you can see from the code above, we've got the subtraction of coins from the pile implemented already whenever the user spins and their wallet amount decrements. But we're not dealing with any addition of coins to the pile.
To get that working, we need to be able to tell when the spinning has stopped and it's time to payout to the user anything they've won. So, let's setup the reels panel to emit an event for that. Unlike the click button, I don't think there's much point in passing anything for this event, so our interface will be simple:
public interface ReelsSubscriber {
void onSpinComplete();
}
Then, after adding in an addSubscriber button and subscribers list field to the
ReelsPanel, we can update its code to let anyone who might care that the reel
has stopped spinning:
public void stopSpinning() {
firstReel.stopReel();
secondReel.stopReel();
thirdReel.stopReel();
maybeSpin = Optional.empty();
for (ReelsSubscriber subscriber : subscribers) {
subscriber.onSpinComplete();
}
}
With that bit of construction done, we can update both our wallet and coin stacks with the payout once the reel has finished spinning inside of the screen class:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
wallet.awardAmount(bet * slotMachine.payout());
coinStacks.addCoins(bet * slotMachine.payout());
}
});
Then, I played a little bit. Verifying things work by carefully not pressing the button more than once per spin 16, and after a little while (4 spins) I got a win of 10 coins!
Look at that, it works! We've officially been paid! Totally ready to ship this off to steam now, right? Call this long blog post done and turn in for the night? Play some pokemon or continue one of my Let's plays?. Nah. We've got some important things to fix up before we can call this one done. Unlike the previous game we did, where my motivation was slain by the trials of obstacle collision, the tank isn't empty yet. And most importantly, we've got some polishing to do.
User experience: buttons ↩
As I've alluded to above, the buttons have an undesirable behavior at the moment. Observe the game as I click on the button:
See how I'm spamming the button? And the little pile of gold is rapidly decreasing? Well, I'm not just losing over and over, I'm just feeding money into the machine for it to eat without paying me back. The payout happens at the end of the spin, and since I never give the game a chance to stop spinning, we get nothing.
Well, we get a terrible user experience, but that's not really what we want. So, let's improve this by improving the buttons to track their state a bit better and avoid the problem I just showed. Let's start by making the buttons disable-able:
public class ClickableButton {
...
private boolean isDisabled;
public ClickableButton(...) {
...
this.isDisabled = false;
}
...
public void up() {
positionBehavior = new StationaryPosition(initialPosition.x, initialPosition.y);
if (isDisabled) {
return;
}
for (ButtonSubscriber subscriber : subscribers) {
subscriber.onClick(this);
}
}
... getter/setter for isDisabed ...
}
We're not getting fancy here, we don't need an extra object to track the button's state as a seperate object for now. A simple boolean will suffice I think. With that created, we can setup the guard rails to prevent multiple presses once the reels start spinning:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
wallet.awardAmount(bet * slotMachine.payout());
coinStacks.addCoins(bet * slotMachine.payout());
spinBtn.setDisabled(false);
}
});
...
spinBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
if (!wallet.hasEnoughToBet(bet)) {
// NO! Do some sort of interaction to show they can't
return;
}
spinBtn.setDisabled(true);
wallet.subtractAmount(bet);
coinStacks.removeCoins(bet);
reelsPanel.startSpinning();
}
});
Great! Now we can spam the button without losing money! 17
That said, I think it's time to improve the button experience a bit more. Let's stop the random jumping around of the button we're doing and put in some less screwy behavior. The huge jumps that it takes right now can actually move the button so that your mouse is no longer positioned over it anymore, and then that leads to the interesting state where your up click doesn't register on the button. This results in the button getting stuck in the "lifted" position, which is no good. As a reminder, this is the random movement code for the button whenever we click it:
public void down() {
positionBehavior = new StationaryPosition(
initialPosition.x + MathUtils.random(),
initialPosition.y + MathUtils.random()
);
}
Let's do something a bit better. Changing the size of the button will work I think. And best of all, we can control this with one additional field:
public class ClickableButton {
...
private float offset;
public ClickableButton(...) {
this.offset = 0;
...
}
public void draw(SpriteBatch batch) {
Vector2 p = positionBehavior.getPosition();
batch.draw(
btnTexture,
p.x - offset,
p.y - offset,
width + offset*2,
height + offset*2
);
}
public void down() {
offset = 0.25f;
}
public void up() {
offset = 0;
positionBehavior = new StationaryPosition(initialPosition.x, initialPosition.y);
if (isDisabled) {
return;
}
for (ButtonSubscriber subscriber : subscribers) {
subscriber.onClick(this);
}
}
}
This works pretty well. The button expands outward from its center (or so it appears), which means that the user can't move their mouse outside of the button when clicking normally. They can certainly hold onto the button and move it away if they want to cause some trouble, but the important case of the average user who clicks on a button like, a, well, your average normal human being, is covered here which is a plus.
Your non-average human being who's trying to cancel a button press while they're in the middle of it, will be treated to this:
So, one: do we fix this, and two: how do we fix this. Part of me doesn't want to bother. Like, it doesn't impact functionality of the game after all. But it is really odd for the button to stay in its click-in-progress-mode. So, if we were to fix it, I believe we've need two new methods and some state in the button. But on the caller side within the multiplexer it would look something like this:
@Override
public boolean mouseMoved(int screenX, int screenY) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (!btn.isPointInside(worldXYZ.x, worldXYZ.y) && btn.isHeld()) {
btn.resetClick();
}
}
return touched;
}
The two highlighted methods don't exist. But they could! Given that it's just a visual thing
based on that offset we're using to expand the texture, we can tell that a user
has clicked in the button based on that:
public boolean isHeld() {
return offset != 0;
}
public void resetClick() {
offset = 0;
}
This is enough to make the previous mouseMoved handler work as expected.
Though, it's still not enough. If you click in a button, move the mouse outside of the
button and then don't move while you release. The button stays up. So, there's one more
tweak we need:
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (btn.isPointInside(worldXYZ.x, worldXYZ.y)) {
btn.up();
touched = true;
} else {
btn.resetClick();
}
}
return touched;
}
We already had the isPointInside check here, but not the else. This fixes
our issue though. If you release the mouse button while you're not inside, then the click
will reset and the up handler will never be called. Easy.
This resolves the issue I had noted before, but there's still one more thing I'd like to tweak about the buttons to make things feel better. Right now, if you click the button it reacts. But if you just move your mouse over it? Nothing. I feel like if we're trying to entice people to play a slot machine, the flashy light reactive nature of it needs to be dialed up a bit. So, let's make the button do something when we hover over it.
My first thought is to make it float up and down. So, I tweaked the code ever so slightly:
// inside the button:
public void draw(SpriteBatch batch) {
Vector2 p = positionBehavior.getPosition();
float y = p.y;
if (isHovering) {
y = p.y + MathUtils.sin(MathUtils.lerp(0, 1f, accum)) * 0.1f;
}
batch.draw(btnTexture, p.x - offset, y - offset, width + offset*2, height + offset*2);
}
public void hover() {
isHovering = true;
}
public void stopHover() {
isHovering = false;
}
// inside the plexer
@Override
public boolean mouseMoved(int screenX, int screenY) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (!btn.isPointInside(worldXYZ.x, worldXYZ.y) && btn.isHeld()) {
btn.resetClick();
} else if (btn.isPointInside(worldXYZ.x, worldXYZ.y)) {
btn.hover();
} else {
btn.stopHover();
}
}
return touched;
}
Then, hoving my mouse over the button:
It's not… flashy enough? I think, rather than that watery slow-mo bounce. Maybe we try making it do what it does when you click it, but pulsating. That might look better. The button moving up and down reveals the gray mask background behind it, as well as, potentially making it harder to click if you're near an edge. I think a pulsating size would avoid some of that trouble and look good.
float dynamicOffset = 0;
if (isHovering) {
dynamicOffset = MathUtils.lerp(0.1f, 0.25f, (float)Math.abs(Math.sin(accum)));
}
batch.draw(
btnTexture,
p.x - offset - dynamicOffset,
y - offset - dynamicOffset,
width + (offset+dynamicOffset)*2,
height + (offset+dynamicOffset)*2
);
And this looks better but also, there's sort of something weird going on:
Initially, I thought the bug might be because I was starting the lerp at 0.1f
but swapping it over to 0 didn't change the weird white flicker that only happens on the
two bet buttons. Since it only happens on the bet buttons, I took the next logical step.
Checking the asset itself:
Ah. That would do it. Removing the thin white line around the edges of those two buttons clears that out and the thing pulses as I wanted it to. Great! Well I'm happy with the button experience, let's move on to the next bit of polish!
User experience: payouts↩
Right now when you win, the coins instantly appear over on the right. There's nothing that makes it obvious you were paid, there's no indicator of how much you just won unless you were keeping track and doing some math. It's just not a good experience for our gambling users who want that dopamine hit.
So there's a couple things I think we can do to improve the experience!
- Make it rain
- Congratulate on how many coins you earned
- ???
- Profit
Both of these improvements can leverage the onSpinComplete event
to trigger. Refreshing our memory, this is what we currently have hooked into
that:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
wallet.awardAmount(bet * slotMachine.payout());
coinStacks.addCoins(bet * slotMachine.payout());
spinBtn.setDisabled(false);
}
});
Okay, looking good 18. For our first trick we can let the coin stack be the one responsible for making the coins rain down from the top to the bottom. That should help isolate the code for us on that. The wallet award is just a pure data tracker, not anything to do with the UI, so we'll need to create something new for the display of the payout beyond the coins falling down.
The pattern we've been using for these things is so straightforward, I can write the calling code before I make the actual implementation. Which, honestly, isn't always the worst method. It can sometimes be really handy to write non-compiling code that gets you a feel for how something will look from the caller perspective before you make the code itself. This also tends to raise places where you go "ah, I need to be able to make this thing available first..." as well.
That definitely leads to better, more user (developer) focused design on the library. Kind of how if you write unit tests and that sort of thing early on, it leads to code being more easily testable than if you slap together a bunch of glue and duct tape to get something huge and working rather than small incremental chunks. Ahem. Enough waxing, let me show you:
public class CoinStacksDisplay {
...
public List<FallingCoin> currentlyFalling;
public CoinStacksDisplay(Texture coinTexture, Vector2 xRange, Vector2 yRange) {
...
currentlyFalling = new LinkedList<>();
}
public void addCoins(int numberOfCoins) {
for (int i = 0; i < numberOfCoins; i++) {
CoinStack stack = stacks.get(MathUtils.random(0, stacks.size() - 1));
currentlyFalling.add(new FallingCoin(stack, coinTexture, coinHeight));
-stack.addCoin();
}
}
public void draw(SpriteBatch batch) {
for (CoinStack stack : stacks) {
stack.draw(batch);
}
for (FallingCoin coin : currentlyFalling) {
coin.draw(batch);
}
}
public void update(float delta) {
Iterator<FallingCoin> iter = currentlyFalling.iterator();
for (FallingCoin coin = iter.next(); iter.hasNext(); coin = iter.next()) {
coin.update(delta);
if (coin.readyToBeRemoved()) {
iter.remove();
}
}
}
}
I haven't defined the FallingCoin class at all yet. But I know that
I'm going to need to keep track of more than one, so list, each of these will be
moving over time, and so we'll have an update method. Similar, we'll
have a drawing method since obviously we need to dry them too.
The CoinStacksDisplay class didn't actually have an update
method before, that's entirely new code. Which means that we need to upate the user of
this class to ensure they call it. That would be the FirstScreen's render
method:
public void render(float delta) {
ScreenUtils.clear(Color.BLACK);
camera.update();
reelsPanel.update(delta);
btnPlexer.update(delta);
coinStacks.update(delta);
...
And now the program is broken and fails to load! But just because it doesn't know what a FallingCoin is and is screaming at us in red text that error: cannot find symbol FallingCoin and that's an easy fix:
public class FallingCoin {
private final Texture coinTexture;
private final float coinHeight;
private final Vector2 end;
private final CoinStack stack;
private final PositionBehavior positionBehavior;
private boolean readyToBeRemoved;
public FallingCoin(CoinStack stackToAddToWhenFinishedFalling, Texture coinTexture, float coinHeight) {
this.coinTexture = coinTexture;
this.coinHeight = coinHeight;
this.stack = stackToAddToWhenFinishedFalling;
this.readyToBeRemoved = false;
end = stackToAddToWhenFinishedFalling.getPosition();
Vector2 start = new Vector2(end.x, 20); // TODO take this in or something
float within = MathUtils.random(3,5);
positionBehavior = new ArrivingAtPosition(start, end, within);
}
public void draw(SpriteBatch batch) {
Vector2 p = positionBehavior.getPosition();
batch.draw(coinTexture, p.x, p.y, 1, coinHeight);
}
public void update(float delta) {
positionBehavior.update(delta);
if (positionBehavior.getPosition().equals(end) && !readyToBeRemoved) {
stack.addCoin(); // Hello! I'm here now!
readyToBeRemoved = true;
}
}
public boolean readyToBeRemoved() {
return readyToBeRemoved;
}
}
This class is accomplishing two things. First off, we've reified the stack.addCoin()
call into its own object. Effectively making it possible for us to queue up the call to add
the coin to its stack for display after it's finished falling. If we didn't do this,
we'd need to set up an event hook on when the coin finishes falling and deal with that over there.
I don't think we need a brand new interface for that sort of subscription when we can just do
this here.
Though, if we didn't do it here, we'd probably do it in the code that checks readyToBeRemoved
and removes the falling coin. If the FallingCoin only needed the reference to the stack for
this, then maybe we'd do that. But we use it for the positioning as well, so I'll just keep it in here
for now…
Ah. The fun thing about swapping back and forth between programming and blogging. If I weren't writing my thought process down here with you, I would have left this as is. But, looking at the class again, there really is no reason to keep a reference to the stack. Sure, I use it to setup some positioning, but once that's set, the position behavior vector is going to deal with that.
So, we can move the concern up a level. The CoinStackDisplay could…
Hey wait a moment. No. I'm wrong. Within this context:
public void update(float delta) {
Iterator<FallingCoin> iter = currentlyFalling.iterator();
for (FallingCoin coin = iter.next(); iter.hasNext(); coin = iter.next()) {
coin.update(delta);
if (coin.readyToBeRemoved()) {
iter.remove();
}
}
}
We can't do something like add in coinStack.addCoin() because
we have no idea which stack was selected to be added to! Tracking that would be one
more list whose sole purpose would be for something that only the falling coins
cares about, which means
I was right all along! Okay! Great! So, anyway. What does this look in action now? I'll artificially toss in 10 coins on each spin so we're not here all day:
Well. I would say the timing is slightly off. But, I think we want to address point two before we do that. I'm not saying this to just postpone the work, I promise. I think if we set up the payout text to happen on the current spin completion event, then we'll expose a second event after that finishes animating that will serve as a better fall trigger. At the very least, the coins won't land at the same time as the reels come to a stop I think.
So, moving onto point two. We should tell the player how many coins they just won and comiserate with them if they've got zero. Or maybe we should insult them? What's the pyschology on making people not want to give up, maybe just showing them something like "ALMOST" or "ONE MORE SPIN?" or "99% of players stop before hitting the big one"… Where's that one meme image of the guy digging the hole and stopping before the diamonds…
Ahem. Sorry let's get back to the code. So, making a random display to just show how much money we won isn't hard. It's the timing on when to show it that is. We're going to have to adjust some of the code in a few places to make it possible to do with any accuracy, but first, let's make a very simple render of that payment:
public class ResultDisplay {
private final Texture resultTexture;
private final NumberRenderer numberRenderer;
private final int coinsWon;
private final PositionBehavior position;
private final TimedAccumulator timer;
public ResultDisplay(
Texture resultTexture,
NumberRenderer numberRenderer,
int coinsWon,
Vector2 position,
float displayFor
) {
this.resultTexture = resultTexture;
this.numberRenderer = numberRenderer;
this.coinsWon = coinsWon;
this.position = new StationaryPosition(position.x, position.y);
this.timer = new TimedAccumulator(displayFor);
}
public void update(float delta) {
position.update(delta);
timer.update(delta);
}
public void draw(SpriteBatch batch) {
if (timer.isDone()) {
return;
}
Vector2 p = position.getPosition();
batch.draw(resultTexture, 4, 3, 24, 15);
numberRenderer.draw(batch, coinsWon, p.x, p.y);
}
public boolean readyToHide() {
return timer.isDone();
}
}
This is basically just a timed render for two textures. Or well, maybe a few more than a few depending on how many numbers the number renderer is doing. But conceptually, we're rendering two things for a given amount at a time at a given position. Since maybe I might want to make the text float, pulse, or wiggle, I'm going to set the position behavior up to be easily swapped out. For now though, stationary is good.
Setting this up from the first screen isn't hard. But to make life easy, in the show
method, we'll setup a display that won't display at all. This saves us worrying about a null pointer
later.
youWonTexture = new Texture(Gdx.files.internal("YouWon.png"));
tryAgainTexture = new Texture(Gdx.files.internal("TryAgain.png"));
youWonDisplay = new ResultDisplay(youWonTexture, numberRenderer, 0, new Vector2(10, 10), 0);
And by later, I mean within the reelsPanel spin complete handler:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
int coinsWon = bet * slotMachine.payout();
wallet.awardAmount(coinsWon);
coinStacks.addCoins(coinsWon);
spinBtn.setDisabled(false);
youWonDisplay = new ResultDisplay(
coinsWon == 0 ? tryAgainTexture : youWonTexture,
numberRenderer,
coinsWon,
(new Vector2(viewport.getWorldWidth(), 0)).scl(0.5f),
4f
);
}
});
This just pops it up right and center for the user:
And, it's pretty jarring and distracting from the fact that the timing is still off. So if I weren't a stubborn person, I might say "looks good move on".
But you know better than that. So let's talk about the tricky bit here. The stopSpinning
function in the reels panel we made, on the surface, looks like it stops the wheels:
public void stopSpinning() {
firstReel.stopReel();
secondReel.stopReel();
thirdReel.stopReel();
maybeSpin = Optional.empty();
for (ReelsSubscriber subscriber : subscribers) {
subscriber.onSpinComplete();
}
}
But our stop reel methods don't actually stop the reel. They just tell it to stop. The code certainly sets the boolean for whether or not the reel is spinning to false, but the actual individual symbols continue spinning until their position behavior lands them at the appropriate places:
public void stopReel() {
isSpinning = false;
...
// Lastly, update all the new symbols to flow to the right place accordingly,
// they should all move at the same rate to not look weird, so we compute the
// time for each based on their position in the queue.
iter = symbols.descendingIterator();
int i = 0;
float time = 1f;
float stopReelWithinSeconds = time * symbols.size();
for (Iterator<ReelSymbol> it = iter; it.hasNext(); i++) {
float stoppingPosition = position.y + i * reelSize;
ReelSymbol symbol = iter.next();
symbol.setPositionBehavior(
new ArrivingAtPosition(
symbol.getPosition(),
new Vector2(position.x, stoppingPosition),
stopReelWithinSeconds
)
);
}
}
This is a bit of a problem now isn't it. We can't rely on a boolean. We can't just say its stopped when it isn't. So, what are we to do? We want the individual reels to have a random stopping time to make the game not feel lockstepped on the UI side after the first spin. So, what can we do?
I think the easiest thing to do, besides perhaps tweaking our method names for clarity,
is to return that random amount of time to the caller of the stopReel. If
we do that, then we can take the maximum of the three reels, setup a timer to wait, and
then trigger the event. Or, well. I thought that would be the right thing to do,
but if we looking closely at the computations here, there's a bit of problem.
float stopReelWithinSeconds = time * symbols.size();
This value varies between 6 - 13 seconds or so. Remember that when we were making these reels spin and stop properly, we swapped over to our generator method. So we always have at least 3 in there since we're displaying 3 in the panel, and we also typically have one above and below the visible area in order to keep the spinning looking continuous.
So, that whole Math.max of the three values won't really do anything besides
make us wait for over 10 seconds in some cases while, to the user, the reels have already
stopped. So, let's step back and think for a brief moment. The reel symbols are being told
to arrive in the positions that correspond to their index in the symbol generator basically.
So the last symbol arriving ends up at 0, the second to last at 1 (the middle of the reel),
and so on. The issue though is that when we generate these symbols, they might be kind of
sort of far away. Remember, this is that code:
// We'll need to know where to start extending the reel out so that the
// winning row spins up into place.
float currentBottommostY = Float.MAX_VALUE;
Iterator<ReelSymbol> iter = symbols.iterator();
for (Iterator<ReelSymbol> it = iter; it.hasNext(); ) {
ReelSymbol symbol = it.next();
currentBottommostY = Math.min(currentBottommostY, symbol.getPosition().y);
}
String symbolToLandOn = reel.getCurrentSymbol();
Gdx.app.log("STOP ON ", symbolToLandOn);
String symbolAdded;
do {
symbolAdded = reelSymbolsToCome.peek();
currentBottommostY -= reelSize;
addNextSymbolToReel(currentBottommostY);
} while (!symbolToLandOn.equals(symbolAdded));
// The last Element added to the symbols is the winner for the reel this spin.
// Since we want it to land in the middle. Add one more.
currentBottommostY -= reelSize;
addNextSymbolToReel(currentBottommostY);
And then the code I showed before with the new ArrivingAtPosition(...) follows
immediately after the above.
Man. And here I thought we were setting ourselves up for success with this. But, it feels like we're just sort of stuck. I had another idea, that perhaps we should keep a diff between the previous position of the symbol in the reel and confirm its moving within some degree of tolerance and go off that. But that feels really REALLY hacky. Besides, how would we even know which symbol of the ones we generated we need to track?
Frustratingly, I think that we'd need to re-examine quite a few things in order to come up with a cleaner way of making this code work. So, for now, because I want to actually finish this project before the start of Advent of Code, we're going to just throw our hands up and
return 2;
This isn't always perfect, but it's better than making the user wait for 6 seconds while the reels sit stationary in front of them. I thought trying to do something like how many symbols there were divided by the speed they move at (24) but even 3.6 - 4s of waiting felt like forever. So. Hardcoding two for now and I'll have to think of another game to make to explore the issue of generated things moving to specific positions and how to properly subscribe to events from whatever those things are stopping. 19
We'll circle back if I think of a nice way to do this, but I think there's one piece of polish that is WAY more important than a perfectly timed result splash. It's super important, especially for a game like this. You maybe already thought of it, or just paid attention to the table of contents. Anyway. Let's hop to it.
User experience: Sound ↩
Ok, so I went on quite a side quest before starting to write this section. All so that I could do this:
echo "hello world" | text2wave -o test.wav -eval '(voice_cmu_us_slt_arctic_clunits)' -
That look on your face. You're confused aren't you?
Let me take a step back and explain. I went onto itch.io in search of game assets as per usual. But after some poking and prodding around, I couldn't really find a pack that had coin noises. I'm sure they're out there, but I didn't have the patience to go and look for them. On itch, you spot a pack that says something like "200 sfx for fantasy!" and open the page and then you have to look through a list, or worse there's a single youtube video linked that has no chapters and all the effects played one after the other.
It's not great. Sometimes you find tons of good stuff and exactly what you need! I had a good experience when I made the match 3 game for example, but slots seems no good. But then I had an idea. When I made pong I just used audio samples of my own voice for everything. That was humorous, but also, very very unleveled. Some sound was low, some was high, and none of it was just right.
But then, while sipping some eggnog, I had a brilliant idea. 20 I was seeing if I could "proof read" the blog post by having the reader mode in the browser do text to speech for me when it dawned on me. The computer can just make sound for me. But all my local text to speech (TTS) sucked and sounded like a dalek, and so I went off on a side quest. Some quick searches for local TTS showed a few options, and after shopping around for a minute or so I decided to go ahead and settle in on festival.
Festival is a pretty neat tool, if not a bit unwiedly at first. Downloading it on a debian based system is simple. First, find out if your repositories have it via search, then install:
$ apt search festival ... festival/jammy,now 1:2.5.0-8 amd64 [installed] General multi-lingual speech synthesis system ... $ sudo apt install festival
Next, the usual thing one does is hit help.
$ festival --help
Usage: festival Usage:
festival <options> <file0> <file1> ...
In evaluation mode "filenames" starting with ( are evaluated inline
Festival Speech Synthesis System: 2.5.0:release December 2017
-q Load no default setup files
--datadir <string>
Set data directory pathname
--libdir <string>
Set library directory pathname
-b Run in batch mode (no interaction)
--batch Run in batch mode (no interaction)
--tts Synthesize text in files as speech
no files means read from stdin
(implies no interaction by default)
-i Run in interactive mode (default)
--interactive
Run in interactive mode (default)
--pipe Run in pipe mode, reading commands from
stdin, but no prompt or return values
are printed (default if stdin not a tty)
--language <string>
Run in named language, default is
english, spanish, russian, welsh and others are available
--server Run in server mode waiting for clients
of server_port (1314)
--script <ifile>
Used in #! scripts, runs in batch mode on
file and passes all other args to Scheme
--heap <int> {10000000}
Set size of Lisp heap, should not normally need
to be changed from its default
-v Display version number and exit
--version Display version number and exit
To qoute one of my favorite spongebob moments,
that didn't help at all. But, it has an interactive mode, so I figured maybe I'd
learn a bit more by poking around in there. So, one quick festival -i later and
I was at a slightly different CLI. I had no clue what to do, so I just typed in ?
thinking maybe it'd be like git add -p and have something inline. It didn't. So
I tried the word helper instead:
festival> help
"The Festival Speech Synthesizer System: Help
Getting Help
(doc '<SYMBOL>) displays help on <SYMBOL>
(manual nil) displays manual in local netscape
C-c return to top level
C-d or (quit) Exit Festival
(If compiled with editline)
M-h displays help on current symbol
M-s speaks help on current symbol
M-m displays relevant manual page in local netscape
TAB Command, symbol and filename completion
C-p or up-arrow Previous command
C-b or left-arrow Move back one character
C-f or right-arrow
Move forward one character
Normal Emacs commands work for editing command line
Doing stuff
(SayText TEXT) Synthesize text, text should be surrounded by
double quotes
(tts FILENAME nil) Say contexts of file, FILENAME should be
surrounded by double quotes
(voice_rab_diphone) Select voice (Britsh Male)
(voice_kal_diphone) Select voice (American Male)
"
Oh ho! It works, and most importantly, there's a "doing stuff" section
that has the very conspicuous SayText command. So I tried
it out, and after re-reading the part that said I needed to use quotes,
I got it to say hi to me.
Yay! But how do I change the voice? The help text noted a select voice option, but typing just
festival> voice_rab_diphone
Did nothing. Squinting at the help a bit more, and looking at the website again I noticed the all important hint:
The system is written in C++ and uses the Edinburgh Speech Tools Library for low level architecture and has a Scheme (SIOD) based command interpreter for control. Documentation is given in the FSF texinfo format which can generate, a printed manual, info files and HTML.
Oh. It's a lisp machine! So I typed in (voice_kal_diphone) and it
accepted the command silently. Taking that as a good sign, I ran another
SayText "Hey there dude" and was greeted by a slightly different,
but still mechanical, voice. Perfect. But, the voice sucks. How do I get better
voices?
When you do apt search festival it also spits out a few related
packages such as
festvox-czech-machac/jammy,jammy 1.0.0-5 all Czech adult male speaker "machac" for Festival festvox-czech-ph/jammy,jammy 0.1-6 all Czech male speaker for Festival festvox-don/jammy,jammy 1.4.0-5 all minimal British English male speaker for festival
Which was enough of a context clue for me to end up over on the festvox manual pages trying to understand what else was available to me. After some time I ended up over on the cmu arctic page. I had been doing some searches for why some voices on the festival website didn't appear in the apt packages, and had landed over on this gist that mention cmu. While the links didn't resolve anymore, the steps it outlined gave me a decent enough hint on how to actually install all these zips and tars mentioned by the festival/festvox pages.
for t in `ls cmu_*` ; do tar xf $t ; done
sudo mkdir -p /usr/share/festival/voices/english/
sudo cp -pr $(ls | grep -v .bz2) /usr/share/festival/voices/english/
for d in `ls /usr/share/festival/voices/english` ; do
if [[ "$d" =~ "cmu_us_" ]] ; then
sudo mv "/usr/share/festival/voices/english/${d}" "/usr/share/festival/voices/english/${d}_clunits"
fi ; done
popd
This actually helped demystify the README file included in one of the tars I downloaded of the "packed" voices. Which, I guess is clear, but also, not at all:
You can run the voice "in place" or link it into your festival installation.
To run "in place" from the database directory
festival festvox/cmu_us_slt_arctic_clunits.scm festival> (voice_cmu_us_slt_arctic_clunits) festival> (SayText "This is a short introduction ...")Or to install as voice in your Festival installation it must appear
as a subdirectory of a subdirectory of a directory listed in the
Festival variable voice-path. For standard installations you can
create the following directory if it doesn't exist
/...WHATEVER.../festival/lib/voices/us/For RPM installed systems (such as RedHat) this would be
/usr/share/festival/voices/us/In that directory create a symbolic link to the arctic voice as in
ln -s /usr/local/arctic/cmu_us_slt_arctic cmu_us_slt_arctic_clunitsNote the name in the us/ directory must be the name of the voice.
This should allow festival to find the voice automatically thus
That whole "subdirectory of a subdirectory of the directory" was a tad confusing to me.
But, what was worse, was that I spent 20 or so minutes failing to get the automatic
discovery working. I was able to load the CMU voice festival festvox/blabla/scm
in interactive mode, but using just plain festival -i and then typing
(voice.list) kept giving me:
festival> (voice.list) (us2_mbrola en1_mbrola kal_diphone us3_mbrola us1_mbrola)
rather than what I expected after downloading two packs:
festival> (voice.list) (cmu_us_slt_arctic_clunits cmu_us_clb_arctic_clunits us2_mbrola en1_mbrola kal_diphone us3_mbrola us1_mbrola)
I had followed the gist, renaming the unzipped tar file to ed in "clunits" like
that and the readme had hinted at. But it wouldn't show up. After staring for a
while, I realized I was making a small mistake. I had extracted the tar file to
my /usr/share/festival/voices/english folder. But, if you'll pay very
close attention to the name of the voice. It's not en_blabla but rather,
us_blabal. That might seem obvious, but trust me, it wasn't. However!
Once I moved the two files over to usr/share/festival/voices/us it
worked as expected!
festival> (voice_cmu_us_slt_arctic_clunits) cmu_us_slt_arctic_clunits festival> (SayText "Plink Plink Plink") #<Utterance 0x71d32a22e970> festival>
Beautiful. Definitely the beautiful jingle jangle of money making goodness. But, interactive mode? Eh, that's no good. Can I redirect the audio somehow so I don't have to record a bunch of lines with OBS and then convert/split the file up?
I looked at chapter 23
of the festvox documentation and immediately felt my brain drop out of my skull.
/dev/audio? dsp? I popped back to the table of contents and noticed
the examples link.
And within those hallowed pages I saw it:
text2wave -mode singing america1.xml -o america1.wav text2wave -mode singing america2.xml -o america2.wav text2wave -mode singing america3.xml -o america3.wav text2wave -mode singing america4.xml -o america4.wav ch_wave -o america.wav -pc longest america?.wav
What is text2wave? Wait, do I have text2wave?
$which text2wave /usr/bin/text2wave
I do! A bit of man page reading and trial and error and I arrived at the hello world I started this section off with. I was pretty excited, so before even bothering to start working on the code to load these things up, I started jotting down the notes and closing out the 10+ tabs I had open from my adventure. Man. What fun. It's the side quests we take that make programming so much fun.
Let's move onto the code then, and also the areas we need to add sounds to. We need sounds for:
- First hover on a button
- Clicking a button
- Spinning of the reels
- Winning some coins on a bet
- Getting no coins on a bet
- Running out of money and game-overing?
The last one we'll need when we add in an actual game over I suppose, but the other ones
are all based on actions we can currently do. So, I can quickly generate some sounds with
our new powers of text2wave!
echo "Plink" | text2wave -o assets/sounds/plink.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "Plonk" | text2wave -o assets/sounds/plonk.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "Beep" | text2wave -o assets/sounds/beep.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "Boop" | text2wave -o assets/sounds/boop.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "Click" | text2wave -o assets/sounds/click.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "Clack" | text2wave -o assets/sounds/clack.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "WeeWooWeeWoo" | text2wave -o assets/sounds/WeeWooWeeWoo.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "winner" | text2wave -o assets/sounds/winner.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "try again" | text2wave -o assets/sounds/tryagain.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' - echo "game over" | text2wave -o assets/sounds/gameover.mp3 -eval '(voice_cmu_us_slt_arctic_clunits)' -
And of course, to actually use these things, we'll need to remind ourselves about the
libgdx wiki page on sound effects.
Which tells us when to use Sound and when to use Music. Given that
our sounds are all less than a megabyte, I think we'll use the sound class. We can wrap up
the references to these assets into our own little helper class.
public class SoundPlayer {
private final Sound plink;
...
private final Sound gameover;
public SoundPlayer() {
plink = Gdx.audio.newSound(Gdx.files.internal("sounds/plink.mp3"));
plonk = Gdx.audio.newSound(Gdx.files.internal("sounds/plonk.mp3"));
beep = Gdx.audio.newSound(Gdx.files.internal("sounds/beep.mp3"));
boop = Gdx.audio.newSound(Gdx.files.internal("sounds/boop.mp3"));
click = Gdx.audio.newSound(Gdx.files.internal("sounds/click.mp3"));
clack = Gdx.audio.newSound(Gdx.files.internal("sounds/clack.mp3"));
weeWoo = Gdx.audio.newSound(Gdx.files.internal("sounds/WeeWooWeeWoo.mp3"));
winner = Gdx.audio.newSound(Gdx.files.internal("sounds/winner.mp3"));
tryagain = Gdx.audio.newSound(Gdx.files.internal("sounds/tryagain.mp3"));
gameover = Gdx.audio.newSound(Gdx.files.internal("sounds/gameover.mp3"));
}
public void playWin() {
winner.play();
}
public void playTryAgain() {
tryagain.play();
}
...
public void dispose() {
plink.dispose();
...
gameover.dispose();
}
}
Not my best work, but I just want to hear the funny text to speech. So, let's wire in one of the simple places to see this work:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
int coinsWon = bet * slotMachine.payout() + 100;
wallet.awardAmount(coinsWon);
coinStacks.addCoins(coinsWon);
spinBtn.setDisabled(false);
if (coinsWon == 0) {
soundPlayer.playTryAgain();
} else {
soundPlayer.playWin();
}
youWonDisplay = new ResultDisplay(
coinsWon == 0 ? tryAgainTexture : youWonTexture,
numberRenderer,
coinsWon,
(new Vector2(viewport.getWorldWidth(), 0)).scl(0.5f),
4f
);
}
});
Annnnnd
Harumph. Channels are screwy huh? How many channels do these files have?
Ah, so, let's just fix that up.
plink = Gdx.audio.newSound(Gdx.files.internal("sounds/plink.wav"));
...
gameover = Gdx.audio.newSound(Gdx.files.internal("sounds/gameover.wav"));
Annnnnd
Perfect. (The audio is a bit quiet, but better than then earblowing I think).
So adding in the calls to the sound on the button subscribers for each of the things in the screen lets us make buttons and reels have sounds too:
I'm loving the result. There's just something very silly and funny about this to me. It's not like I'm selling the game, and it's not like I'm going to make it a super duper gambling machine either. It's just for fun and for the amusement of being nerd-sniped by that wikipedia page after all, so might as well make more parts of it make me laugh.
The question though is how do I get the sounds for the coins? Obviously I could play a single sound when adding the coins to the stacks here:
public void onSpinComplete() {
int coinsWon = bet * slotMachine.payout() + 100;
wallet.awardAmount(coinsWon);
coinStacks.addCoins(coinsWon);
spinBtn.setDisabled(false);
soundPlayer.stopWeeWoo();
if (coinsWon == 0) {
soundPlayer.playTryAgain();
} else {
soundPlayer.playWin();
}
youWonDisplay = new ResultDisplay(
coinsWon == 0 ? tryAgainTexture : youWonTexture,
numberRenderer,
coinsWon,
(new Vector2(viewport.getWorldWidth(), 0)).scl(0.5f),
4f
);
}
But we know that won't play at the same sound as the coins coming down or anything. So, we need to make the coin's display aware of the sound player I think. And prepare ourselves for lots, and lots, and lots of plinking noises:
public void addCoins(int numberOfCoins) {
soundPlayer.playPlink();
for (int i = 0; i < numberOfCoins; i++) {
CoinStack stack = stacks.get(MathUtils.random(0, stacks.size() - 1));
currentlyFalling.add(new FallingCoin(stack, coinTexture, coinHeight));
}
}
public void update(float delta) {
Iterator<FallingCoin> iter = currentlyFalling.iterator();
for (FallingCoin coin = iter.next(); iter.hasNext(); coin = iter.next()) {
coin.update(delta);
if (coin.readyToBeRemoved()) {
soundPlayer.stopPlink();
soundPlayer.playPlonk();
iter.remove();
}
}
}
Hiliarious. Now would be a good time to mention that two of the sounds are setup
not to call sound.play(), but instead:
public void playPlink() {
plink.loop();
}
public void playWeeWoo() {
weeWoo.loop();
}
Loop is the same thing as play, but instead of playing once, it just keeps going
until you tell it to stop. Since our needs are simple and meager, we can just call loop
and not do anything with the id number that it returns. If we had more complicated stuff
in the UI, then we could track that ID and then tell specific instances of the sound playing
to stop. But since we disable buttons and spawn all the coins at once, then we don't really
need the extra tracking.
But if we did want to, we could make the coins get added with random offsets, then have each coin track its sound ID, then stop it and play the plonk noise when it finishes falling. It wouldn't be hard to do, but I think I'll leave it as an exercise to the reader! Make your own gambling game and have lots of fun making silly sounds and whatnot. For me, I want to add one more set in to see what it's like if I try to make a sound on hover:
@Override
public boolean mouseMoved(int screenX, int screenY) {
Vector3 worldXYZ = camera.unproject(new Vector3(screenX, screenY, 0));
boolean touched = false;
for (ClickableButton btn : buttons) {
if (!btn.isPointInside(worldXYZ.x, worldXYZ.y) && btn.isHeld()) {
btn.resetClick();
} else if (btn.isPointInside(worldXYZ.x, worldXYZ.y)) {
btn.hover();
soundPlayer.playClick();
} else {
btn.stopHover();
soundPlayer.playClack();
}
}
return touched;
}
Eh. Maybe we don't do that one. It's a bit much. It borders more on annoying than funny I think. Alright, so we won't use the click or clack for now. We can use that somewhere else. There's just the one last sound to use:
public void playGameOver() {
gameover.play();
}
To use this one, we gotta have an actual loss condition. So, I think it's time for us to finally add a new screen to the mix.
Game over ↩
Up until this point we've been prototyping in the one screen class. We've ignored the asset manager and declared all our textures in one place, same for the sounds too. But, now? You'd think we'd introduce the asset manager at this point, after all, I normally get that out of the way first and setup my "scene" classes and whatnot. But, let's keep on pushing that off for now and just declare what we need again:
At first, I added in the number renderer and the coin texture, then I realized that if you're in this screen, there's no number to show. You're out of cash! There's no "you won X amount" either, since you have to bet it all to get to this point. So... We can just make a simple game over screen and toss in a button to restart or quit:
public class GameOver implements Screen {
private final GameRunner gameRunner;
private SoundPlayer soundPlayer;
private SpriteBatch batch;
private OrthographicCamera camera;
private FitViewport viewport;
private Texture gameOverTexture;
private Texture againBtnTexture;
private Texture quitBtnTexture;
private ClickableButton quitBtn;
private ClickableButton againBtn;
public GameOver(GameRunner gameRunner) {
this.gameRunner = gameRunner;
}
@Override
public void show() {
soundPlayer = new SoundPlayer();
batch = new SpriteBatch();
camera = new OrthographicCamera();
viewport = new FitViewport(32, 18, camera);
camera.setToOrtho(false);
camera.update();
gameOverTexture = new Texture(Gdx.files.internal("gameOver.png"));
againBtnTexture = new Texture(Gdx.files.internal("tryAgain.png"));
againBtn = new ClickableButton(againBtnTexture, 6, 2, 4, 6);
quitBtnTexture = new Texture(Gdx.files.internal("quit.png"));
quitBtn = new ClickableButton(quitBtnTexture, 12, 6, 4, 6);
}
@Override
public void render(float delta) {
ScreenUtils.clear(Color.BLACK);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(gameOverTexture, 0, 0, 32, 18);
quitBtn.draw(batch);
againBtn.draw(batch);
batch.end();
if (Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) {
Gdx.app.exit();
}
}
}
There's no input handling yet, but our silly little game over screen is red, white, and blue just like all the other stuff themed in the game so far:
Very slapped together. But as I've said before, I'm not an artist. The important thing is to make something even if I'd probably never tell anyone I'm proud of it, I can at least say that I had fun making it and learned one or two things along the way. Let's get these buttons working. We can make the hover effect work with a simple multiplexer:
public void show() {
...
btnPlexer = new ButtonMultiplexer(camera);
btnPlexer.addButton(againBtn);
btnPlexer.addButton(quitBtn);
Gdx.input.setInputProcessor(btnPlexer);
}
@Override
public void render(float delta) {
btnPlexer.update(delta);
And if we add in some subscribers, we can make quit and the again button work:
againBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
gameRunner.setScreen(new FirstScreen(gameRunner));
}
});
quitBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
Gdx.app.exit();
}
});
In order for this to compile, we need to update the FirstScreen to
take the game runner instance as an argument. The reason for this is present in
the above code, we need to be able to tell the system to swap screens. But, once
both screens have the GameRunner, then we can update the spin complete
event to transition if the player runs out of coins:
reelsPanel.addSubscriber(new ReelsSubscriber() {
@Override
public void onSpinComplete() {
int coinsWon = bet * slotMachine.payout();
wallet.awardAmount(coinsWon);
...
if (wallet.getFunds() == 0) {
gameRunner.setScreen(new GameOver(gameRunner));
}
}
});
If I make the wallet be much smaller than that initial 100 coins, I can easily show you that these two pieces tie things together up nicely:
Lovely. One last little tweak though. In the spin completion handler, we can save the user a few clicks of the bet button if we round the bet down to the number of funds once we start getting low:
if (wallet.getFunds() < bet) {
bet = wallet.getFunds();
}
And that's a slightly better experience. Because you know, text to speech, paint doodles made in 30 seconds, and absolutely 0 tutorial instructions make for the ideal arcade cabinet experience. Maybe. I've never been to Vegas so I don't have anything to compare to; It seems fun and fine to me! Though there is one last thing we're missing…
A title screen ↩
I suppose, maybe it feels a bit funny. Doing the title screen last. But honestly, I find that this happens more often than not if you're making little games like I've been doing for these 20 game challenges. Your first step is pretty much always: get something core to the game logic working. Title screens aren't that. 21
The title screen here is going to be very, very similar to the end screen. We'll have two buttons and a background. So, it's off to the painting program again and also, I suppose I need to come up with a better name for the game than "slot machine"? Maybe?
I never claimed to be clever. It's a game where you gamble, so, it's name is gamble. As the first of its kind, it gets to be named after the action. Though, the code is not the first of its kind at all. It's literally a copy paste of the game over code with some variable names and asset paths tweaked:
public class TitleScreen implements Screen {
private final GameRunner gameRunner;
... all the private fields ...
public TitleScreen(GameRunner gameRunner) {
this.gameRunner = gameRunner;
}
@Override
public void show() {
soundPlayer = new SoundPlayer();
soundPlayer.playGameOver();
batch = new SpriteBatch();
camera = new OrthographicCamera();
viewport = new FitViewport(32, 18, camera);
camera.setToOrtho(false);
camera.update();
titleTexture = new Texture(Gdx.files.internal("gamble.png"));
startBtnTexture = new Texture(Gdx.files.internal("start.png"));
startBtn = new ClickableButton(startBtnTexture, 7, 2, 6, 4);
quitBtnTexture = new Texture(Gdx.files.internal("quit1.png"));
quitBtn = new ClickableButton(quitBtnTexture, 18, 2, 6, 4);
btnPlexer = new ButtonMultiplexer(camera);
btnPlexer.addButton(startBtn);
btnPlexer.addButton(quitBtn);
Gdx.input.setInputProcessor(btnPlexer);
startBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
gameRunner.setScreen(new FirstScreen(gameRunner));
soundPlayer.playClick();
}
});
quitBtn.addSubscriber(new ButtonSubscriber() {
@Override
public void onClick(ClickableButton clickableButton) {
soundPlayer.playClack();
Gdx.app.exit();
}
});
}
@Override
public void render(float delta) {
btnPlexer.update(delta);
ScreenUtils.clear(Color.BLACK);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(titleTexture, 0, 0, 32, 18);
quitBtn.draw(batch);
startBtn.draw(batch);
batch.end();
if (Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) {
Gdx.app.exit();
}
}
...
}
It really is that simple. So, let's do a quick run through of the game and observe any last minute tweeks we should do before we call the game complete.
I think the main thing is that the game screen is still using the basic mask for the "machine", and we should sketch out our slot machine a bit to make the game screen be more in line with the title/end screen.
I think it bears repeating, I am not an artist. But, here we go. The machine that will definitely make people stop and wow at its splendor. Truly, a gambling machine above all gambling machines that makes people take notice:
I upscaled our 32x18 pixel mask to 1280x720, which led to that faded effect. Then just added in boxes and doodled in silly things. I figure that noting that the red white and blue seven combo will win you a bunch of coins is important to call out, and we had a bunch of screen real estate on the right side, so it just seemed correct to do. I'm not much of a dark pattern type person, so I can't really think of anything sketchy to do here.
I think that about wraps that up though. So let's put this post to rest. It's been in the hopper for quite some time and went on longer than I thought it would!
Wrapping up ↩
If I take my tongue out of my cheek for a moment and be honest: no this isn't a very good game. I had fun making it obviously. It's fun to attempt to draw a straight line in a paint program. It's fun to use text to speech to make sounds that should definitely not be used for what I'm using them for. It's fun to read through a wikipedia page and then get swallowed up for a weekend to make the core mechanics.
It's a little less fun thinking about tweaking and refactoring things to the usual structure I use. Though it certainly could be done. Similar, since I was avoiding using the freetype library so I could target the HTML "platform" for libGDX, I used textures with text in them instead of having to load up a basic BitMapFont. I do think that this gives the game an early 00s feel. Like something you'd find uploaded to newgrounds or a similar flash site that someone made for their first game.
Granted, this was our 4th game in the 20 games challenge, so I suppose I can't use inexperience as an excuse here for how bad of a "game" it is. But it was a well needed change in pace compared to the others. Doing obstacle collision code is annoying, and getting frustrated by math isn't really how I want to spend my weekends. So, making something silly that uses text to speech that makes me smile helps refill that battery I need to power through that stuff.
Not to mention, veering off the path that the 20 game challenge website paves for you if you want to follow their recommendations is also good. Am I dodging some of the harder things they suggest to do? Sure! I think it's too early for me to dig into 3d graphics of any kind yet, but did I learn something new doing this? Yes! Slot machines are kinda neat from a math perspective, and the reels logic can be tricky to figure out.
I do think you could make this more data-driven pretty easily. The only hard part to figure out would be how to encode the payout table to match your reel. Like, you'd need to setup some way to categorize or tag the different items in the reels so that you wouldn't have a combinatorial explosion trying to list all the potential payouts. Being able to say "this is X" "three X is this much", where X isn't just a reel symbol but the color of many, would be powerful and neccesary.
I'd say you could probably do something like define a format like
SYMBOL,tag1,tag2,tag3,etc...
Then have a file per reel, then one more file that would note your payouts. Like:
tag1,tag1,tag2,300 tag2,tag2,tag2,200 symbol0,symbol1,symbol2,10 ...
And as long as you encoded the payout table in a way that the appropriate precedence was followed to deal with potentially multi-matching rows, then you'd be able to allow people to make custom reels to load into the system. Besides the hardcoded 64 we put in place, this wouldn't be too hard to support I think. Maybe that'd make for a fun post later on down the line if we return to the gambling machine, but I would say that the chances of that are about as high as someone both playing this game and hitting the jackpot.
But who knows! All it'd take is a bit more nerd sniping and I might end up back here. Tweaking and twiddling away. Though, a different bit of geekiness is currently taking up my attention as I write this. I was perusing the code of a game I played a while ago and was thinking about how libGDX has support for scala and scala's a wonderful language to write DSLs in, so, it's tempting to take another crack at putting together some visual novel primitives.
Doing that involves braving the world of font height and width calculations though, which is about as fun as object collision code. So, we shall see which wins. My love of scala, or my laziness. Or who knows. Maybe something else will come along and distract me. It is almost advent of code season after all!
Lastly, I mentioned I was targetting the HTML backend this time in addition to the usual Java distribution. Upon my first attempt to run the dist task for gradle I was greeted with a rather bothersome error:
This is an error that occurs to indicate that GWT does not have any support for the class
you want to try to use. As
noted on stackoverflow, if you run into this you pretty much have to find a replacement
for whatever you were trying to use and use something that is instead.
The supported classes are here,
and in my case the class I was trying to use was the java.util.HashTable class
of all things.
Thankfully, HashTables are pretty easily replaceable with a HashMap
in our case. And luckily for me, that was the only class that needed replacing. Once that was
done I was able to trigger the HTML build target and bam. Distribution ready! So, if you made
it this far, congrats! You can play the game right over here!
Or if you'd prefer the desktop version running Java, then you can pick up the releases
here on github for your platform.
Thanks for reading, and I hope that this helps inspire you to make your own stupid silly games, even if they're not "professional" or polished, the experience itself is still fun! Now if you'll excuse me, I've got some chinese food for Thanksgiving to eat!