Football AI

In this example below you will see how to do a Football AI with some HTML / CSS and Javascript

Thumbnail
This awesome code was written by Ni55aN, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright Ni55aN ©
  • HTML
  • CSS
  • JavaScript
<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  <title>Football AI</title>
  
  
  
      <link rel="stylesheet" href="css/style.css">

  
</head>

<body>

  <div class="buttons-group"><button onclick="newGame()">New game</button><button onclick="playing=true">Start</button><button onclick="playing=false">Pause</button></div>
<field>
  <img class="background"/>
  <svg class="content" viewBox="0 0 100 64">
  </svg>
</field>
  <script src='https://d3js.org/d3.v4.min.js'></script>

  

    <script  src="js/index.js"></script>




</body>

</html>

/*Downloaded from https://www.codeseek.co/Ni55aN/football-ai-QqVgbN */
body {
  padding: 0;
  margin: 0;
}
body .buttons-group {
  position: absolute;
  left: 0;
  top: 0;
  text-align: center;
  width: 100%;
  z-index: 2;
}
body field {
  display: block;
  position: relative;
  width: 100%;
  padding-top: 64%;
}
body field img.background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: url("https://thumbs.dreamstime.com/t/seamlessly-green-grasses-texture-background-grass-all-field-activities-just-like-game-baseball-football-soccer-outdoor-57685139.jpg"), url("https://upload.wikimedia.org/wikipedia/commons/b/b9/Soccer_field_-_empty.svg");
  background-blend-mode: lighten;
  background-size: auto, cover;
}
body field .content {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
body field .content > * {
  position: absolute;
}


/*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();

Comments