SteamWar/FightSystem
Archiviert
13
1

WIP: Rated Plan based AI
Alle Prüfungen waren erfolgreich
SteamWarCI Build successful

Signed-off-by: Lixfel <agga-games@gmx.de>
Dieser Commit ist enthalten in:
Lixfel 2024-01-04 23:52:09 +01:00
Ursprung 97a9acdf53
Commit f104ccc49a
5 geänderte Dateien mit 182 neuen und 169 gelöschten Zeilen

Datei anzeigen

@ -47,6 +47,6 @@ dependencies {
compileOnly 'io.netty:netty-all:4.1.68.Final'
compileOnly 'com.mojang:authlib:1.5.25'
compileOnly swdep("FastAsyncWorldEdit-1.18")
compileOnly swdep("WorldEdit-1.15")
compileOnly swdep("SpigotCore")
}

Datei anzeigen

@ -58,6 +58,7 @@ import java.util.logging.Level;
public abstract class AI {
public static final int MOVEMENT_DELAY = 4;
public static final double INTERACTION_RANGE = 5.0;
private static final Map<UUID, AI> ais = new HashMap<>();
@ -209,7 +210,7 @@ public abstract class AI {
}
public void move(Vector pos) {
queue.add(new Action(4) {
queue.add(new Action(MOVEMENT_DELAY) {
@Override
public void run() {
Location location = entity.getLocation();

Datei anzeigen

@ -20,51 +20,41 @@
package de.steamwar.fightsystem.ai;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.registry.state.BooleanProperty;
import com.sk89q.worldedit.world.block.BaseBlock;
import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.world.block.BlockState;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.ai.lixfel.PlanResult;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.sql.SchematicData;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.function.Supplier;
public class LixfelAI extends AI {
private static final Random random = new Random();
private final Random random = new Random();
private final List<Supplier<PlanResult>> plans = new ArrayList<>();
private final BukkitTask timerTask;
private int currentTime;
private LixfelPathplanner pathplanner;
private List<Vector> setup;
private int preRunningStart;
private List<TimedVector> timedStart;
private List<Cannon> cannons;
private Cannon currentCannon;
private Vector plannedPosition;
public LixfelAI(FightTeam team, String user) {
super(team, SteamwarUser.get(user));
timerTask = Bukkit.getScheduler().runTaskTimer(FightSystem.getPlugin(), () -> currentTime++, 1, 1);
}
@Override
public SchematicNode chooseSchematic() {
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.SchematicType.toDB());
SchematicNode schem = publics.get(random.nextInt(publics.size()));
schem = publics.stream().filter(s -> s.getName().equals("TheUnderground")).findAny().orElse(schem);
SchematicNode schem = publics.stream().filter(s -> s.getName().equals("TheUnderground")).findAny().orElseGet(() -> publics.get(random.nextInt(publics.size())));
Clipboard clipboard;
try {
clipboard = new SchematicData(schem).load();
@ -73,9 +63,13 @@ public class LixfelAI extends AI {
}
pathplanner = new LixfelPathplanner(clipboard);
setup = new ArrayList<>(Collections.singletonList(new Vector(25, 15, 25)));
timedStart = new ArrayList<>(Collections.singletonList(new TimedVector(420L, new Vector(21, 15, 24))));
cannons = new ArrayList<>(Arrays.asList(
plans.clear();
plans.add(new MovementEmergency());
plans.add(new ReadyPlan());
plans.add(new TimedInteraction(200.0, -1, new Vector(25, 15, 25)));
plans.add(new TimedInteraction(210.0, 420, new Vector(21, 15, 24)));
List<Cannon> cannons = new ArrayList<>(Arrays.asList(
new Cannon(new Vector(11,25,11), 10,23,14, 12,23,14, 10,23,12, 12,23,12, 12,24,15, 10,24,14, 12,23,15, 12,24,14, 11,24,12, 10,24,15, 12,24,12, 10,23,15, 10,24,12),
new Cannon(new Vector(39,25,11), 38,24,15, 38,23,12, 40,23,14, 40,24,12, 40,23,15, 40,24,14, 39,24,12, 38,23,14, 40,24,15, 40,23,12, 38,23,15, 38,24,12, 38,24,14),
new Cannon(new Vector(12,18,11), 13,17,15, 13,17,16, 13,18,16, 13,18,14, 14,18,14, 14,17,15, 14,17,16, 14,18,16, 13,17,14, 14,17,14, 13,18,15, 14,18,15),
@ -84,125 +78,120 @@ public class LixfelAI extends AI {
new Cannon(null, 23,5,9, 23,6,9, 23,7,9, 23,8,9),
new Cannon(null, 27,5,9, 27,6,9, 27,7,9, 27,8,9)
));
assignWater(clipboard);
chooseCannon();
assignWater(cannons, clipboard);
plans.addAll(cannons);
return schem;
}
@Override
protected void plan() {
switch (FightState.getFightState()) {
case POST_SCHEM_SETUP:
if(prepareSchem() && scanEnemy())
setReady();
break;
case PRE_RUNNING:
if(start() && scanEnemy())
ensureInRange(currentCannon.tnt.keySet().iterator().next());
break;
case RUNNING:
while(currentCannon.shoot() && chooseCannon() && scanEnemy()) {}
break;
default:
break;
}
public void move(Vector pos) {
plannedPosition = pos;
super.move(pos);
}
@Override
public void stop() {
if(!timerTask.isCancelled())
timerTask.cancel();
super.stop();
protected void plan() {
plans.stream().map(Supplier::get).filter(result -> result != PlanResult.EMPTY).max(Comparator.comparingDouble(PlanResult::getRating)).ifPresent(PlanResult::act);
}
private void assignWater(Clipboard clipboard) {
BooleanProperty waterlogged = new BooleanProperty("waterlogged", Arrays.asList(false, true));
private void assignWater(List<Cannon> cannons, Clipboard clipboard) {
List<Vector> waterSources = new ArrayList<>();
clipboard.getRegion().forEach(block -> {
BaseBlock state = clipboard.getFullBlock(block);
if(state.getBlockType().getMaterial().isLiquid() || Boolean.TRUE.equals(state.getState(waterlogged)))
BlockState state = clipboard.getBlock(block);
Property<?> waterlogged = state.getBlockType().getPropertyMap().get("waterlogged");
if(state.getBlockType().getMaterial().isLiquid() || (waterlogged != null && Boolean.TRUE.equals(state.getState(waterlogged))))
waterSources.add(pathplanner.clipboardToSchem(block));
});
for(Vector water : waterSources) {
cannons.stream().min(Comparator.comparingDouble(c -> c.waterDistance(water))).ifPresent(cannon -> cannon.addWater(water));
cannons.stream().filter(cannon -> cannon.getTNTMinY() >= water.getBlockY()).min(Comparator.comparingDouble(c -> c.waterDistance(water))).ifPresent(cannon -> cannon.addWater(water));
}
}
private boolean prepareSchem() {
return doWith(setup, this::interact);
private PlanResult inRange(double rating, Vector target, Supplier<PlanResult> plan) {
return inRangeAndTime(rating, 0, target, plan);
}
private boolean scanEnemy() {
//TODO
return true;
}
private boolean start() {
if(preRunningStart == 0)
preRunningStart = currentTime;
if(timedStart.isEmpty())
return true;
long time = currentTime - preRunningStart;
TimedVector vector = timedStart.get(0);
if(!ensureInRange(vector.vector))
return false;
if(time >= vector.getTime()) {
interact(timedStart.remove(0).vector);
} else {
scanEnemy();
}
return false;
}
private boolean chooseCannon() {
List<Cannon> availableCannons = cannons.stream().filter(Cannon::available).collect(Collectors.toList());
if(availableCannons.isEmpty())
return false;
currentCannon = availableCannons.get(random.nextInt(availableCannons.size()));
return true;
}
private boolean doWith(List<Vector> targets, Consumer<Vector> method) {
if(targets.isEmpty())
return true;
if(ensureInRange(targets.get(0)))
method.accept(targets.remove(0));
return false;
}
private boolean ensureInRange(Vector target) {
private PlanResult inRangeAndTime(double rating, int time, Vector target, Supplier<PlanResult> plan) {
Vector position = getPosition();
boolean inRange = new Vector(0, getEntity().getEyeHeight(), 0).add(position).distance(target) <= 5;
boolean inRange = new Vector(0, getEntity().getEyeHeight(), 0).add(position).distance(target) <= AI.INTERACTION_RANGE;
if(inRange)
return true;
return time <= 0 ? plan.get() : PlanResult.EMPTY;
List<Vector> path = new ArrayList<>(pathplanner.planToRange(position, new Vector(0, -getEntity().getEyeHeight(), 0).add(target), 5.0));
for(Vector v : path)
Config.world.spawnParticle(Particle.DRIP_LAVA, translate(v, false), 1);
if(path.isEmpty()) {
move(new Vector(0, 1.2, 0).add(position));
setTNT(position);
} else {
move(path.get(0));
}
if(path.isEmpty() || time > path.size()*AI.MOVEMENT_DELAY)
return PlanResult.EMPTY;
return false;
return new PlanResult(rating, () -> move(path.get(0)));
}
private class Cannon {
private class ReadyPlan implements Supplier<PlanResult> {
@Override
public PlanResult get() {
return new PlanResult(Double.MIN_VALUE, () -> {
setReady();
plans.remove(this);
});
}
}
private class MovementEmergency implements Supplier<PlanResult> {
@Override
public PlanResult get() {
//TODO defunct ladder
if(plannedPosition == null || getPosition().equals(plannedPosition) || pathplanner.getLadders().contains(plannedPosition))
return PlanResult.EMPTY;
pathplanner.getWalkable().remove(plannedPosition); //TODO neighbour-table update
if(getEntity().getVelocity().getY() < 0) {
return new PlanResult(1000.0, () -> setTNT(getPosition().subtract(new Vector(0, 1.0, 0))));
} else {
pathplanner.getWalkable().add(getPosition()); //TODO idealized position, neighbour-table update
//TODO handle offgrid location
//TODO update grid
return new PlanResult(10.0, () -> {
});
}
}
}
private class TimedInteraction implements Supplier<PlanResult> {
private final double rating;
private final int time;
private final Vector vector;
private int timeReference = 0;
public TimedInteraction(double rating, int time, Vector vector) {
this.rating = rating;
this.vector = vector;
this.time = time;
}
@Override
public PlanResult get() {
if(!FightState.ingame())
timeReference = getEntity().getTicksLived();
int timeRemaining = time - (getEntity().getTicksLived() - timeReference);
return inRangeAndTime(rating, timeRemaining, vector, () -> new PlanResult(rating, () -> {
interact(vector);
plans.remove(this);
}));
}
}
private class Cannon implements Supplier<PlanResult> {
private final List<Vector> water = new ArrayList<>();
private final Vector activator;
private final Map<Vector, Boolean> tnt = new HashMap<>();
private final int minY;
private int freeAt;
private int lastCannonCheck = -1;
@ -212,6 +201,12 @@ public class LixfelAI extends AI {
for(int i = 0; i < tntpos.length; i+=3) {
tnt.put(new Vector(tntpos[i], tntpos[i+1], tntpos[i+2]), false);
}
minY = tnt.keySet().stream().min(Comparator.comparingInt(Vector::getBlockY)).orElse(new Vector()).getBlockY();
}
public int getTNTMinY() {
return minY;
}
public void addWater(Vector vector) {
@ -219,75 +214,47 @@ public class LixfelAI extends AI {
}
public double waterDistance(Vector vector) {
return tnt.keySet().stream().mapToDouble(t -> {
double distance = t.distance(vector);
if(t.getY() < vector.getY())
distance += 10.0; //It is unlikely that TNT is loaded from below into the cannon
return distance;
}).min().orElse(Double.MAX_VALUE);
return tnt.keySet().stream().mapToDouble(t -> t.distance(vector)).min().orElse(Double.MAX_VALUE);
}
public boolean available() {
return currentTime >= freeAt && !water.isEmpty();
}
public boolean shoot() {
for(Vector w : water)
Config.world.spawnParticle(Particle.VILLAGER_HAPPY, translate(w, true).add(0.5, 0.5, 0.5), 1);
if(currentTime < freeAt || water.isEmpty())
return true;
@Override
public PlanResult get() {
//TODO smarter ratings
if(getEntity().getTicksLived() < freeAt || water.isEmpty())
return PlanResult.EMPTY;
if(lastCannonCheck < freeAt) {
Vector w = water.get(random.nextInt(water.size()));
BlockData data = getBlockData(w);
if(data.getMaterial() == Material.WATER || (data instanceof Waterlogged && ((Waterlogged)data).isWaterlogged()))
lastCannonCheck = freeAt;
else
water.remove(w);
return false;
return new PlanResult(80.0, () -> {
Vector w = water.get(random.nextInt(water.size()));
BlockData data = getBlockData(w);
if(data.getMaterial() == Material.WATER || (data instanceof Waterlogged && ((Waterlogged)data).isWaterlogged()))
lastCannonCheck = freeAt;
else
water.remove(w);
});
}
for(Map.Entry<Vector, Boolean> entry : tnt.entrySet()) {
if(!entry.getValue()) {
if(ensureInRange(entry.getKey())) {
setTNT(entry.getKey());
entry.setValue(true);
}
return false;
}
}
return tnt.entrySet().stream().filter(entry -> !entry.getValue()).map(entry -> inRangeAndTime(90.0, FightState.infight() ? 0 : 1, entry.getKey(), () -> new PlanResult(100.0, () -> {
setTNT(entry.getKey());
entry.setValue(true);
if(activator != null) {
if(!ensureInRange(activator))
return false;
if(activator == null && tnt.values().stream().allMatch(b -> b))
fired();
}))).max(Comparator.comparingDouble(PlanResult::getRating)).orElseGet(() -> {
if(activator == null)
return PlanResult.EMPTY;
interact(activator);
}
return inRange(110.0, activator, () -> new PlanResult(120.0, () -> {
interact(activator);
fired();
}));
});
}
freeAt = currentTime + 80;
private void fired() {
freeAt = getEntity().getTicksLived() + 80;
for(Map.Entry<Vector, Boolean> entry : tnt.entrySet())
entry.setValue(false);
return false;
}
}
public static class TimedVector {
private final long time;
private final Vector vector;
public TimedVector(long time, Vector vector) {
this.time = time;
this.vector = vector;
}
public long getTime() {
return time;
}
public Vector getVector() {
return vector;
}
}
}

Datei anzeigen

@ -78,6 +78,7 @@ public class LixfelPathplanner {
private final Vector clipboardToSchem;
private final BlockVector3 diff;
private final List<Vector> ladders = new ArrayList<>();
private final List<Vector> walkable = new ArrayList<>();
private final Map<Vector, Vector[]> neighbours = new HashMap<>();
@ -95,6 +96,9 @@ public class LixfelPathplanner {
return toBukkit(vector.subtract(diff), 0.0, 0.0);
}
public List<Vector> getLadders() {
return ladders;
}
public List<Vector> getWalkable() {
return walkable;
}
@ -117,10 +121,10 @@ public class LixfelPathplanner {
if(height >= 1.0 && region.contains(above))
return;
Vector block = toBukkit(vector.subtract(diff), 0.5, Math.max(height, 0.0));
walkable.add(block);
if(height < 0.0)
height = 0.0;
walkable.add(toBukkit(vector.subtract(diff), 0.5, height));
ladders.add(block);
});
for(Vector vector : walkable) {

Datei anzeigen

@ -0,0 +1,41 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.ai.lixfel;
public class PlanResult {
public static final PlanResult EMPTY = new PlanResult(Double.NEGATIVE_INFINITY, () -> {});
private final double rating;
private final Runnable action;
public PlanResult(double rating, Runnable action) {
this.rating = rating;
this.action = action;
}
public double getRating() {
return rating;
}
public void act() {
action.run();
}
}