From 961dcc3a252ebe807df13c9642ac7cf2dd461aa9 Mon Sep 17 00:00:00 2001 From: Pierre Wessman <4029607+pierrewessman@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:54:41 +0200 Subject: [PATCH] AI --- src/entities/player.js | 271 ++++++++++++++++++++++++++++++++++----- src/systems/ai-system.js | 33 ++++- 2 files changed, 273 insertions(+), 31 deletions(-) diff --git a/src/entities/player.js b/src/entities/player.js index d47d913..09881a6 100644 --- a/src/entities/player.js +++ b/src/entities/player.js @@ -141,25 +141,36 @@ class Player { const nearestOpponent = this.findNearestPlayer(opponents); const distanceToGoal = this.position.distance(enemyGoal); + const distanceToNearestOpponent = nearestOpponent ? this.position.distance(nearestOpponent.position) : Infinity; - if (distanceToGoal < 200 && Math.random() < 0.3) { - this.shoot(puck, enemyGoal); - } else if (nearestOpponent && this.position.distance(nearestOpponent.position) < 80) { - const bestTeammate = this.findBestPassTarget(teammates, opponents); - if (bestTeammate && Math.random() < 0.7) { - this.pass(puck, bestTeammate); - } else { - this.moveToPosition(enemyGoal); + // More aggressive shooting when in scoring position + if (distanceToGoal < 250 && this.hasGoodShootingAngle(enemyGoal, opponents)) { + if (distanceToGoal < 150 || Math.random() < 0.5) { + this.shoot(puck, enemyGoal); + return; } - } else { - this.moveToPosition(enemyGoal); } + + // If under heavy pressure, look for a pass first + if (distanceToNearestOpponent < 60) { + const bestTeammate = this.findBestPassTarget(teammates, opponents); + if (bestTeammate && Math.random() < 0.8) { + this.pass(puck, bestTeammate); + return; + } + } + + // Default behavior: advance aggressively toward goal + this.advanceTowardGoal(enemyGoal, opponents, gameState.rink); } behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck) { const puckOwner = opponents.find(p => p.state.hasPuck) || teammates.find(p => p.state.hasPuck); + const isClosestToPuck = this.isClosestPlayerToPuck(puck, teammates); + const allPlayers = [...teammates, ...opponents, this]; - if (!puckOwner && distanceToPuck < 200) { + if (!puckOwner && isClosestToPuck && distanceToPuck < 200) { + // Only chase if this player is closest to the puck on their team this.chasePuck(puck); } else if (puckOwner && puckOwner.team !== this.team) { if (distanceToPuck < 150 && Math.random() < 0.2) { @@ -168,7 +179,7 @@ class Player { this.defendPosition(gameState, puckOwner); } } else { - this.moveToFormationPosition(gameState); + this.moveToFormationPosition(gameState, puck, allPlayers); } } @@ -254,30 +265,119 @@ class Player { this.aiState.behavior = 'defending'; } - moveToFormationPosition(gameState) { - this.moveToPosition(this.getFormationPosition(gameState)); + moveToFormationPosition(gameState, puck, players) { + this.moveToPosition(this.getFormationPosition(gameState, puck, players)); this.aiState.behavior = 'formation'; } - getFormationPosition(gameState) { + getFormationPosition(gameState, puck, players) { const side = this.team === 'home' ? -1 : 1; - const centerX = gameState.rink.centerX; - const centerY = gameState.rink.centerY; + const rink = gameState.rink; - switch (this.role) { - case 'C': - return new Vector2(centerX + side * 100, centerY); - case 'LW': - return new Vector2(centerX + side * 150, centerY - 100); - case 'RW': - return new Vector2(centerX + side * 150, centerY + 100); - case 'LD': - return new Vector2(centerX + side * 200, centerY - 80); - case 'RD': - return new Vector2(centerX + side * 200, centerY + 80); - default: - return this.homePosition; + // Determine if team is attacking or defending based on puck position and possession + const puckOwner = players.find(p => p.state.hasPuck); + const isAttacking = this.determineTeamState(puck, puckOwner, rink); + + // Get base formation position based on attacking/defending state + return this.getContextualPosition(rink, side, isAttacking, puck); + } + + determineTeamState(puck, puckOwner, rink) { + const homeAttackingZone = rink.width * 0.67; // Right side for home team + const awayAttackingZone = rink.width * 0.33; // Left side for away team + + // If teammate has puck, team is likely attacking + if (puckOwner && puckOwner.team === this.team) { + return true; } + + // If opponent has puck, team is defending + if (puckOwner && puckOwner.team !== this.team) { + return false; + } + + // No possession - determine by puck location + if (this.team === 'home') { + return puck.position.x > homeAttackingZone; + } else { + return puck.position.x < awayAttackingZone; + } + } + + getContextualPosition(rink, side, isAttacking, puck) { + const centerY = rink.centerY; + const puckInfluenceX = (puck.position.x - rink.centerX) * 0.3; // Follow puck horizontally + const puckInfluenceY = (puck.position.y - centerY) * 0.2; // Follow puck vertically (less influence) + + let baseX, baseY; + + if (isAttacking) { + // Attacking formation - push forward toward opponent's goal + const attackZone = this.team === 'home' ? rink.width * 0.75 : rink.width * 0.25; + + switch (this.role) { + case 'C': + baseX = attackZone; + baseY = centerY; + break; + case 'LW': + baseX = attackZone - 50; + baseY = centerY - 120; + break; + case 'RW': + baseX = attackZone - 50; + baseY = centerY + 120; + break; + case 'LD': + baseX = attackZone - 150; + baseY = centerY - 80; + break; + case 'RD': + baseX = attackZone - 150; + baseY = centerY + 80; + break; + default: + return this.homePosition; + } + } else { + // Defensive formation - fall back toward own goal + const defenseZone = this.team === 'home' ? rink.width * 0.25 : rink.width * 0.75; + + switch (this.role) { + case 'C': + baseX = defenseZone; + baseY = centerY; + break; + case 'LW': + baseX = defenseZone + side * 50; + baseY = centerY - 100; + break; + case 'RW': + baseX = defenseZone + side * 50; + baseY = centerY + 100; + break; + case 'LD': + baseX = defenseZone + side * 100; + baseY = centerY - 60; + break; + case 'RD': + baseX = defenseZone + side * 100; + baseY = centerY + 60; + break; + default: + return this.homePosition; + } + } + + // Apply puck influence for more dynamic positioning + baseX += puckInfluenceX; + baseY += puckInfluenceY; + + // Keep positions within rink bounds + baseX = Math.max(50, Math.min(rink.width - 50, baseX)); + baseY = Math.max(50, Math.min(rink.height - 50, baseY)); + + return new Vector2(baseX, baseY); } findNearestPlayer(players) { @@ -326,6 +426,117 @@ class Player { return bestTarget; } + isClosestPlayerToPuck(puck, teammates) { + // Check if this player (excluding goalies) is closest to the puck on their team + if (this.role === 'G' || this.state.hasPuck) return false; + + const myDistance = this.position.distance(puck.position); + + // Include self in the list to compare against + const allTeamPlayers = [this, ...teammates.filter(t => t.role !== 'G' && !t.state.hasPuck)]; + + // Find the closest player + let closestDistance = Infinity; + let closestPlayer = null; + + allTeamPlayers.forEach(player => { + const distance = player.position.distance(puck.position); + if (distance < closestDistance) { + closestDistance = distance; + closestPlayer = player; + } + }); + + return closestPlayer === this; + } + + hasGoodShootingAngle(goalPosition, opponents) { + // Check if there's a clear line to goal (simplified check) + const directionToGoal = goalPosition.subtract(this.position).normalize(); + + // Check if opponents are blocking the shot + for (let opponent of opponents) { + const directionToOpponent = opponent.position.subtract(this.position); + const distanceToOpponent = directionToOpponent.magnitude(); + + // Skip distant opponents + if (distanceToOpponent > 150) continue; + + const directionToOpponentNorm = directionToOpponent.normalize(); + const dot = directionToGoal.dot(directionToOpponentNorm); + + // If opponent is roughly in line with goal and close enough to block + if (dot > 0.8 && distanceToOpponent < 80) { + return false; + } + } + + return true; + } + + advanceTowardGoal(goalPosition, opponents, rink) { + // Create an intelligent path toward the goal + let targetPosition = goalPosition.copy(); + + // Adjust approach based on position and opponents + const directionToGoal = goalPosition.subtract(this.position).normalize(); + const distanceToGoal = this.position.distance(goalPosition); + + // If close to goal, move more directly + if (distanceToGoal < 200) { + this.moveToPosition(goalPosition); + return; + } + + // Look for the best path around opponents + const pathAdjustment = this.findBestPathToGoal(goalPosition, opponents, rink); + targetPosition = targetPosition.add(pathAdjustment); + + // Keep target in bounds + targetPosition.x = Math.max(50, Math.min(rink.width - 50, targetPosition.x)); + targetPosition.y = Math.max(50, Math.min(rink.height - 50, targetPosition.y)); + + this.moveToPosition(targetPosition); + } + + findBestPathToGoal(goalPosition, opponents, rink) { + const currentPos = this.position; + const adjustment = new Vector2(0, 0); + + // Check for opponents blocking direct path + const directPath = goalPosition.subtract(currentPos).normalize(); + + opponents.forEach(opponent => { + const toOpponent = opponent.position.subtract(currentPos); + const distanceToOpponent = toOpponent.magnitude(); + + // Only consider opponents that might interfere + if (distanceToOpponent > 120 || distanceToOpponent < 30) return; + + const toOpponentNorm = toOpponent.normalize(); + const dot = directPath.dot(toOpponentNorm); + + // If opponent is somewhat in the path + if (dot > 0.5) { + // Calculate avoidance vector (perpendicular to opponent direction) + const avoidVector = new Vector2(-toOpponentNorm.y, toOpponentNorm.x); + const influence = (120 - distanceToOpponent) / 120; // Stronger influence when closer + + // Choose direction based on field position to avoid going out of bounds + if (currentPos.y < rink.height / 2) { + adjustment.y += Math.abs(avoidVector.y) * influence * 30; + } else { + adjustment.y -= Math.abs(avoidVector.y) * influence * 30; + } + + // Slight lateral adjustment + adjustment.x += avoidVector.x * influence * 15; + } + }); + + return adjustment; + } + keepInBounds() { this.position.x = Math.max(this.radius, Math.min(1000 - this.radius, this.position.x)); this.position.y = Math.max(this.radius, Math.min(600 - this.radius, this.position.y)); diff --git a/src/systems/ai-system.js b/src/systems/ai-system.js index 080364c..5acbd82 100644 --- a/src/systems/ai-system.js +++ b/src/systems/ai-system.js @@ -37,6 +37,10 @@ class AISystem { const puckOwner = this.findPuckOwner(players); const gameContext = this.analyzeGameContext(players, puck, gameState, puckOwner); + // Find closest player to puck for each team (excluding goalies) + const closestPlayers = this.findClosestPlayersByTeam(players, puck); + gameContext.closestPlayers = closestPlayers; + players.forEach(player => { if (player.role !== 'G') { this.updatePlayerAI(player, gameContext); @@ -48,6 +52,31 @@ class AISystem { return players.find(player => player.state.hasPuck) || null; } + findClosestPlayersByTeam(players, puck) { + const homeClosest = { player: null, distance: Infinity }; + const awayClosest = { player: null, distance: Infinity }; + + players.forEach(player => { + // Skip goalies and players with the puck + if (player.role === 'G' || player.state.hasPuck) return; + + const distance = player.position.distance(puck.position); + + if (player.team === 'home' && distance < homeClosest.distance) { + homeClosest.player = player; + homeClosest.distance = distance; + } else if (player.team === 'away' && distance < awayClosest.distance) { + awayClosest.player = player; + awayClosest.distance = distance; + } + }); + + return { + home: homeClosest.player, + away: awayClosest.player + }; + } + analyzeGameContext(players, puck, gameState, puckOwner) { const context = { puckOwner, @@ -151,6 +180,7 @@ class AISystem { updatePlayerBehavior(player, context) { const distanceToPuck = player.position.distance(context.puckPosition); const isNearPuck = distanceToPuck < 100; + const isClosestToPuck = context.closestPlayers[player.team] === player; if (player.state.hasPuck) { player.aiState.behavior = 'puck_carrier'; @@ -161,7 +191,8 @@ class AISystem { } else if (context.puckOwner && context.puckOwner.team !== player.team) { player.aiState.behavior = 'pressure'; this.executePressureBehavior(player, context); - } else if (isNearPuck) { + } else if (!context.puckOwner && isClosestToPuck && isNearPuck) { + // Only allow the closest player to chase loose pucks player.aiState.behavior = 'chase'; this.executeChaseBehavior(player, context); } else {