I had a very similar requirement for a game I'm working on. Here are some code snippets (although its in Swift and not C#) with some explanations:
Input state:
before filling in gaps
wave 0 = [1.0, 0.0, 0.0, 0.0]
wave 1 =
wave 2 =
wave 3 =
wave 4 =
wave 5 = [0.5, 0.5, 0.0, 0.0]
wave 6 =
wave 7 = [0.3, 0.3, 0.3, 0.0]
wave 8 =
wave 9 = [0.75, 0.0, 0.0, 0.25]
Output state:
after filling in gaps
wave 0 = [1.0, 0.0, 0.0, 0.0] --> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
wave 1 = [0.9, 0.1, 0.0, 0.0] --> [1, 1, 1, 1, 1, 1, 1, 2, 1, 1]
wave 2 = [0.8, 0.2, 0.0, 0.0] --> [2, 2, 1, 1, 1, 1, 1, 1, 1, 1]
wave 3 = [0.7, 0.3, 0.0, 0.0] --> [2, 1, 1, 1, 2, 1, 1, 1, 2, 1]
wave 4 = [0.6, 0.4, 0.0, 0.0] --> [1, 1, 2, 2, 2, 1, 1, 1, 2, 1]
wave 5 = [0.5, 0.5, 0.0, 0.0] --> [2, 1, 1, 2, 2, 2, 2, 1, 1, 1]
wave 6 = [0.4, 0.4, 0.15, 0.0] --> [2, 1, 1, 3, 3, 1, 2, 1, 2, 2]
wave 7 = [0.3, 0.3, 0.3, 0.0] --> [2, 3, 2, 1, 1, 2, 3, 1, 3]
wave 8 = [0.525, 0.15, 0.15, 0.125] --> [2, 4, 1, 1, 1, 1, 3, 3, 1, 2]
wave 9 = [0.75, 0.0, 0.0, 0.25] --> [1, 4, 1, 1, 1, 4, 1, 1, 1, 4]
Some basic data structures
A basic function to interpolate two values
/// interpolates a value between start and end, for specified step
/// step varies between 0 and 1.0
/// for step = 0.5 for start 0 and end 10 should give 5
func interpolate(start:Double, end:Double, step:Double) -> Double {
return start + (step)*(end-start)
}
Represents a wave, takes in an array of double values that represent the enemy distribution for a wave. It also has a utility function that generates a set of enemies of a specific size based on the distribution
class Wave {
var distribution:[Double]
/// initialize with the distribution
init(distribution:[Double]) {
self.distribution = distribution
}
/// get a set of enemies of specified size
// based on the distribution
func enemies(size:Int) -> [Int] {
var e = [Int]() // list of enemies to generate
for (index, d) in enumerate(distribution) {
let count = Int(round((d * Double(size))))
for i in 0..<count {
e.append(index+1)
}
}
// due to round up , we can have e.count > size
// e.g. if distribution is 0.25, 0.75, for size 10, we end up with 11 elements
if e.count > size {
// remove an item from the largest distribution
var dlargest = distribution[0]
var largetEnemyType = 1
for (index,d) in enumerate(distribution) {
if dlargest < d {
dlargest = d
largetEnemyType = index+1
}
}
// now remove one element from e where e[x] == largestEnemyType
var indexToRemove = 0
for (index, enemyType) in enumerate(e) {
if e[index] == largetEnemyType {
indexToRemove = index
break
}
}
e.removeAtIndex(indexToRemove)
}
return e
}
var description:String { return "\(distribution)" }
}
A simple struct used to hold interpolation blocks for between waves. An interpolation block is used to identify missing chunks of waves.
struct InterpolationBlock {
var start:Int
var end:Int
}
Algorithm
To start off, initialize our waves array to empty. This example assumes 10 waves
// initialize our waves with nil
var waves = [Wave?]()
for var i = 0; i < 10; i++ {
waves.append(nil)
}
We define 4 enemies, so all distributions will have 4 values. Also, once the waves are initialized to nil (empty) pick and set which waves we need. For this snippet example, we need to have the first and last waves set to some values in the minimum. All other intermediate values for wave distributions will be interpolated.
let totalEnemies:Int = 4
// set up specific wave distributions
// the first and last ones must be defined
waves[0] = Wave(distribution: [1.0,0,0,0])
waves[5] = Wave(distribution: [0.5,0.5,0,0])
waves[7] = Wave(distribution: [0.3,0.3,0.3,0])
waves[9] = Wave(distribution: [0.75,0.0,0.0,0.25])
Pass 1 - identify missing wave chunks and store them in interpolation blocks
var blocks = [InterpolationBlock]()
var currentBlock = InterpolationBlock(start: 0, end: 0)
for var i = 1; i < waves.count; i++ {
if waves[i] != nil {
currentBlock.end = i
blocks.append(currentBlock)
currentBlock.start = i
}
}
Pass 2 - fill up the waves array using interpolatation to find missing waves using the block start and end indexes
for block in blocks {
// for current block, find out how many missing waves
// needs to be filled up
let missingWaves = Double(block.end - block.start)
let startWave = waves[block.start]!
let endWave = waves[block.end]!
for var missingWaveIndex = block.start+1, i=1; missingWaveIndex < block.end; missingWaveIndex++, i++ {
// interpolate values for missing
// enemy distribution
var missingDistribution = [Double]()
for var ed = 0; ed < totalEnemies; ed++ {
let start = startWave.distribution[ed]
let end = endWave.distribution[ed]
let step = Double(i)/missingWaves
let gapFill = interpolate(start, end, step)
missingDistribution.append(gapFill)
}
waves[missingWaveIndex] = Wave(distribution: missingDistribution)
}
}
At this stage, all waves are filled up. To generate a set of enemies, say 10 enemies, we could call wave[2].enemies(10)
which would give [1, 1, 1, 1, 1, 1, 1, 1, 2, 2]
and if you want a randomized set, use a shuffle algorithm like Fischer-Yates.