wip: implement action tree and minimax for ai
This commit is contained in:
parent
81cbec5348
commit
7a5d9dca76
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,6 @@ enum ActionType {
|
|||||||
MeleeAttack,
|
MeleeAttack,
|
||||||
RangedAttack,
|
RangedAttack,
|
||||||
UseStone,
|
UseStone,
|
||||||
GiveStone
|
GiveStone,
|
||||||
|
EndTurn
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user