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

Implement healUnit, getHealCost, and AI Unit Creation Logic (#4) #29

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 107 additions & 0 deletions dionysus/DionysusAlpha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ type CreateUnitCombination = {
weight: number;
};

// Define or import the necessary constants and types
const UnitType = {
Medic: 'Medic',
SupportShip: 'SupportShip',
};

const ResourceType = {
Metal: 'Metal',
};

const Medic = {
cost: 100, // Example cost, replace with actual cost
metalCost: 50, // Example metal cost, replace with actual cost
getInfo: () => ({ id: 'medic', type: UnitType.Medic, maxHp: 100, ...otherMedicProperties }),
};

const SupportShip = {
cost: 150, // Example cost, replace with actual cost
metalCost: 75, // Example metal cost, replace with actual cost
getInfo: () => ({ id: 'supportShip', type: UnitType.SupportShip, maxHp: 200, ...otherSupportShipProperties }),
};


// DionysusAlpha is the first AI, and it is the most aggressive one.
// At each turn, it checks which unit can do the most damage to another unit and then attacks.
export default class DionysusAlpha extends BaseAI {
Expand All @@ -88,6 +111,7 @@ export default class DionysusAlpha extends BaseAI {
this.fold(map) ||
this.createBuilding(map) ||
this.move(map) ||
this.healUnit(map) ||
this.unfold(map) ||
this.buySkills(map) ||
this.createUnit(map) ||
Expand Down Expand Up @@ -364,6 +388,59 @@ export default class DionysusAlpha extends BaseAI {
return null;
}


private healUnit(map: MapData): MapData | null {
const currentPlayer = map.getCurrentPlayer();
const healers = map.units.filter(
(unit: any) =>
!unit.isCompleted() &&
unit.info.hasAbility(Ability.Heal) &&
map.matchesPlayer(currentPlayer, unit),
);

if (!healers.size) {
return null;
}

for (const [from, healer] of healers) {
const healableUnits = map.units
.filter((unit: any) => {
return (
unit &&
map.isPlayerUnit(currentPlayer, unit) &&
unit.currentHp < unit.info.maxHp
);
})
.map((unit: any) => unit.position);

for (const to of healableUnits) {
const healCost = this.getHealCost(healer.info, map.units.get(to)!.info);
if (currentPlayer.resources >= healCost) {
this.tryAttacking();
return this.execute(map, this.HealUnitAction(from, to));
}
}
}

return null;
}




HealUnitAction(from: Vector, to: Vector): { from: Vector, to: Vector } {
return { from, to };
}

getHealCost(unit: Unit, map: MapData): number {
const healCostPerHp = 10;
const maxHp = unit.info.maxHp;
const missingHp = maxHp - unit.currentHp;
return missingHp * healCostPerHp;
}



private rescue(map: MapData): MapData | null {
if (!map.hasNeutralUnits()) {
return null;
Expand Down Expand Up @@ -665,6 +742,33 @@ export default class DionysusAlpha extends BaseAI {
return null;
}

//logic to consider building a Medic or Support Ship
const shouldBuildMedicOrSupportShip =
Math.random() < 0.1 && // Adjust the probability
currentPlayer.funds >= Medic.cost &&
currentPlayer.resources.some(resource => resource.type === ResourceType.Metal && resource.amount >= Medic.metalCost);

// Check if Medic or Support Ship should be built
if (shouldBuildMedicOrSupportShip) {
const availableBuildings = [...buildings.values()];
const validBuildings = availableBuildings.filter(building =>
building.canBuildUnitType(currentPlayer, UnitType.Medic) ||
building.canBuildUnitType(currentPlayer, UnitType.SupportShip)
);

if (validBuildings.length > 0) {
const building = validBuildings[Math.floor(Math.random() * validBuildings.length)];
const unitType = Math.random() < 0.5 ? UnitType.Medic : UnitType.SupportShip; // Randomly choose between Medic and Support Ship
const unitInfo = unitType === UnitType.Medic ? Medic.getInfo() : SupportShip.getInfo();
const deployableVectors = getDeployableVectors(map, unitInfo, building.position, currentPlayer.id);

if (deployableVectors.length > 0) {
const to = deployableVectors[Math.floor(Math.random() * deployableVectors.length)];
return this.execute(map, CreateUnitAction(building.position, unitInfo.id, to));
}
}
}

const buildCapabilities = getPossibleUnitAbilitiesForBuildings(
[...buildings.values()],
currentPlayer,
Expand Down Expand Up @@ -1270,3 +1374,6 @@ const shouldBuildNavalUnits = (
const hasTooManyOfType = (units: ReadonlyArray<Unit>, unitInfo: UnitInfo) =>
units.length > 10 &&
units.filter(({ id }) => id === unitInfo.id).length / units.length > 0.2;



61 changes: 61 additions & 0 deletions tests/__tests__/AIBehavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import MapData, { SizeVector } from '@deities/athena/MapData.tsx';
import AIRegistry from '@deities/dionysus/AIRegistry.tsx';
import { expect, test } from 'vitest';
import snapshotGameState from '../snapshotGameState.tsx';
import { Abilities, Ability } from '../../athena/info/Unit.tsx';

const initialMap = MapData.createMap({
config: {
Expand Down Expand Up @@ -846,3 +847,63 @@ test('AI will move onto escort vectors even if it is a long-range unit', () => {
GameEnd { condition: { hidden: false, label: [ 2 ], players: [ 2 ], reward: null, type: 4, vectors: [ '5,4' ] }, conditionId: 1, toPlayer: 2 }"
`);
});

test('AI heals units when healers are available', () => {
const map = new MapData();
const currentPlayer = map.getCurrentPlayer();
const healerUnit = new Unit({ player: currentPlayer, abilities: [Ability.Heal], currentHp: 100, maxHp: 100 });
const injuredUnit = new Unit({ player: currentPlayer, currentHp: 50, maxHp: 100 });

map.units.set(new Vector(1, 1), healerUnit);
map.units.set(new Vector(2, 2), injuredUnit);

const ai = new AIBehavior();
const result = ai.healUnit(map);

expect(result).not.toBeNull();
expect(injuredUnit.currentHp).toBeGreaterThan(50); // Check if the unit has been healed
});

test('AI does not heal when no healable units are present', () => {
const map = new MapData();
const currentPlayer = map.getCurrentPlayer();
const healerUnit = new Unit({ player: currentPlayer, abilities: [Ability.Heal], currentHp: 100, maxHp: 100 });
const healthyUnit = new Unit({ player: currentPlayer, currentHp: 100, maxHp: 100 });

map.units.set(new Vector(1, 1), healerUnit);
map.units.set(new Vector(2, 2), healthyUnit);

const ai = new AIBehavior();
const result = ai.healUnit(map);

expect(result).toBeNull(); // No heal should happen since the unit is already at max health
});

test('AI considers building a Medic or Support Ship', () => {
const map = new MapData();
const currentPlayer = new Player({ id: 1, funds: 200, resources: [{ type: ResourceType.Metal, amount: 100 }] });
map.setCurrentPlayer(currentPlayer);

const building = new Building({ player: currentPlayer, buildableUnitTypes: [UnitType.Medic, UnitType.SupportShip] });
map.buildings.set(new Vector(1, 1), building);

const ai = new AIBehavior();
const result = ai.createUnit(map);

expect(result).not.toBeNull();
expect([UnitType.Medic, UnitType.SupportShip]).toContain(result.unitType);
});

test('AI does not build Medic or Support Ship when funds are insufficient', () => {
const map = new MapData();
const currentPlayer = new Player({ id: 1, funds: 50, resources: [{ type: ResourceType.Metal, amount: 50 }] });
map.setCurrentPlayer(currentPlayer);

const building = new Building({ player: currentPlayer, buildableUnitTypes: [UnitType.Medic, UnitType.SupportShip] });
map.buildings.set(new Vector(1, 1), building);

const ai = new AIBehavior();
const result = ai.createUnit(map);

expect(result).toBeNull();
});