Skip to content

Instantly share code, notes, and snippets.

@ostrowr
Last active September 6, 2020 18:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ostrowr/b011fc73aab1d54994838cc6c8146847 to your computer and use it in GitHub Desktop.
Save ostrowr/b011fc73aab1d54994838cc6c8146847 to your computer and use it in GitHub Desktop.
The Battle For Riddler Nation, Round 2
license: mit

Simulated annealing, attempting to find the troop deployment with the best winning percentage from the last battle.

Every step, the algorithm moves one soldier from a random castle to another. If this improves the winning percentage, it continues from the new deployment. To avoid local minima, it takes 10% of random switches that don't improve the winning percentage.

In order to defend against people with similar strategies, this algorithm adds its own solutions to the list of deployments as it finds them. The "combined winning percentage" is

.5 * (winning percentage against deployments from battle 1) + .5 * (winning percentage against all deployments, including intermediate solutions.)

With luck, this finds solutions with > 90% winning percentages.

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
ROUND_1_URL = 'https://raw.githubusercontent.com/fivethirtyeight/data/master/riddler-castles/castle-solutions.csv';
var initial_deployment = [10,10,10,10,10,10,10,10,10,10];
var width = 960;
var height = 500;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("font-family", "Arial");
var bars = svg.append("g").selectAll("rect")
.data(initial_deployment)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (width / initial_deployment.length);
})
.attr("y", height - 1)
.attr("width", width / initial_deployment.length - 5)
.attr("height", d=>d);
var text = svg.append("g").selectAll("barText")
.data(initial_deployment)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", (d, i) => (i + .5) * (width / initial_deployment.length))
.attr("y", height - 1);
var best_bars = svg.append("g").selectAll("rect")
.data(initial_deployment)
.enter()
.append("rect")
.attr("fill", "blue")
.attr("x", (d, i) => i * (width / initial_deployment.length))
.attr("y", height-1)
.attr("width", width / initial_deployment.length - 5)
.attr("height", 3);
var best_winning_percentage = svg.append("text")
.attr("text-anchor", "left")
.attr("x", 20)
.attr("y", 20)
.text("Best Combined Winning Percentage: 0")
var current_winning_percentage = svg.append("text")
.attr("text-anchor", "left")
.attr("x", 20)
.attr("y", 40)
.text("Current Combined Winning Percentage: 0")
var best_deployment = svg.append("text")
.attr("text-anchor", "left")
.attr("x", 20)
.attr("y", 60)
.text("Best Deployment: " + initial_deployment)
function anneal(play, games, gl2, best, saved_best, saved_play) {
var old_play = play.slice()
var attempt = swap(play)
var new_score = (score(attempt, games) + score(attempt, gl2)) / 2.
if (Math.random() < .1 || new_score > best) {
best = new_score
play = attempt
if (best > saved_best) {
saved_best = best
saved_play = play
}
gl2.push(attempt)
}
var n = 0
best_winning_percentage.text("Best Combined Winning Percentage: " + saved_best.toFixed(4))
current_winning_percentage.text("Current Combined Winning Percentage: " + new_score.toFixed(4))
best_deployment.text("Best Deployment: " + saved_play)
best_bars.transition()
.duration(90)
.attr("y", (d, i) => height - (saved_play[i] * 4));
text.attr("y", (d, i) => height - (play[i] * 4) - 10)
.text((d, i) => play[i]);
bars.transition()
.duration(90)
.attr("fill", function(d, i) {
if (old_play[i] == attempt[i]) {
return "rgb(68, 68, 68)"
}
if (old_play[i] > attempt[i]) {
return "rgb(204, 59, 59)"
}
if (old_play[i] < attempt[i]) {
return "rgb(106, 173, 65)"
}
})
.attr("y", (d, i) => height - (play[i] * 4))
.attr("height", (d, i) => play[i] * 4)
.on("end", function() {
n++;
if (n == play.length) {
anneal(play, games, gl2, best, saved_best, saved_play)
}
}.bind(this))
}
function a_wins(a, b){
var a_pts = 0
var b_pts = 0
for (var i = 0; i < 10; i++) {
if (a[i] > b[i]) {
a_pts += i + 1
}
else if (b[i] > a[i]) {
b_pts += i + 1
}
}
if (a_pts > b_pts) {
return true
}
return false
}
function score(play, games) {
var did_win = games.map(game => a_wins(play, game))
wins = did_win.reduce(function(a, b) {return a + b})
return wins / did_win.length
}
function swap(play) {
var p = play.slice()
var take_indices_available = p.map((v, i) => v > 0 ? i : -1)
.filter(v => v >= 0)
take_index = take_indices_available[
Math.floor(Math.random() * take_indices_available.length)
]
give_index = Math.floor(Math.random() * p.length)
p[take_index] -= 1
p[give_index] += 1
return p
}
d3.csv(ROUND_1_URL, function(data) {
games = data.map(function(row) {
r = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
return r.map(castle => parseInt(row["Castle " + castle]))
})
anneal(initial_deployment, games, games.slice(), 0, 0, initial_deployment)
})
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment