wip: implement action tree and minimax for ai

This commit is contained in:
punchready 2021-06-07 03:06:33 +02:00
parent 81cbec5348
commit 7a5d9dca76
7 changed files with 274 additions and 36 deletions

View File

@ -48,11 +48,13 @@ class AI {
Character character = (Character)state.getEntities().findEntity(turn); Character character = (Character)state.getEntities().findEntity(turn);
BoardAnalyzer analyzer = new BoardAnalyzer(state, turn); BoardAnalyzer analyzer = new BoardAnalyzer(state, turn);
Action action = analyzer.analyze(state, character.getPosition(), turn); Action action = analyzer.analyze(state);
switch(action.type) { switch(action.type) {
case None -> { case None -> {
System.out.println(" Result: doing nothing"); System.out.println(" Result: doing nothing");
result.add(new RequestBuilder(RequestType.EndRoundRequest)
.buildGameRequest());
} }
case Move -> { case Move -> {
System.out.println(" Result: moving to " + action.target); System.out.println(" Result: moving to " + action.target);
@ -163,10 +165,12 @@ class AI {
.buildCharacterRequest()); .buildCharacterRequest());
} }
} }
} case EndTurn -> {
System.out.println(" Result: ending turn");
result.add(new RequestBuilder(RequestType.EndRoundRequest) result.add(new RequestBuilder(RequestType.EndRoundRequest)
.buildGameRequest()); .buildGameRequest());
}
}
return result; return result;
} }

View File

@ -51,4 +51,8 @@ class Action {
this.targetEntity = targetEntity; this.targetEntity = targetEntity;
this.stone = stone; this.stone = stone;
} }
public String toString() {
return this.type.toString();
}
} }

View File

@ -6,5 +6,6 @@ enum ActionType {
MeleeAttack, MeleeAttack,
RangedAttack, RangedAttack,
UseStone, UseStone,
GiveStone GiveStone,
EndTurn
} }

View File

@ -11,13 +11,22 @@ import java.util.Objects;
class Board { class Board {
private final Piece[][] data; private final Piece[][] data;
private final EntityType origin; public final EntityType origin;
private final EntityID turn; public final EntityID turn;
public final boolean ended;
private Board(Piece[][] data, EntityType origin, EntityID turn) { private Board(Piece[][] data, EntityType origin, EntityID turn) {
this.data = data; this.data = data;
this.origin = origin; this.origin = origin;
this.turn = turn; this.turn = turn;
this.ended = false;
}
private Board(Piece[][] data, EntityType origin, EntityID turn, boolean ended) {
this.data = data;
this.origin = origin;
this.turn = turn;
this.ended = ended;
} }
public static Board generate(GameStateView state, EntityID turn) { public static Board generate(GameStateView state, EntityID turn) {
@ -100,10 +109,16 @@ class Board {
} }
for(IntVector2 dir: IntVector2.CardinalDirections) { for(IntVector2 dir: IntVector2.CardinalDirections) {
IntVector2 target = position.add(dir); IntVector2 target = position.add(dir);
if(target.getX() < 0 || target.getX() >= state.getMapSize().getX() || target.getY() < 0 || target.getY() >= state.getMapSize().getY()) { if(
target.getX() < 0 || target.getX() >= state.getMapSize().getX() ||
target.getY() < 0 || target.getY() >= state.getMapSize().getY()
) {
continue; continue;
} }
if(this.data[target.getY()][target.getX()].type == PieceType.Empty) { if(
this.data[target.getY()][target.getX()].type == PieceType.Empty ||
this.data[target.getY()][target.getX()].type == PieceType.Character
) {
result.add(new Action(ActionType.Move, target)); result.add(new Action(ActionType.Move, target));
} }
} }
@ -142,6 +157,9 @@ class Board {
continue; continue;
} }
for(StoneType stone: StoneType.values()) { for(StoneType stone: StoneType.values()) {
if(state.getStoneCooldown(stone) > 0) {
continue;
}
if(!character.inventory.hasStone(stone)) { if(!character.inventory.hasStone(stone)) {
continue; continue;
} }
@ -223,6 +241,9 @@ class Board {
} }
} }
} }
case EndTurn -> {
result.add(new Action(ActionType.EndTurn));
}
} }
} }
@ -251,6 +272,8 @@ class Board {
} }
} }
boolean ended = false;
//TODO: finalize action applying //TODO: finalize action applying
switch(action.type) { switch(action.type) {
@ -262,9 +285,10 @@ class Board {
case MeleeAttack -> { case MeleeAttack -> {
clone[action.target.getY()][action.target.getX()].hp.decreaseValue(character.meleeDamage); clone[action.target.getY()][action.target.getX()].hp.decreaseValue(character.meleeDamage);
current.ap.decreaseValue(1); current.ap.decreaseValue(1);
//todo: drop stones........
} }
case RangedAttack -> { case RangedAttack -> {
clone[action.target.getY()][action.target.getX()].hp.decreaseValue(character.meleeDamage); clone[action.target.getY()][action.target.getX()].hp.decreaseValue(character.rangedDamage);
current.ap.decreaseValue(1); current.ap.decreaseValue(1);
} }
case UseStone -> { case UseStone -> {
@ -292,12 +316,32 @@ class Board {
case GiveStone -> { case GiveStone -> {
} }
case EndTurn -> {
ended = true;
}
} }
//...get next turn in turn order... if(ended || (current.mp.getValue() == 0 && current.ap.getValue() == 0)) {
//TODO: figure out next turn properly ArrayList<EntityID> alive = new ArrayList<>();
for(EntityID next: state.getTurnOrder()) {
if(next.type == EntityType.NPC && next.id == NPCType.Thanos.getID()) {
alive.add(next);
}else if(next.type == EntityType.P1 || next.type == EntityType.P2) {
if(((Character)state.getEntities().findEntity(next)).isAlive()) {
alive.add(next);
}
}
}
int index = alive.indexOf(turn);
if(index == alive.size() - 1) {
return new Board(clone, origin, turn, true);
}else {
return new Board(clone, origin, alive.get(index + 1));
}
}else {
return new Board(clone, origin, turn); return new Board(clone, origin, turn);
} }
}
protected float calculateScore() { protected float calculateScore() {
float score = 0; float score = 0;

View File

@ -1,6 +1,5 @@
package uulm.teamname.marvelous.gamelibrary.ai; package uulm.teamname.marvelous.gamelibrary.ai;
import uulm.teamname.marvelous.gamelibrary.IntVector2;
import uulm.teamname.marvelous.gamelibrary.entities.EntityID; import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType; import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameStateView; import uulm.teamname.marvelous.gamelibrary.gamelogic.GameStateView;
@ -11,18 +10,148 @@ import java.util.HashMap;
class BoardAnalyzer { class BoardAnalyzer {
private final Board origin; private final Board origin;
private final EntityType player; private final EntityType player;
private final HashMap<Integer, Float> cache = new HashMap<>(); private final HashMap<Integer, Float> scoreCache = new HashMap<>();
private final HashMap<Integer, ArrayList<Action>> actionCache = new HashMap<>();
private Action bestAction = null;
public BoardAnalyzer(GameStateView state, EntityID turn) { public BoardAnalyzer(GameStateView state, EntityID turn) {
this.origin = Board.generate(state, turn); this.origin = Board.generate(state, turn);
this.player = turn.type; this.player = turn.type;
} }
public Action analyze(GameStateView state, IntVector2 position, EntityID turn) { public Action analyze(GameStateView state) {
ArrayList<Action> actions = origin.generateActions(state); Node tree = new Node(origin, new Action(ActionType.None));
//TODO: create minimax tree int maxDepth = 2;
int depth = 0;
long startTime = System.nanoTime();
while(System.nanoTime() - startTime <= 1000 * 1000 * 1000) {
expandTree(tree, state);
depth++;
if(depth > maxDepth) {
break;
}
}
alphaBetaMax(tree, Float.MIN_VALUE, Float.MAX_VALUE, true);
if(bestAction != null) {
return bestAction;
}
return new Action(ActionType.None); return new Action(ActionType.None);
} }
private Float alphaBetaMax(Node root, Float a, Float b, boolean main) {
if(!root.hasChildren()) {
return calculateScore(root.board);
}
Float w = a;
for(Node child: root.getChildren()) {
Float v;
if(child.board.turn.type == child.board.origin) {
v = alphaBetaMax(child, a, w, false);
}else {
v = alphaBetaMin(child, w, b);
}
if(v > w) {
w = v;
}
if(w >= b) {
if(main) {
bestAction = child.action;
}
return w;
}
if(main) {
bestAction = child.action;
}
}
return w;
}
private Float alphaBetaMin(Node root, Float a, Float b) {
if(!root.hasChildren()) {
return calculateScore(root.board);
}
Float w = b;
for(Node child: root.getChildren()) {
Float v;
if(child.board.turn.type == child.board.origin) {
v = alphaBetaMax(child, a, w, false);
}else {
v = alphaBetaMin(child, w, b);
}
if(v < w) {
w = v;
}
if(w <= a) {
return w;
}
}
return w;
}
private ArrayList<Node> getLeaves(Node root) {
return getLeaves(root, new ArrayList<>());
}
private ArrayList<Node> getLeaves(Node root, ArrayList<Node> accumulator) {
for(Node child: root.getChildren()) {
if(child.hasChildren()) {
getLeaves(child, accumulator);
}else {
accumulator.add(child);
}
}
return accumulator;
}
private void expandTree(Node root, GameStateView state) {
if(!root.hasChildren()) {
expandNode(root, state);
return;
}
for(Node child: root.getChildren()) {
if(child.board.ended) {
continue;
}
if(child.hasChildren()) {
expandTree(child, state);
}else {
expandNode(child, state);
}
}
}
private void expandNode(Node origin, GameStateView state) {
ArrayList<Action> actions = generateActions(origin.board, state);
for(Action action: actions) {
Board result = origin.board.applyAction(state, action);
scoreCache.put(result.hashCode(), result.calculateScore());
origin.addChild(result, action, result.calculateScore());
}
}
private ArrayList<Action> generateActions(Board board, GameStateView state) {
int hash = board.hashCode();
if(actionCache.containsKey(hash)) {
return actionCache.get(hash);
}else {
ArrayList<Action> result = board.generateActions(state);
actionCache.put(hash, result);
return result;
}
}
private Float calculateScore(Board board) {
int hash = board.hashCode();
if(scoreCache.containsKey(hash)) {
return scoreCache.get(hash);
}else {
Float result = board.calculateScore();
scoreCache.put(hash, result);
return result;
}
}
} }

View File

@ -0,0 +1,60 @@
package uulm.teamname.marvelous.gamelibrary.ai;
import java.util.ArrayList;
import java.util.List;
public class Node {
public final Board board;
public final Action action;
public final Float score;
private final List<Node> children = new ArrayList<>();
private Node parent = null;
public Node(Board board, Action action) {
this.board = board;
this.action = action;
this.score = 0f;
}
public Node(Board board, Action action, Float score) {
this.board = board;
this.action = action;
this.score = score;
}
public void addChild(Node child) {
child.setParent(this);
this.children.add(child);
}
public void addChild(Board board, Action action) {
this.addChild(new Node(board, action));
}
public void addChild(Board board, Action action, Float score) {
this.addChild(new Node(board, action, score));
}
public void addChildren(List<Node> children) {
for(Node t : children) {
t.setParent(this);
}
this.children.addAll(children);
}
public List<Node> getChildren() {
return children;
}
public boolean hasChildren() {
return !children.isEmpty();
}
private void setParent(Node parent) {
this.parent = parent;
}
public Node getParent() {
return parent;
}
}

View File

@ -8,15 +8,15 @@ import java.util.HashSet;
class Piece { class Piece {
public final PieceType type; public final PieceType type;
public final EntityID id; public EntityID id;
public final Stat hp; public Stat hp;
public final Stat mp; public Stat mp;
public final Stat ap; public Stat ap;
public final HashSet<StoneType> inventory; public HashSet<StoneType> inventory;
public final StoneType stone; public StoneType stone;
public Piece(PieceType type) { public Piece(PieceType type) {
this.type = type; this.type = type;
@ -69,17 +69,13 @@ class Piece {
} }
public Piece clone() { public Piece clone() {
return new Piece( Piece clone = new Piece(type);
this.type, clone.id = this.id != null ? this.id.clone() : null;
this.id, clone.hp = this.hp != null ? new Stat(StatType.HP, this.hp.getValue(), this.hp.getMax()) : null;
this.hp.getValue(), clone.mp = this.mp != null ? new Stat(StatType.MP, this.mp.getValue(), this.mp.getMax()) : null;
this.hp.getMax(), clone.ap = this.ap != null ? new Stat(StatType.AP, this.ap.getValue(), this.ap.getMax()) : null;
this.mp.getValue(), clone.inventory = this.inventory != null ? (HashSet<StoneType>)this.inventory.clone() : null;
this.mp.getMax(), clone.stone = this.stone;
this.ap.getValue(), return clone;
this.ap.getMax(),
this.inventory.toArray(new StoneType[0]),
this.stone
);
} }
} }