spoooq
Nov 03, 2006, 12:48 PM
Hi,
I wanted a way to know which tiles are the best for a city to work in the long-term, so I threw together this proof-of-concept to see what was possible. The algorithm is fairly horrendous right now, so I wouldn't advise having more than about 8 tiles per city, or maybe running it overnight. It's written in Ruby, which I don't expect to be a popular language round here, but I like it, so that's what I used. I'd like to expand the number of things it takes into account, such as happiness or whipping. At the moment the tile values are just randomly generated - no desert or plains here, just a bunch of numbers.
TurnsToSimulate = 80
tiles = []
tiles << "plains,none,forest,none"
tiles << "grassland,none,none,rice"
tiles << "grassland,none,farm,rice"
tiles << "plains,none,none,none"
# [Food,Gold,Hammers]
Attributes = [
base = {
"none" => [0,0,0],
"plains" => [1,0,2],
"grassland" => [2,0,1],
"tundra" => [1,0,0],
"floodplains" => [3,0,0]
},
feature = {
"none" => [0,0,0],
"hills" => [0,0,1]
},
overlay = {
"none" => [0,0,0],
"forest" => [0,0,1],
"farm" => [lambda { |tile|
bonus = 1
case tile[4]
when "rice"
bonus = 2
end
bonus
},0,0]
},
special = {
"none" => [0,0,0],
"rice" => [1,0,0]
}
]
tiles.map! { |tile|
v = tile.split(',')
Attributes.inject([0,0,0]) { |total, type|
res = type[v.shift]
3.times { |t| total[t] += res[t].class == Proc ? res[t].call(tile.split(',')) : res[t] }
total
}
}
# Calc food required to achieve a pop size
def inc(size)
10 * 2 ** size - 20 # Or similar
end
# Given a total amount of food, calc current size
def currentSize(food)
s = 1
s += 1 while food > inc(s)
s
end
puts "Starting combinations #{Time.now}"
# Generate combinations
Combinations = (1..tiles.size).inject([[]]) { |acc, node| # Combinations (ints)
acc.inject([]) { |acc, injectedNodes| acc + [injectedNodes, injectedNodes + [node]] }
}.map { |c|
c.inject([0,2,1,2]) { |acc, node| # Combinations (tiles)
(0..2).inject([acc[0].succ]) { |i,n| i + [acc[n.succ] + tiles[node-1][n]] }
}
}.uniq.sort.inject([]) { |acc, node| # Tranches
acc + [acc.size <= node[0] ? [node] : acc.pop + [node]]
}.each { |set|
set.delete_if { |test| # Prune
set.inject(false) { |acc, comp|
acc or (!test.equal?(comp) and ((1..3).inject(true) { |i,n| test[n] <= comp[n] and i } and acc = true))
}
}
}
puts "Starting permutations #{Time.now}"
# Generate permutations
class Array
def permute(i = 0, *a)
return [a] if i == size
self[i].map { |x| permute(i+1, *(a + [x])) }.inject([]) { |m,x| m + x }
end
end
Permutations = Combinations.permute
# Container to store results
bestResult = []
5.times { |x| bestResult << ([0,0,0,0,0].dup << "Pop,Food,Gold,Hammers,Total".split(',')[x]) }
# Integrate across t
PermutationResults = Permutations.map { |p|
result = [1,0,0,0]
TurnsToSimulate.times { |t|
result = [currentSize(result[1])] + (1..3).to_a.inject([]) { |i,n|
i + [result[n] + p[result[0]][n]]
}
result[0] = tiles.size if result[0] > tiles.size # Cap size
}
result
}.inject(bestResult) { |acc, node|
node[4] = 0
(1..3).each { |x| node[4] += node[x] }
5.times{ |x| acc[x] = node + [acc[x][acc.size]] if node[x] > acc[x][x] }
acc
}
puts "Writing results"
# Write results to disk
File::open("output.txt", "w") { |file|
file << "# Combinations\n"
Combinations.each { |set|
set.each { |comb| file << " " << comb.join(" ") << "\n" }
file << "\n"
}
file << "# Permutations\n"
Permutations.each { |pset|
pset.each { |p| file << " " << p.join(" ") << "\n" }
file << "\n"
}
file << "# Results\n"
PermutationResults.each { |res| file << " " << res.join(" ") << "\n" }
}
puts "Finished #{Time.now}"
I wanted a way to know which tiles are the best for a city to work in the long-term, so I threw together this proof-of-concept to see what was possible. The algorithm is fairly horrendous right now, so I wouldn't advise having more than about 8 tiles per city, or maybe running it overnight. It's written in Ruby, which I don't expect to be a popular language round here, but I like it, so that's what I used. I'd like to expand the number of things it takes into account, such as happiness or whipping. At the moment the tile values are just randomly generated - no desert or plains here, just a bunch of numbers.
TurnsToSimulate = 80
tiles = []
tiles << "plains,none,forest,none"
tiles << "grassland,none,none,rice"
tiles << "grassland,none,farm,rice"
tiles << "plains,none,none,none"
# [Food,Gold,Hammers]
Attributes = [
base = {
"none" => [0,0,0],
"plains" => [1,0,2],
"grassland" => [2,0,1],
"tundra" => [1,0,0],
"floodplains" => [3,0,0]
},
feature = {
"none" => [0,0,0],
"hills" => [0,0,1]
},
overlay = {
"none" => [0,0,0],
"forest" => [0,0,1],
"farm" => [lambda { |tile|
bonus = 1
case tile[4]
when "rice"
bonus = 2
end
bonus
},0,0]
},
special = {
"none" => [0,0,0],
"rice" => [1,0,0]
}
]
tiles.map! { |tile|
v = tile.split(',')
Attributes.inject([0,0,0]) { |total, type|
res = type[v.shift]
3.times { |t| total[t] += res[t].class == Proc ? res[t].call(tile.split(',')) : res[t] }
total
}
}
# Calc food required to achieve a pop size
def inc(size)
10 * 2 ** size - 20 # Or similar
end
# Given a total amount of food, calc current size
def currentSize(food)
s = 1
s += 1 while food > inc(s)
s
end
puts "Starting combinations #{Time.now}"
# Generate combinations
Combinations = (1..tiles.size).inject([[]]) { |acc, node| # Combinations (ints)
acc.inject([]) { |acc, injectedNodes| acc + [injectedNodes, injectedNodes + [node]] }
}.map { |c|
c.inject([0,2,1,2]) { |acc, node| # Combinations (tiles)
(0..2).inject([acc[0].succ]) { |i,n| i + [acc[n.succ] + tiles[node-1][n]] }
}
}.uniq.sort.inject([]) { |acc, node| # Tranches
acc + [acc.size <= node[0] ? [node] : acc.pop + [node]]
}.each { |set|
set.delete_if { |test| # Prune
set.inject(false) { |acc, comp|
acc or (!test.equal?(comp) and ((1..3).inject(true) { |i,n| test[n] <= comp[n] and i } and acc = true))
}
}
}
puts "Starting permutations #{Time.now}"
# Generate permutations
class Array
def permute(i = 0, *a)
return [a] if i == size
self[i].map { |x| permute(i+1, *(a + [x])) }.inject([]) { |m,x| m + x }
end
end
Permutations = Combinations.permute
# Container to store results
bestResult = []
5.times { |x| bestResult << ([0,0,0,0,0].dup << "Pop,Food,Gold,Hammers,Total".split(',')[x]) }
# Integrate across t
PermutationResults = Permutations.map { |p|
result = [1,0,0,0]
TurnsToSimulate.times { |t|
result = [currentSize(result[1])] + (1..3).to_a.inject([]) { |i,n|
i + [result[n] + p[result[0]][n]]
}
result[0] = tiles.size if result[0] > tiles.size # Cap size
}
result
}.inject(bestResult) { |acc, node|
node[4] = 0
(1..3).each { |x| node[4] += node[x] }
5.times{ |x| acc[x] = node + [acc[x][acc.size]] if node[x] > acc[x][x] }
acc
}
puts "Writing results"
# Write results to disk
File::open("output.txt", "w") { |file|
file << "# Combinations\n"
Combinations.each { |set|
set.each { |comb| file << " " << comb.join(" ") << "\n" }
file << "\n"
}
file << "# Permutations\n"
Permutations.each { |pset|
pset.each { |p| file << " " << p.join(" ") << "\n" }
file << "\n"
}
file << "# Results\n"
PermutationResults.each { |res| file << " " << res.join(" ") << "\n" }
}
puts "Finished #{Time.now}"