Skip to content

Instantly share code, notes, and snippets.

@roehst
Last active December 3, 2022 17:48
Show Gist options
  • Save roehst/bbde0c1c16688080d439f20bd05752b4 to your computer and use it in GitHub Desktop.
Save roehst/bbde0c1c16688080d439f20bd05752b4 to your computer and use it in GitHub Desktop.
Soccer Tactics based on Minimal Spanning Trees

I now this is very naive, but after watching a few soccer games I was vary curious to see if minimal spanning trees between soccer players could somehow model soccer tactics.

The file above can be opened in a browser, with no dependencies.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Minimum Spanning Trees in Soccer Tactics</title>
<style>
/* The soccer field should a 1050/680 px green rectangle */
/* inside which we will place players */
svg#soccer-field {
background-color: green;
width: 1050px;
height: 680px;
}
circle.soccer-player {
fill: darkgreen;
stroke: lightgreen;
opacity: 50%;
stroke-width: 2px;
filter: drop-shadow(0 0 2px black);
}
</style>
</head>
<body>
<!-- Coordinates should be given in terms of METERS -->
<!-- 1 METER = 10px -->
<svg id="soccer-field" viewBox="0 0 105 68">
<!-- The goalie, numbered 11 -->
<circle id="goalie" class="soccer-player" cx="10" cy="25" r="1" />
<!-- Four players in the back, numbered 1 2 3 4 -->
<circle id="player-1" class="soccer-player" cx="30" cy="10" r="1" />
<circle id="player-2" class="soccer-player" cx="30" cy="20" r="1" />
<circle id="player-3" class="soccer-player" cx="30" cy="30" r="1" />
<circle id="player-4" class="soccer-player" cx="30" cy="40" r="1" />
<!-- Four players in the middle, numbered 5 6 7 8 -->
<circle id="player-5" class="soccer-player" cx="50" cy="10" r="1" />
<circle id="player-6" class="soccer-player" cx="50" cy="20" r="1" />
<circle id="player-7" class="soccer-player" cx="50" cy="30" r="1" />
<circle id="player-8" class="soccer-player" cx="50" cy="40" r="1" />
<!-- Two players in the front, numbered 9 10 -->
<circle id="player-9" class="soccer-player" cx="70" cy="20" r="1" />
<circle id="player-10" class="soccer-player" cx="70" cy="30" r="1" />
</svg>
<script>
function noise() {const players = document.querySelectorAll("circle.soccer-player");
// Move each player by a tiny, random amount by mutating cx/cy
// attributes of each player
players.forEach((player) => {
const cx = parseFloat(player.getAttribute("cx"));
const cy = parseFloat(player.getAttribute("cy"));
const dx = Math.random() * 0.2 - 0.05;
const dy = Math.random() * 0.2 - 0.05;
player.setAttribute("cx", cx + dx);
player.setAttribute("cy", cy + dy);
});
drawMinimalSpanningTree();
}
setInterval(noise, 50);
function drawMinimalSpanningTree() {
const players = document.querySelectorAll("circle.soccer-player");
const edges = [];
players.forEach((player1) => {
players.forEach((player2) => {
if (player1 === player2) return;
const cx1 = parseFloat(player1.getAttribute("cx"));
const cy1 = parseFloat(player1.getAttribute("cy"));
const cx2 = parseFloat(player2.getAttribute("cx"));
const cy2 = parseFloat(player2.getAttribute("cy"));
const dx = cx1 - cx2;
const dy = cy1 - cy2;
const distance = Math.sqrt(dx * dx + dy * dy);
edges.push({
player1,
player2,
distance,
selected: false
});
});
})
// Select the edges belonging to the minimal spanning tree
// using Kruskal's algorithm
edges.sort((a, b) => a.distance - b.distance);
const components = new Map();
edges.forEach((edge) => {
const component1 = components.get(edge.player1);
const component2 = components.get(edge.player2);
if (component1 === undefined && component2 === undefined) {
// Both players are in different components
// Create a new component
const component = new Set([edge.player1, edge.player2]);
components.set(edge.player1, component);
components.set(edge.player2, component);
edge.selected = true;
} else if (component1 === undefined) {
// Player 1 is in a component, player 2 is not
// Add player 1 to the component of player 2
component2.add(edge.player1);
components.set(edge.player1, component2);
edge.selected = true;
} else if (component2 === undefined) {
// Player 2 is in a component, player 1 is not
// Add player 2 to the component of player 1
component1.add(edge.player2);
components.set(edge.player2, component1);
edge.selected = true;
} else if (component1 !== component2) {
// Both players are in different components
// Merge the two components
component1.forEach((player) => {
components.set(player, component2);
component2.add(player);
});
edge.selected = true;
}
});
// Draw selected edges as new
const newEdges = document.createElementNS("http://www.w3.org/2000/svg", "g");
edges.forEach((edge) => {
if (edge.selected) {
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", edge.player1.getAttribute("cx"));
line.setAttribute("y1", edge.player1.getAttribute("cy"));
line.setAttribute("x2", edge.player2.getAttribute("cx"));
line.setAttribute("y2", edge.player2.getAttribute("cy"));
line.setAttribute("stroke", "white");
line.setAttribute("stroke-width", "0.1");
newEdges.appendChild(line);
}
});
// Delete all edges in soccer-field
document.querySelectorAll("line").forEach((g) => g.remove());
// Add new edges to soccer-field
document.querySelector("svg").appendChild(newEdges);
}
// Make players dragable
const players = document.querySelectorAll("circle.soccer-player");
// Apply a factor to the mouse movement to make dragging more natural
players.forEach((player) => {
player.addEventListener("mousedown", (event) => {
const cx = parseFloat(player.getAttribute("cx"));
const cy = parseFloat(player.getAttribute("cy"));
const mousemove = (event) => {
player.setAttribute("cx", event.clientX / 10);
player.setAttribute("cy", event.clientY / 10);
drawMinimalSpanningTree();
};
const mouseup = () => {
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
};
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
});
});
</script>
</div>
</body>
</html>
@roehst
Copy link
Author

roehst commented Dec 3, 2022

It looks like this currently:

image

@roehst
Copy link
Author

roehst commented Dec 3, 2022

Any one willing to hack along?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment