/*Downloaded from https://www.codeseek.co/Ni55aN/football-ai-QqVgbN */
class Scene {
constructor(objects) {
this.container = d3.select("svg.content");
this.bbox = this.container
.attr("viewBox")
.split(" ")
.map(a => +a);
this.borders = [3.5, 3];
this.objects = objects;
this.initView();
this.initPhysics();
}
initView() {
var data = this.objects;
var self = this;
this.container
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.call(
d3
.drag()
.on("drag", function(d) {
var x = d.x + d3.event.dx;
var y = d.y + d3.event.dy;
d.direction = Math.atan2(d3.event.dy, d3.event.dx);
d.velocity = Math.hypot(d3.event.dx, d3.event.dy);
/*d3.select(this)
.attr("cx", (d.x = x))
.attr("cy", (d.y = y));*/
self.updatePhysics();
self.updateView();
})
.on("start", function(d) {
d3.select(this).raise();
})
);
this.updateView();
}
updateView() {
var data = this.objects;
var upd = this.container
.selectAll("circle")
.data(data)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
})
.attr("fill", function(d) {
return d.fill;
});
}
initPhysics() {
this.objects = this.objects.map(o => {
o.velocity = typeof o.velocity === "number" ? o.velocity : 0;
o.direction = typeof o.direction === "number" ? o.direction : 0;
o.maxVelocity = 0.3;
return o;
});
}
decomposeSpeedRelatedToAngle(ball, angle) {
return {
x: ball.velocity * Math.cos(ball.direction - angle),
y: ball.velocity * Math.sin(ball.direction - angle)
};
}
normalizeAngle(radians, base) {
if (radians > base) return radians - base;
if (radians < 0) return radians + base;
return radians;
}
normalizeDirection(radians) {
return this.normalizeAngle(radians, 2 * Math.PI);
}
detectCollision(b1) {
this.objects.map(b2 => {
let objDist = Math.hypot(b1.x - b2.x, b1.y - b2.y);
if (objDist > b1.r + b2.r) return;
const collision = {
x: (b1.x + b2.x) / 2,
y: (b1.y + b2.y) / 2,
distance: (b1.r + b2.r - objDist) / 2,
angle: Math.atan2(b2.y - b1.y, b2.x - b1.x)
};
const newSpeed1 = this.decomposeSpeedRelatedToAngle(b1, collision.angle);
const newSpeed2 = this.decomposeSpeedRelatedToAngle(b2, collision.angle);
const ballsSum = b1.mass + b2.mass;
const ballsDiff = b1.mass - b2.mass;
const finalSpeed1X =
(ballsDiff * newSpeed1.x + ballsSum * newSpeed2.x) / ballsSum;
const finalSpeed2X =
(ballsSum * newSpeed1.x - ballsDiff * newSpeed2.x) / ballsSum;
newSpeed1.x = finalSpeed1X;
newSpeed2.x = finalSpeed2X;
b1.velocity = Math.hypot(newSpeed1.x, newSpeed1.y);
b2.velocity = Math.hypot(newSpeed2.x, newSpeed2.y);
if (b1.velocity > b1.maxVelocity) b1.velocity = b1.maxVelocity;
if (b2.velocity > b2.maxVelocity) b2.velocity = b2.maxVelocity;
b1.direction = Math.atan2(newSpeed1.y, newSpeed1.x) + collision.angle;
b2.direction = Math.atan2(newSpeed2.y, newSpeed2.x) + collision.angle;
b1.direction = this.normalizeDirection(b1.direction);
b2.direction = this.normalizeDirection(b2.direction);
if (collision.distance <= 0) return;
const diff = {
x: collision.distance * Math.cos(collision.angle),
y: collision.distance * Math.sin(collision.angle)
};
b1.x -= diff.x;
b1.y -= diff.y;
b2.x += diff.x;
b2.y += diff.y;
});
}
updatePhysics() {
this.objects.map(o => {
this.detectCollision(o);
if (o.bounces) {
let dx = Math.cos(o.direction);
let dy = Math.sin(o.direction);
if (
o.x + dx < this.bbox[0] + this.borders[0] ||
o.x + dx > this.bbox[2] - this.borders[0]
) {
this.goal =
o.x + dx < this.bbox[0] + this.borders[0] ? "left" : "right";
o.direction = Math.PI - o.direction;
}
if (
o.y + dy < this.bbox[1] + this.borders[1] ||
o.y + dx > this.bbox[3] - this.borders[1]
)
o.direction = 2 * Math.PI - o.direction;
}
var dx = Math.cos(o.direction);
var dy = Math.sin(o.direction);
var v = o.velocity;
o.velocity *= 0.99;
o.x += v * dx;
o.y += v * dy;
});
}
}
var playing = false;
var scene, yourTeam, opponentsTeam, balls;
var testCase = null;
function newGame() {
var data = testCase(activeTestCase);
yourTeam = data[0].map(a => {
a.fill = "#D50000";
a.r = 1;
a.mass = 5;
return a;
});
opponentsTeam = data[1].map(a => {
a.fill = "#1565C0";
a.r = 1;
a.mass = 5;
return a;
});
balls = data[2].map(a => {
a.fill = "white";
a.r = 1;
a.bounces = true;
a.mass = 0.05;
return a;
});
scene = new Scene([...yourTeam, ...opponentsTeam, ...balls]);
}
function start() {
if (playing) {
var ball = balls[0];
var field = scene.bbox;
yourTeam.map(p => {
var teammates = yourTeam.filter(t => t !== p);
managePlayer(p, ball, teammates, opponentsTeam, field);
});
opponentsTeam.map(p => {
p.velocity = p.maxVelocity;
p.direction = Math.atan2(ball.y - p.y, ball.x - p.x);
});
scene.updatePhysics();
}
if (scene.goal) playing = false;
scene.updateView();
requestAnimationFrame(start);
}
var testCase = function(id) {
switch (id) {
case 0: // мяч летит в наши ворота, следом бежит игрок
return [
[{ x: 55, y: 15 }],
[],
[{ x: 40, y: 15, velocity: 0.3, direction: Math.PI }]
];
case 1: // мяч летит в наши ворота, с фланга бежит игрок
return [
[{ x: 15, y: 40 }],
[],
[{ x: 40, y: 15, velocity: 0.4, direction: Math.PI }]
];
case 2: // противник ведет мяч в наши ворота
return [[{ x: 5, y: 40 }], [{ x: 45, y: 15 }], [{ x: 40, y: 15 }]];
case 3: // два противника ведет мяч в наши ворота
return [
[{ x: 5, y: 45 }],
[{ x: 44, y: 14 }, { x: 45, y: 16 }],
[{ x: 40, y: 15 }]
];
case 4:
return [
[{ x: 5, y: 45, role: 'defender'}, { x: 60, y: 15 }],
[{ x: 44, y: 14 }, { x: 37, y: 40 }],
[{ x: 40, y: 15 }]
];
default:
return [
[{ x: 35, y: 15 }, { x: 10, y: 32, role: 'defender' }, { x: 35, y: 49 }],
[{ x: 75, y: 49 }, { x: 90, y: 32 }, { x: 75, y: 15 }],
[{ x: 50, y: 32 }]
];
}
};
function ballPredict(ball, iters = 1) {
var dx = Math.cos(ball.direction);
var dy = Math.sin(ball.direction);
var v = ball.velocity;
return { x: ball.x + iters * v * dx, y: ball.y + iters * v * dy };
}
function aheadBall(p,ball){
var bpos = ballPredict(ball,1);
var bv = ball.velocity;
var pv = p.velocity;
var ps = distance(p,bpos);
var tp = ps/pv;
return ballPredict(ball,tp);
}
var activeTestCase = 1;
function managePlayer(p, ball, teammates, opponents, field) {
var moveTo = { x: ball.x, y: ball.y };
var nextBall = ballPredict(ball);
var sumr = ball.r + p.r;
var bdir = scene.normalizeDirection(ball.direction);
// мяч летит у наши ворота, обогнать
if (Math.cos(bdir) < 0 && p.x + p.r> nextBall.x) { /// +
// test case 0
moveTo.y += sumr;
moveTo.x -= sumr;
}
else{
// направить мяч у ворота противника
moveTo.x -= Math.cos(directionTo(p, ball)) * p.r*0.7; // test case 1,2 +
/// опередить мяч и выбить
if(Math.cos(bdir)<0){
moveTo = aheadBall(p,ball);
if(p.x>ball.x)
moveTo.x -= p.r;
}
}
if(p.role==="defender")
{
var thField = field[2]/3;
if(moveTo.x>thField)
moveTo.x = thField;
}
var p2bdir = directionTo(p, moveTo);
p.direction = p2bdir;
p.velocity = 1 * p.maxVelocity;
}
function personsNear(persons, player, threshold) {
return persons.filter(pl => distance(player, pl) < threshold);
}
function directionTo(startPoint, endPoint) {
return Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x);
}
function distance(a, b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
newGame();
start();