Why This Is UsefulSuppose you have 6 players, and 6 bases, that are both somehow indexed. Now let's say the default game setup, would assign Player X to Base X. You can think of this as a rather simple set of pairs. (A "mapping" actually, in the mathematical sense).
F = { (Player 0, Base 0), (Player 1, Base 1), (Player 2, Base 2), (Player 3, Base 3), (Player 4, Base 4), (Player 5, Base 5) }But, a mapping is just a 1-1, and onto function. So, if we write it in a functional form, it would look like:
{ F(Player 0) = Base 0, F(Player 1) = Base 1, F(Player 2) = Base 2, F(Player 3) = Base 3, F(Player 4) = Base 4, F(Player 5) = Base 5 }Now, in a more computer science like way, let's implement the function using a lookup table:
int f[] = {0, 1, 2, 3, 4, 5};Then to compute the function F, we simply index into the lookup table.
So, to see which base (number) a player (number) is associated with, we just index like so:
for (playerNum = 0; playerNum < numPlayers; playerNum++)
{
baseNum = f[playerNum];
// Insert code to create the identified base here (for the current player)
CreateBase(playerNum, baseNum);
}
Now to randomize those locations, it's as easy as randomizing that lookup table.
int f[] = { 0, 1, 2, 3, 4, 5 };
const int numElementsInF = sizeof(f) / sizeof(*f); // Byte size of array / byte size of 1 element
RandomizeList(numElementsInF, f);
for (playerNum = 0; playerNum < numPlayers; playerNum++)
{
baseNum = f[playerNum];
// Insert code to create the identified base here (for the current player)
CreateBase(playerNum, baseNum);
}
Of course, the typical usage of this is with an array from 0..X, so why bother creating such a trivial array in those cases? That's where the other two functions in OP2Helper come in. The easiest way to create the bases is to have the base data specified in some sort of data structure, and then use identical code to create all the bases. To create the right base, you just pass a pointer to the right data structure (and a player number to create the base for). As there is already an array of base data in this case, that array is randomized directly, rather than creating a dummy mapping array. Using this idea, we get RandomizeBases() and RandomizeResources() (for beacons) in OP2Helper.
Advanced UsesWhat if you only want to randomize some bases? Perhaps there is an AI that must always appear in the center. Or maybe there is a team game where the first 3 players are against the last 3 players, and you want to put teammates together.
This can be done using array slicing. Actually, that's mostly what it would be called in some other languages. In C/C++ arrays and pointers are type equivalent, and arrays tend to not have a size directly associated with them, so what we're doing is maybe a little lower level, and perhaps more error prone. (It's also the reason why there is a difference between delete and delete[]). But, the finer points of why type equivalence between arrays and pointers is probably a bad idea aside, let's see how this is done.
int a;
int b[] = {1, 2, 3};
// Assume function f() takes an array size, and a pointer to the array
f(1, &a); // Pass address of single variable 'a', and treat it as an array of size 1
f(3, b); // Pass address of array b, giving it's full size of 3
f(2, b); // Pass address of array b, giving only a partial size of 2
f(1, &b[1]); // Pass address of the second element in b, and treat as an array of size 1
f(2, &b[1]); // Pass address of the second element in b, and treat as an array of size 2
Basically, an array is a group of consecutive memory cells, and we're just being explicit about which ones to use as a new array/subarray. To apply this to the AI-out example above, we might write the randomize code like this:
int f[] = { 0, 1, 2, 3, 4, 5 }; // Note: Array of size 6. The last entry, Player 5 is the AI
RandomizeList(5, f); // Note: Using 5 instead of 6 for the array size. That is, randomize the subarray consisting of the first 5 elements, but not the 6th (Player 5).
If instead, we want to group team members, in two groups of three people, we could do something like this:
RandomizeList(3, f); // Randomize first 3 players (Note: could have used &f[0] instead of just f)
RandomizeList(3, &f[3]); // Randomize last 3 players
This of course generalizes, so we might want to randomize three teams of two players each like so:
RandomizeList(2, f); // Players 0, 1
RandomizeList(2, &f[2]); // Players 2, 3
RandomizeList(2, &f[4]); // Players 4, 5
Note that these solutions above will exchange players withing the team slots, but won't change the order of the teams. So say, the first team is always at the top of the map, while the second team is always at the bottom of the map.
Now, what if you want to randomize the team locations as well. For this we could use a two level index structure.
// Note: **Untested** Array syntax might be a bit off here
int teamIndex[] = { 0, 1, 2 };
int baseIndex[3][] = { {0, 1}, {2, 3}, {4, 5} };
RandomizeList(3, teamIndex); // Randomize where to place teams
RandomizeList(2, baseIndex[0]); // Randomize players on first team
RandomizeList(2, baseIndex[1]); // Randomize players on second team
RandomizeList(2, baseIndex[2]); // Randomize players on third team
for (playerNum = 0; playerNum < numPlayers; playerNum++)
{
teamNum = playerNum / 2; // Note: Rounds down, so Player (0, 1) -> Team 0, Player (2, 3) -> Team 1, Player (4, 5) -> Team 2
teamBaseNum = playerNum % 2 // Index within a team, Players (0, 2, 4) -> 0, Players (1, 2, 5) -> 1
// Remap player's team location, and then the base within that team's location
baseNum = baseIndex[teamIndex[teamNum], teamBaseNum];
// Insert code to create the base here
CreateBase(playerNum, baseNum);
}
This would randomly place the 3 teams, each of which contain two base locations, and then randomly swap the players among those two base locations.
Finally, what if we want to have more possible team locations, and more possible player locations than can ever be filled at once? Well, we use something rather similar to array slicing here too. Let's say we have 5 possible team locations, with 3 possible base locations for 4 of the teams, and 2 possible base locations for 1 of the teams.
// Note: **Untested** Array syntax might be a bit off here
int teamIndex[] = { 0, 1, 2, 3, 4 };
int baseIndex[5][] = { {0, 1, 2 }, {3, 4, 5}, {6, 7, 8}, { 9, 10, 11}, {12, 13} }; // Note: This will zero fill until the array is "rectangular" (It creates a 5 x 3 array if we use this notation)
RandomizeList(5, teamIndex); // Randomize where to place teams
RandomizeList(3, baseIndex[0]); // Randomize players on first team
RandomizeList(3, baseIndex[1]); // Randomize players on second team
RandomizeList(3, baseIndex[2]); // Randomize players on third team
RandomizeList(3, baseIndex[3]); // Randomize players on fourth team
RandomizeList(2, baseIndex[4]); // Randomize players on fifth team (only 2 slots)
for (playerNum = 0; playerNum < numPlayers; playerNum++)
{
teamNum = playerNum / 2; // Player (0, 1) -> Team 0, Player (2, 3) -> Team 1, Player (4, 5) -> Team 2
teamBaseNum = playerNum % 2 // Index within a team, Players (0, 2, 4) -> 0, Players (1, 2, 5) -> 1
// Remap player's team location, and then the base within that team's location
baseNum = baseIndex[teamIndex[teamNum], teamBaseNum];
// Insert code to create the base here
CreateBase(playerNum, baseNum);
}
Note that the only thing that changed between this example and the previous, was the arrays, and the randomizing of those arrays. The loop didn't change. What happens, is the first 3 teams in the randomized team array are selected (the rest are ignored), and the first 2 base locations in the randomized base location lists are selected, and the rest (if it "exists") is ignored.
An example might look like this after randomization:
teamIndex[] = { 3, 2, 0, 4, 1 }; // Team locations (3, 2, 0) are used (4, 1) are ignored
baseIndex[5][] = { {1, 0, 2}, {who cares?}, {8, 7, 6}, {10, 9, 11}, {who cares?} }; // Bases (10, 9), (8, 7), (1, 0) are used (in that order), the rest are ignored
Other Uses and GeneralizationsPretty much anything that's like shuffling a deck of cards can be handled this way. Base locations is the most obvious one. Resource randomization was another obvious one. For things that are less specific, you can use the integer shuffling routine, and do the indexing yourself, like I've done in the above examples. Perhaps you could shuffle the turrets on a few combat vehicles.
You can also extend beyond the two level indexing. Maybe some base locations for a team overlap, and you want to partition them into disjoint sets of bases that don't overlap. A third level index could be used here to achieve this. Perhaps select a general location for each team, then select a layout of non-overlapping base locations, and then shuffle the players between each of the locations. You might also experiment with reusing some of the same places (numbers) in the final array. Something like: { { {0, 1, 2}, {0, 3, 4}, {5, 1, 6}, ... /* Other team layouts*/}, ... /* Other team locations */ }. Here is might be that base 0 and 5 are shifted by half a base size, and so they overlap. Same perhaps for bases 1 and 3, or 2 and 4. There's no reason you can't reuse the information for base 0 if the only difference in the layout is the shifting of locations of bases 1 and 2 (to 3 and 4). Or perhaps reuse base location 1 if you're shifting base locations 0 and 2 (to 5 and 6).
For these more deeply nested shufflings, you probably want to form a hierarchy, where you shuffle the order of sets independently of the contents of those sets. The ordering of the shuffle isn't particularly important. Maybe shuffle the largest set first, and then each of the contained sets, or you could start at the inside, shuffling the elements within a set, and then shuffling a set of sets. (Or really just do it in any "random" order).
Just pay attention to the indexing as you increase the depth. Generally, you use % to pull out the lower order index value and / to pull out the higher order index value, from some larger external index (such as a player num).
int array[7][5][6]; // Where the last [] is the innermost set
// Assume we want to slice it to have [3][1][2] (say 3 teams, 1 layout per team, 2 base locations selected per layout)
restOfIndex = playerNum; // For illustration purposes
baseNum = restOfIndex % 2; // Within a team
restOfIndex = restOfIndex / 2;
layoutNum = restOfIndex % 1; // Quite useless for 1 really, since you can only get 0
restOfIndex = restOfIndex / 1; // Also quite useless for 1
teamNum = restOfIndex % 3; // Actually the % ususally isn't needed for the outmost index, since the value should be less than what you would mod by
//restOfIndex = restOfIndex / 3; // Useless, as you don't need to index past where we already were, but try to convince yourself that this value is 0. (Assuming you don't try to index past the end of the output array of what you're slicing)
Of course you'll probably want to optimize that a little, or at least write it nicer.