Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stable AI improvements #814

Merged
merged 58 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a1f18a2
Run AI at the beginning of the game tick
SpacialCircumstances May 26, 2022
945c906
Merge branch 'dev' into feature/ai-improvements-3
SpacialCircumstances May 29, 2022
da2463d
Implement improved border star algorithm
SpacialCircumstances May 29, 2022
644da60
Fix finding border stars
SpacialCircumstances May 31, 2022
6e6f872
Use degrees for border star angle computation and fix
SpacialCircumstances Jun 1, 2022
f00c21f
Fix border star angle threshold
SpacialCircumstances Jun 1, 2022
96b6fb5
Treat stars with only one neighboring star as border stars automatically
SpacialCircumstances Jun 1, 2022
b84efe3
Fix border star math again
SpacialCircumstances Jun 1, 2022
3cf737d
Fix border star angles array size and sorting
SpacialCircumstances Jun 1, 2022
509808b
Use "logical" range for determining border stars
SpacialCircumstances Jun 1, 2022
9a54a0e
Make border detection less sensitive
SpacialCircumstances Jun 1, 2022
d455a71
Remove debug logging
SpacialCircumstances Jun 1, 2022
26d0b35
Merge branch 'dev' into feature/ai-improvements-3
SpacialCircumstances Jun 8, 2022
65c9892
Store basic data about border stars
SpacialCircumstances Jun 8, 2022
257f7fe
Move basic AI logic out into different service to allow advanced infr…
SpacialCircumstances Jun 8, 2022
9aa7aef
Do not bulk upgrade stars on hostile borders
SpacialCircumstances Jun 8, 2022
3c9f80c
Do not upgrade infrastructure on attacked stars and allow ind/science…
SpacialCircumstances Jun 14, 2022
024d429
Find max invasion score for star
SpacialCircumstances Jun 14, 2022
54bc11f
Account for distance when invading enemy stars
SpacialCircumstances Jun 15, 2022
f9301c4
Merge branch 'master' into feature/stable-ai-improvements
SpacialCircumstances Jul 16, 2023
96f577f
Fix errors after merge
SpacialCircumstances Jul 16, 2023
49cd35f
Improvements to border star data
SpacialCircumstances Jul 16, 2023
2d30acf
Fix AI player marking itself on borders
SpacialCircumstances Jul 18, 2023
89306b8
Work on better ship logistics
SpacialCircumstances Aug 2, 2023
88933eb
Create logistics orders according to score
SpacialCircumstances Aug 4, 2023
afea13f
Remove logistics from order system and handle separately
SpacialCircumstances Aug 4, 2023
1d2bf8b
Find path for logistics
SpacialCircumstances Aug 4, 2023
3e9890c
Move logistics carriers if no carrier must be built
SpacialCircumstances Aug 5, 2023
995efdb
Build carriers and handle ship transfers for logistics
SpacialCircumstances Aug 5, 2023
01047d5
Skip unimportant logistics
SpacialCircumstances Aug 5, 2023
c6930cf
Skip empty stars for logistics
SpacialCircumstances Aug 5, 2023
0f69b81
Refactor movements computation out of logistics
SpacialCircumstances Aug 5, 2023
1545d07
Allow moving ships from empty-border stars
SpacialCircumstances Aug 5, 2023
0673e19
Fix finding non-important stars
SpacialCircumstances Aug 5, 2023
2a6f1f8
Fix path finding
SpacialCircumstances Aug 5, 2023
e6afc7a
Remove debug printing
SpacialCircumstances Aug 5, 2023
39c743d
Attempt to improve evaluation which logistic movements are worth exec…
SpacialCircumstances Aug 6, 2023
e8b0959
Fix players own stars making stars count as border stars
SpacialCircumstances Aug 6, 2023
23728fe
Fix accidentally using carriers that are already moving
SpacialCircumstances Aug 6, 2023
65ab786
Calculate scores so they are zero less often+
SpacialCircumstances Aug 6, 2023
161ec65
Allow longer stockpiling
SpacialCircumstances Aug 6, 2023
c10f18a
Introduce pathfindingservice to the backend
SpacialCircumstances Aug 6, 2023
40df200
Use new pathfinding for AI logistics
SpacialCircumstances Aug 6, 2023
f75d27b
Small fix to the pathfinding algorithm
SpacialCircumstances Aug 6, 2023
033bf19
Fix more possible id comparison bugs
SpacialCircumstances Aug 6, 2023
df1f048
Fix pathfinding
SpacialCircumstances Aug 8, 2023
e5bd02a
Improve score calculation
SpacialCircumstances Aug 8, 2023
5e498f2
Add todos
SpacialCircumstances Aug 8, 2023
bb9b31d
Refactor out star priority computation
SpacialCircumstances Aug 9, 2023
437da0a
Do not create logistics movements for stars that will have their ship…
SpacialCircumstances Aug 9, 2023
2ea0318
Pick up ships from more stars if possible
SpacialCircumstances Aug 9, 2023
0b8b001
Fix pathfinding type error
SpacialCircumstances Aug 9, 2023
7c388fa
Fix waypoints for removed movements
SpacialCircumstances Aug 9, 2023
5cd7dfe
Improve picking up ships while doing logistics by revisiting movement…
SpacialCircumstances Aug 9, 2023
21f8607
Fix return waypoints containing collect orders
SpacialCircumstances Aug 10, 2023
6aaa362
Remove debug printing
SpacialCircumstances Aug 10, 2023
9602a04
Merge branch 'master' into feature/stable-ai-improvements
SpacialCircumstances Aug 10, 2023
85db085
Merge branch 'dev' into feature/stable-ai-improvements
SpacialCircumstances Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
601 changes: 400 additions & 201 deletions server/services/ai.ts

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions server/services/basicAi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Game} from "./types/Game";
import {Player} from "./types/Player";
import StarUpgradeService from "./starUpgrade";

const FIRST_TICK_BULK_UPGRADE_SCI_PERCENTAGE = 20;
const FIRST_TICK_BULK_UPGRADE_IND_PERCENTAGE = 30;
const LAST_TICK_BULK_UPGRADE_ECO_PERCENTAGE = 100;

export default class BasicAIService {
starUpgradeService: StarUpgradeService;

constructor(starUpgradeService: StarUpgradeService) {
this.starUpgradeService = starUpgradeService;
}

async _doBasicLogic(game: Game, player: Player, isFirstTickOfCycle: boolean, isLastTickOfCycle: boolean) {
if (isFirstTickOfCycle) {
await this._playFirstTick(game, player);
} else if (isLastTickOfCycle) {
await this._playLastTick(game, player);
}

// TODO: Not sure if this is an issue but there was an occassion during debugging
// where the player credits amount was less than 0, I assume its the AI spending too much somehow
// so adding this here just in case but need to investigate.
player.credits = Math.max(0, player.credits);
}

async _playFirstTick(game: Game, player: Player) {
if (!player.credits || player.credits < 0) {
return
}

// On the first tick after production:
// 1. Bulk upgrade X% of credits to ind and sci.
let creditsToSpendSci = Math.floor(player.credits / 100 * FIRST_TICK_BULK_UPGRADE_SCI_PERCENTAGE);
let creditsToSpendInd = Math.floor(player.credits / 100 * FIRST_TICK_BULK_UPGRADE_IND_PERCENTAGE);

if (creditsToSpendSci) {
await this.starUpgradeService.upgradeBulk(game, player, 'totalCredits', 'science', creditsToSpendSci, false);
}

if (creditsToSpendInd) {
await this.starUpgradeService.upgradeBulk(game, player, 'totalCredits', 'industry', creditsToSpendInd, false);
}
}

async _playLastTick(game: Game, player: Player) {
if (!player.credits || player.credits <= 0) {
return
}

// On the last tick of the cycle:
// 1. Spend remaining credits upgrading economy.
let creditsToSpendEco = Math.floor(player.credits / 100 * LAST_TICK_BULK_UPGRADE_ECO_PERCENTAGE);

if (creditsToSpendEco) {
await this.starUpgradeService.upgradeBulk(game, player, 'totalCredits', 'economy', creditsToSpendEco, false);
}
}
}
6 changes: 3 additions & 3 deletions server/services/gameTick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ export default class GameTickService extends EventEmitter {

logTime(`Tick ${game.state.tick}`);

await this._playAI(game);
logTime('AI controlled players turn');

await this._captureAbandonedStars(game, gameUsers);
logTime('Capture abandoned stars');

Expand All @@ -222,9 +225,6 @@ export default class GameTickService extends EventEmitter {

await this._gameLoseCheck(game, gameUsers);
logTime('Game lose check');

await this._playAI(game);
logTime('AI controlled players turn');

await this.researchService.conductResearchAll(game, gameUsers);
logTime('Conduct research');
Expand Down
11 changes: 8 additions & 3 deletions server/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import UserLeaderboardService from "./userLeaderboard";

const bcrypt = require('bcrypt');

import GameModel from '../db/models/Game';
Expand Down Expand Up @@ -55,6 +53,7 @@ import SpecialStarBanService from './specialStarBan';
import AchievementService from './achievement';
import ConversationService from './conversation';
import ReputationService from './reputation';
import BasicAIService from "./basicAi";
import AIService from './ai';
import GuildService from './guild';
import GuildUserService from './guildUser';
Expand Down Expand Up @@ -86,6 +85,7 @@ import NotificationService from './notification';
import DiscordService from './discord';
import ShipService from './ship';
import SpectatorService from './spectator';
import PathfindingService from "./pathfinding";

import { DependencyContainer } from './types/DependencyContainer';

Expand All @@ -98,6 +98,7 @@ import { Guild } from './types/Guild';
import { Payment } from './types/Payment';
import { Report } from './types/Report';
import TeamService from "./team";
import UserLeaderboardService from './userLeaderboard';

const gameNames = require('../config/game/gameNames');
const starNames = require('../config/game/starNames');
Expand Down Expand Up @@ -183,7 +184,9 @@ export default (config): DependencyContainer => {
const specialistHireService = new SpecialistHireService(gameRepository, specialistService, achievementService, waypointService, playerCreditsService, starService, gameTypeService, specialistBanService, technologyService);
const starUpgradeService = new StarUpgradeService(gameRepository, starService, carrierService, achievementService, researchService, technologyService, playerCreditsService, gameTypeService, shipService);
const shipTransferService = new ShipTransferService(gameRepository, carrierService, starService);
const aiService = new AIService(starUpgradeService, carrierService, starService, distanceService, waypointService, combatService, shipTransferService, technologyService, playerService, playerAfkService, reputationService, diplomacyService, playerStatisticsService, shipService);
const pathfindingService = new PathfindingService(distanceService, starService, waypointService);
const basicAIService = new BasicAIService(starUpgradeService);
const aiService = new AIService(starUpgradeService, carrierService, starService, distanceService, waypointService, combatService, shipTransferService, technologyService, playerService, playerAfkService, reputationService, diplomacyService, shipService, playerStatisticsService, basicAIService, pathfindingService);
const historyService = new HistoryService(historyRepository, playerService, gameService, playerStatisticsService);
const battleRoyaleService = new BattleRoyaleService(starService, carrierService, mapService, starDistanceService, waypointService, carrierMovementService);
const starMovementService = new StarMovementService(mapService, starDistanceService, specialistService, waypointService);
Expand Down Expand Up @@ -251,6 +254,7 @@ export default (config): DependencyContainer => {
achievementService,
conversationService,
reputationService,
basicAIService,
aiService,
battleRoyaleService,
starMovementService,
Expand All @@ -274,5 +278,6 @@ export default (config): DependencyContainer => {
shipService,
spectatorService,
teamService,
pathfindingService,
};
};
120 changes: 120 additions & 0 deletions server/services/pathfinding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {Game} from "./types/Game";
import {Player} from "./types/Player";
import {Carrier} from "./types/Carrier";
import {DBObjectId} from "./types/DBObjectId";
import DistanceService from "./distance";
import StarService from "./star";
import { Star } from "./types/Star";
import WaypointService from "./waypoint";

// Copied and adapted from WaypointHelper in the frontend
// Should probably be put into a shared library at some point

interface Node {
cost: number;
costFromStart: number;
neighbors: Node[] | null;
parent: Node | null;
star: Star;
}

export default class PathfindingService {
distanceService: DistanceService;
starService: StarService;
waypointService: WaypointService;

constructor(distanceService: DistanceService, starService: StarService, waypointService: WaypointService) {
this.distanceService = distanceService;
this.starService = starService;
this.waypointService = waypointService;
}

calculateShortestRoute(game: Game, player: Player, carrier: Carrier, sourceStarId: string, destinStarId: string): Node[] {
const hyperspaceDistance = this.distanceService.getHyperspaceDistance(game, player.research.hyperspace.level);

const graph: Node[] = game.galaxy.stars.map(star => {
return {
star,
cost: 0,
costFromStart: 0,
neighbors: null,
parent: null
}
})

const getNeighbors = (node: Node) => graph
.filter(s => s.star._id.toString() !== node.star._id.toString())
.filter(s => this.distanceService.getDistanceBetweenLocations(s.star.location, node.star.location) <= hyperspaceDistance || this.starService.isStarPairWormHole(s.star, node.star));

const start = graph.find(s => s.star._id.toString() === sourceStarId)!;
const end = graph.find(s => s.star._id.toString() === destinStarId)!;

const openSet: Node[] = [start]
const closedSet: Node[] = []

while (openSet.length) {
// This sort makes us look at the nodes where we can get the quickest first.
// This guarantees that all nodes that already have a calculated route (which may not be the quickest)
// will have their quickest route found. This in turn guarantees that the final fastest route can be found.

// Note from Tristanvds: Unfortunately we cannot also take into account a sorting system where we look at the
// distance to the end star. This kind of sorting system would favour going in a direct line towards that star
// instead of going for wormholes. Therefore we have to take a (computationally) slower approach by sorting
// based on the distance from the start.
openSet.sort((a, b) => a.costFromStart - b.costFromStart); // Ensure we start with the node that has the lowest total cost
const current = openSet.shift()!;

closedSet.push(current); // We're evaluating, so might as well close it.

// If we've found the end, return the reversed path.
if (current.star._id.toString() === end.star._id.toString()) {
let temp = current;

const path: Node[] = [];

path.push(temp);

while (temp.parent) {
path.push(temp.parent);
temp = temp.parent;
}

return path.reverse();
}

// Dynamically load neighbors as its more efficient
if (!current.neighbors) {
current.neighbors = getNeighbors(current);
}

for (const neighbor of current.neighbors) {
// If the neighbor has already been checked, then no need to check again.
const isClosed = closedSet.find(n => n.star._id.toString() === neighbor.star._id.toString()) != null;

if (!isClosed) {
neighbor.cost = this.waypointService.calculateTicksForDistance(game, player, carrier, current.star, neighbor.star);

// Calculate what the next cost will be, we don't want to check
// any paths that lead us to more cost.
const nextCost = current.costFromStart + neighbor.cost;

// But if we haven't tried this path, enqueue it.
const isOpen = openSet.find(n => n.star._id.toString() === neighbor.star._id.toString()) != null;

if (!isOpen) {
openSet.push(neighbor);
} else if (nextCost >= neighbor.costFromStart) {
continue;
}

// Calculate the final cost from the start to the end
// while updating the path taken.
neighbor.costFromStart = nextCost
neighbor.parent = current
}
}
}

return []
}
}
8 changes: 4 additions & 4 deletions server/services/starDistance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default class StarDistanceService {

getClosestStars(star: Star, stars: Star[], amount: number) {
let sorted = stars
.filter(s => s._id !== star._id) // Exclude the current star.
.filter(s => s._id.toString() !== star._id.toString()) // Exclude the current star.
.sort((a, b) => {
return this.getDistanceBetweenStars(star, a)
- this.getDistanceBetweenStars(star, b);
Expand All @@ -46,7 +46,7 @@ export default class StarDistanceService {

getClosestUnownedStars(star: Star, stars: Star[], amount: number) {
let sorted = stars
.filter(s => s._id !== star._id) // Exclude the current star.
.filter(s => s._id.toString() !== star._id.toString()) // Exclude the current star.
.filter(s => !s.ownedByPlayerId)
.sort((a, b) => {
return this.getDistanceBetweenStars(star, a)
Expand All @@ -62,7 +62,7 @@ export default class StarDistanceService {

getClosestOwnedStars(star: Star, stars: Star[]) {
return stars
.filter(s => s._id !== star._id) // Exclude the current star.
.filter(s => s._id.toString() !== star._id.toString()) // Exclude the current star.
.filter(s => s.ownedByPlayerId)
.sort((a, b) => {
return this.getDistanceBetweenStars(star, a)
Expand Down Expand Up @@ -110,7 +110,7 @@ export default class StarDistanceService {

getStarsWithinRadiusOfStar(star: Star, stars: Star[], radius: number) {
let nearby = stars
.filter(s => (s._id !== star._id) && (this.getDistanceBetweenStars(star, s) <= radius))
.filter(s => (s._id.toString() !== star._id.toString()) && (this.getDistanceBetweenStars(star, s) <= radius))

return nearby;
}
Expand Down
4 changes: 4 additions & 0 deletions server/services/types/DependencyContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import UserLevelService from "../userLevel";
import SpecialStarBanService from "../specialStarBan";
import ShipService from "../ship";
import SpectatorService from "../spectator";
import BasicAIService from "../basicAi";
import PathfindingService from "../pathfinding";
import UserLeaderboardService from "../userLeaderboard";
import TeamService from "../team";

Expand Down Expand Up @@ -122,6 +124,7 @@ export interface DependencyContainer {
conversationService: ConversationService,
reputationService: ReputationService,
aiService: AIService,
basicAIService: BasicAIService,
battleRoyaleService: BattleRoyaleService,
starMovementService: StarMovementService,
cacheService: CacheService,
Expand All @@ -144,4 +147,5 @@ export interface DependencyContainer {
shipService: ShipService,
spectatorService: SpectatorService,
teamService: TeamService,
pathfindingService: PathfindingService,
};
13 changes: 13 additions & 0 deletions server/services/waypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,19 @@ export default class WaypointService {
return distanceBetweenStars <= hyperspaceDistance
}

calculateTicksForDistance(game: Game, player: Player, carrier: Carrier, sourceStar: Star, destinationStar: Star): number {
const distance = this.distanceService.getDistanceBetweenLocations(sourceStar.location, destinationStar.location);
const warpSpeed = this.carrierMovementService.canTravelAtWarpSpeed(game, player, carrier, sourceStar, destinationStar);

let tickDistance = this.carrierMovementService.getCarrierDistancePerTick(game, carrier, warpSpeed, false);

if (tickDistance) {
return Math.ceil(distance / tickDistance);
}

return 1;
}

calculateWaypointTicks(game: Game, carrier: Carrier, waypoint: CarrierWaypoint) {
const delayTicks = waypoint.delayTicks || 0;

Expand Down
Loading