From 7a5d9dca76eca08e33fa78773508fdfa099a9708 Mon Sep 17 00:00:00 2001 From: punchready Date: Mon, 7 Jun 2021 03:06:33 +0200 Subject: [PATCH] wip: implement action tree and minimax for ai --- .../teamname/marvelous/gamelibrary/ai/AI.java | 12 +- .../marvelous/gamelibrary/ai/Action.java | 4 + .../marvelous/gamelibrary/ai/ActionType.java | 3 +- .../marvelous/gamelibrary/ai/Board.java | 60 +++++++- .../gamelibrary/ai/BoardAnalyzer.java | 139 +++++++++++++++++- .../marvelous/gamelibrary/ai/Node.java | 60 ++++++++ .../marvelous/gamelibrary/ai/Piece.java | 32 ++-- 7 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Node.java diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/AI.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/AI.java index 9e25077..71ceed0 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/AI.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/AI.java @@ -48,11 +48,13 @@ class AI { Character character = (Character)state.getEntities().findEntity(turn); BoardAnalyzer analyzer = new BoardAnalyzer(state, turn); - Action action = analyzer.analyze(state, character.getPosition(), turn); + Action action = analyzer.analyze(state); switch(action.type) { case None -> { System.out.println(" Result: doing nothing"); + result.add(new RequestBuilder(RequestType.EndRoundRequest) + .buildGameRequest()); } case Move -> { System.out.println(" Result: moving to " + action.target); @@ -163,11 +165,13 @@ class AI { .buildCharacterRequest()); } } + case EndTurn -> { + System.out.println(" Result: ending turn"); + result.add(new RequestBuilder(RequestType.EndRoundRequest) + .buildGameRequest()); + } } - result.add(new RequestBuilder(RequestType.EndRoundRequest) - .buildGameRequest()); - return result; } } diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Action.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Action.java index e0fb690..fdc2f50 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Action.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Action.java @@ -51,4 +51,8 @@ class Action { this.targetEntity = targetEntity; this.stone = stone; } + + public String toString() { + return this.type.toString(); + } } diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/ActionType.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/ActionType.java index 37689c4..f089f1a 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/ActionType.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/ActionType.java @@ -6,5 +6,6 @@ enum ActionType { MeleeAttack, RangedAttack, UseStone, - GiveStone + GiveStone, + EndTurn } diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Board.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Board.java index 44926af..222202d 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Board.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Board.java @@ -11,13 +11,22 @@ import java.util.Objects; class Board { private final Piece[][] data; - private final EntityType origin; - private final EntityID turn; + public final EntityType origin; + public final EntityID turn; + public final boolean ended; private Board(Piece[][] data, EntityType origin, EntityID turn) { this.data = data; this.origin = origin; 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) { @@ -100,10 +109,16 @@ class Board { } for(IntVector2 dir: IntVector2.CardinalDirections) { 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; } - 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)); } } @@ -142,6 +157,9 @@ class Board { continue; } for(StoneType stone: StoneType.values()) { + if(state.getStoneCooldown(stone) > 0) { + continue; + } if(!character.inventory.hasStone(stone)) { 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 switch(action.type) { @@ -262,9 +285,10 @@ class Board { case MeleeAttack -> { clone[action.target.getY()][action.target.getX()].hp.decreaseValue(character.meleeDamage); current.ap.decreaseValue(1); + //todo: drop stones........ } 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); } case UseStone -> { @@ -292,11 +316,31 @@ class Board { case GiveStone -> { } + case EndTurn -> { + ended = true; + } } - //...get next turn in turn order... - //TODO: figure out next turn properly - return new Board(clone, origin, turn); + if(ended || (current.mp.getValue() == 0 && current.ap.getValue() == 0)) { + ArrayList 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); + } } protected float calculateScore() { diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/BoardAnalyzer.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/BoardAnalyzer.java index 2f348b0..6a25f2a 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/BoardAnalyzer.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/BoardAnalyzer.java @@ -1,6 +1,5 @@ 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.EntityType; import uulm.teamname.marvelous.gamelibrary.gamelogic.GameStateView; @@ -11,18 +10,148 @@ import java.util.HashMap; class BoardAnalyzer { private final Board origin; private final EntityType player; - private final HashMap cache = new HashMap<>(); + private final HashMap scoreCache = new HashMap<>(); + private final HashMap> actionCache = new HashMap<>(); + private Action bestAction = null; public BoardAnalyzer(GameStateView state, EntityID turn) { this.origin = Board.generate(state, turn); this.player = turn.type; } - public Action analyze(GameStateView state, IntVector2 position, EntityID turn) { - ArrayList actions = origin.generateActions(state); + public Action analyze(GameStateView 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); } + + 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 getLeaves(Node root) { + return getLeaves(root, new ArrayList<>()); + } + + private ArrayList getLeaves(Node root, ArrayList 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 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 generateActions(Board board, GameStateView state) { + int hash = board.hashCode(); + if(actionCache.containsKey(hash)) { + return actionCache.get(hash); + }else { + ArrayList 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; + } + } } diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Node.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Node.java new file mode 100644 index 0000000..3db2116 --- /dev/null +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Node.java @@ -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 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 children) { + for(Node t : children) { + t.setParent(this); + } + this.children.addAll(children); + } + + public List getChildren() { + return children; + } + + public boolean hasChildren() { + return !children.isEmpty(); + } + + private void setParent(Node parent) { + this.parent = parent; + } + + public Node getParent() { + return parent; + } +} diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Piece.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Piece.java index 49e8489..153e83f 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Piece.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/ai/Piece.java @@ -8,15 +8,15 @@ import java.util.HashSet; class Piece { public final PieceType type; - public final EntityID id; + public EntityID id; - public final Stat hp; - public final Stat mp; - public final Stat ap; + public Stat hp; + public Stat mp; + public Stat ap; - public final HashSet inventory; + public HashSet inventory; - public final StoneType stone; + public StoneType stone; public Piece(PieceType type) { this.type = type; @@ -69,17 +69,13 @@ class Piece { } public Piece clone() { - return new Piece( - this.type, - this.id, - this.hp.getValue(), - this.hp.getMax(), - this.mp.getValue(), - this.mp.getMax(), - this.ap.getValue(), - this.ap.getMax(), - this.inventory.toArray(new StoneType[0]), - this.stone - ); + Piece clone = new Piece(type); + clone.id = this.id != null ? this.id.clone() : null; + clone.hp = this.hp != null ? new Stat(StatType.HP, this.hp.getValue(), this.hp.getMax()) : null; + clone.mp = this.mp != null ? new Stat(StatType.MP, this.mp.getValue(), this.mp.getMax()) : null; + clone.ap = this.ap != null ? new Stat(StatType.AP, this.ap.getValue(), this.ap.getMax()) : null; + clone.inventory = this.inventory != null ? (HashSet)this.inventory.clone() : null; + clone.stone = this.stone; + return clone; } }