Foundation for building AI

This commit is contained in:
Anuken 2020-06-05 20:41:05 -04:00
parent 685f915656
commit 0b4de1a4fa
13 changed files with 132 additions and 21 deletions

View File

@ -0,0 +1,14 @@
package mindustry.ai;
import mindustry.game.Teams.*;
public class BaseAI{
public void update(TeamData data){
//only schedule when there's something to build.
if(data.blocks.isEmpty()){
//TODO
}
}
}

View File

@ -0,0 +1,79 @@
package mindustry.ai.types;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.entities.units.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.blocks.BuildBlock.*;
import static mindustry.Vars.*;
public class BuilderAI extends AIController{
@Override
public void update(){
Builderc builder = (Builderc)unit;
//approach request if building
if(builder.building()){
BuildRequest req = builder.buildRequest();
boolean valid =
(req.tile().entity instanceof BuildEntity && req.tile().<BuildEntity>ent().cblock == req.block) ||
(req.breaking ?
Build.validBreak(unit.team(), req.x, req.y) :
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation));
if(valid){
//move toward the request
moveTo(req.tile(), buildingRange - 20f);
}else{
//discard invalid request
builder.requests().removeFirst();
}
}else{
//find new request
if(!unit.team().data().blocks.isEmpty()){
Queue<BrokenBlock> blocks = unit.team().data().blocks;
BrokenBlock block = blocks.first();
//check if it's already been placed
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block().id == block.block){
blocks.removeFirst();
}else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation)){ //it's valid.
//add build request.
BuildRequest req = new BuildRequest(block.x, block.y, block.rotation, content.block(block.block));
if(block.config != null){
req.configure(block.config);
}
builder.addBuild(req);
}else{
//shift head of queue to tail, try something else next time
blocks.removeFirst();
blocks.addLast(block);
}
}else{
//TODO implement AI base building
}
}
}
protected void moveTo(Position target, float circleLength){
vec.set(target).sub(unit);
float length = circleLength <= 0.001f ? 1f : Mathf.clamp((unit.dst(target) - circleLength) / 100f, -1f, 1f);
vec.setLength(unit.type().speed * Time.delta() * length);
if(length < -0.5f){
vec.rotate(180f);
}else if(length < 0){
vec.setZero();
}
unit.moveAt(vec);
}
}

View File

@ -46,6 +46,8 @@ public class FlyingAI extends AIController{
unit.controlWeapons(shoot, shoot);
}
//TODO clean up
protected void circle(float circleLength){
circle(circleLength, unit.type().speed);
}

View File

@ -3,6 +3,7 @@ package mindustry.content;
import arc.graphics.*;
import arc.math.*;
import arc.struct.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.ctype.*;
import mindustry.entities.bullet.*;
@ -430,6 +431,8 @@ public class UnitTypes implements ContentList{
}};
phantom = new UnitType("phantom"){{
defaultController = BuilderAI::new;
flying = true;
drag = 0.05f;
speed = 3f;

View File

@ -65,21 +65,21 @@ public class Logic implements ApplicationListener{
//remove existing blocks that have been placed here.
//painful O(n) iteration + copy
for(int i = 0; i < data.brokenBlocks.size; i++){
BrokenBlock b = data.brokenBlocks.get(i);
for(int i = 0; i < data.blocks.size; i++){
BrokenBlock b = data.blocks.get(i);
if(b.x == tile.x && b.y == tile.y){
data.brokenBlocks.removeIndex(i);
data.blocks.removeIndex(i);
break;
}
}
data.brokenBlocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config()));
data.blocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config()));
});
Events.on(BlockBuildEndEvent.class, event -> {
if(!event.breaking){
TeamData data = state.teams.get(event.team);
Iterator<BrokenBlock> it = data.brokenBlocks.iterator();
Iterator<BrokenBlock> it = data.blocks.iterator();
while(it.hasNext()){
BrokenBlock b = it.next();
Block block = content.block(b.block);
@ -309,6 +309,12 @@ public class Logic implements ApplicationListener{
//weather is serverside
if(!net.client()){
updateWeather();
for(TeamData data : state.teams.getActive()){
if(data.hasAI()){
data.ai.update(data);
}
}
}
if(state.rules.waves && state.rules.waveTimer && !state.gameOver){

View File

@ -80,11 +80,11 @@ abstract class BuilderComp implements Unitc{
}
if(!(tile.block() instanceof BuildBlock)){
if(!current.initialized && !current.breaking && Build.validPlace(team(), current.x, current.y, current.block, current.rotation)){
if(!current.initialized && !current.breaking && Build.validPlace(current.block, team(), current.x, current.y, current.rotation)){
boolean hasAll = !Structs.contains(current.block.requirements, i -> !core.items().has(i.item));
if(hasAll || state.rules.infiniteResources){
Build.beginPlace(team(), current.x, current.y, current.block, current.rotation);
Build.beginPlace(current.block, team(), current.x, current.y, current.rotation);
}else{
current.stuck = true;
}
@ -138,7 +138,7 @@ abstract class BuilderComp implements Unitc{
control.input.drawBreaking(request);
}else{
request.block.drawRequest(request, control.input.allRequests(),
Build.validPlace(team(), request.x, request.y, request.block, request.rotation) || control.input.requestMatches(request));
Build.validPlace(request.block, team(), request.x, request.y, request.rotation) || control.input.requestMatches(request));
}
}

View File

@ -90,7 +90,7 @@ abstract class PayloadComp implements Posc, Rotc{
Tilec tile = payload.entity;
int tx = Vars.world.toTile(x - tile.block().offset()), ty = Vars.world.toTile(y - tile.block().offset());
Tile on = Vars.world.tile(tx, ty);
if(on != null && Build.validPlace(tile.team(), tx, ty, tile.block(), tile.rotation())){
if(on != null && Build.validPlace(tile.block(), tile.team(), tx, ty, tile.rotation())){
int rot = (int)((rotation() + 45f) / 90f) % 4;
payload.place(on, rot);

View File

@ -5,6 +5,7 @@ import arc.math.geom.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.gen.*;
import mindustry.world.blocks.storage.CoreBlock.*;
@ -147,7 +148,8 @@ public class Teams{
public final Array<CoreEntity> cores = new Array<>();
public final Array<Team> enemies = new Array<>();
public final Team team;
public Queue<BrokenBlock> brokenBlocks = new Queue<>();
public Queue<BrokenBlock> blocks = new Queue<>();
public BaseAI ai = new BaseAI();
public TeamData(Team team){
this.team = team;
@ -169,6 +171,11 @@ public class Teams{
return cores.isEmpty() ? null : cores.first();
}
/** @return whether this team is controlled by the AI and builds bases. */
public boolean hasAI(){
return state.rules.attackMode && team == state.rules.waveTeam;
}
@Override
public String toString(){
return "TeamData{" +

View File

@ -120,7 +120,7 @@ public class BlockRenderer implements Disposable{
}
if(brokenFade > 0.001f){
for(BrokenBlock block : state.teams.get(player.team()).brokenBlocks){
for(BrokenBlock block : state.teams.get(player.team()).blocks){
Block b = content.block(block.block);
if(!camera.bounds(Tmp.r1).grow(tilesize * 2f).overlaps(Tmp.r2.setSize(b.size * tilesize).setCenter(block.x * tilesize + b.offset(), block.y * tilesize + b.offset()))) continue;

View File

@ -507,7 +507,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
}
for(BrokenBlock req : player.team().data().brokenBlocks){
for(BrokenBlock req : player.team().data().blocks){
Block block = content.block(req.block);
if(block.bounds(req.x, req.y, Tmp.r2).overlaps(Tmp.r1)){
drawSelected(req.x, req.y, content.block(req.block), Pal.remove);
@ -629,7 +629,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
//remove blocks to rebuild
Iterator<BrokenBlock> broken = state.teams.get(player.team()).brokenBlocks.iterator();
Iterator<BrokenBlock> broken = state.teams.get(player.team()).blocks.iterator();
while(broken.hasNext()){
BrokenBlock req = broken.next();
Block block = content.block(req.block);
@ -907,7 +907,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
return false;
}
}
return Build.validPlace(player.team(), x, y, type, rotation);
return Build.validPlace(type, player.team(), x, y, rotation);
}
public boolean validBreak(int x, int y){

View File

@ -269,8 +269,8 @@ public abstract class SaveVersion extends SaveFileReader{
stream.writeInt(data.size);
for(TeamData team : data){
stream.writeInt(team.team.id);
stream.writeInt(team.brokenBlocks.size);
for(BrokenBlock block : team.brokenBlocks){
stream.writeInt(team.blocks.size);
for(BrokenBlock block : team.blocks){
stream.writeShort(block.x);
stream.writeShort(block.y);
stream.writeShort(block.rotation);
@ -297,7 +297,7 @@ public abstract class SaveVersion extends SaveFileReader{
TeamData data = team.data();
int blocks = stream.readInt();
for(int j = 0; j < blocks; j++){
data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, TypeIO.readObject(Reads.get(stream))));
data.blocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, TypeIO.readObject(Reads.get(stream))));
}
}

View File

@ -21,7 +21,7 @@ public class Save3 extends LegacySaveVersion{
TeamData data = team.data();
int blocks = stream.readInt();
for(int j = 0; j < blocks; j++){
data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt()));
data.blocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt()));
}
}

View File

@ -42,8 +42,8 @@ public class Build{
/** Places a BuildBlock at this location. */
@Remote(called = Loc.server)
public static void beginPlace(Team team, int x, int y, Block result, int rotation){
if(!validPlace(team, x, y, result, rotation)){
public static void beginPlace(Block result, Team team, int x, int y, int rotation){
if(!validPlace(result, team, x, y, rotation)){
return;
}
@ -62,7 +62,7 @@ public class Build{
}
/** Returns whether a tile can be placed at this location by this team. */
public static boolean validPlace(Team team, int x, int y, Block type, int rotation){
public static boolean validPlace(Block type, Team team, int x, int y, int rotation){
//the wave team can build whatever they want as long as it's visible - banned blocks are not applicable
if(type == null || (!type.isPlaceable() && !(state.rules.waves && team == state.rules.waveTeam && type.isVisible()))){
return false;