artificer/lib/brain.zig

573 lines
20 KiB
Zig

const std = @import("std");
const ArtifactsAPI = @import("artifacts-api");
const Server = ArtifactsAPI.Server;
const Allocator = std.mem.Allocator;
const Position = Server.Position;
const assert = std.debug.assert;
const CharacterTask = @import("./task.zig").Task;
const CharacterBrain = @This();
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
pub const QueuedAction = union(enum) {
move: Position,
fight,
gather,
deposit_gold: u64,
deposit_item: Server.ItemIdQuantity,
withdraw_item: Server.ItemIdQuantity,
craft_item: Server.ItemIdQuantity,
pub fn perform(self: QueuedAction, api: *Server, name: []const u8, ) !QueuedActionResult {
const log = std.log.default;
switch (self) {
.fight => {
log.debug("[{s}] attack", .{name});
return .{
.fight = api.actionFight(name)
};
},
.move => |pos| {
log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y});
return .{
.move = api.actionMove(name, pos.x, pos.y)
};
},
.deposit_gold => |quantity| {
log.debug("[{s}] deposit {} gold", .{name, quantity});
return .{
.deposit_gold = api.actionBankDepositGold(name, quantity)
};
},
.deposit_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity});
return .{
.deposit_item = api.actionBankDepositItem(name, code, item.quantity)
};
},
.withdraw_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity});
return .{
.withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity)
};
},
.gather => {
log.debug("[{s}] gather", .{name});
return .{
.gather = api.actionGather(name)
};
},
.craft_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity});
return .{
.craft_item = api.actionCraft(name, code, item.quantity)
};
}
}
}
};
const QueuedActionResult = union(enum) {
move: Server.MoveError!Server.MoveResult,
fight: Server.FightError!Server.FightResult,
gather: Server.GatherError!Server.GatherResult,
deposit_gold: Server.BankDepositGoldError!Server.GoldTransactionResult,
deposit_item: Server.BankDepositItemError!Server.ItemTransactionResult,
withdraw_item: Server.BankWithdrawItemError!Server.ItemTransactionResult,
craft_item: Server.CraftError!Server.CraftResult,
const Tag = @typeInfo(QueuedActionResult).Union.tag_type.?;
fn fieldType(comptime kind: Tag) type {
const field_type = std.meta.fields(QueuedActionResult)[@intFromEnum(kind)].type;
return @typeInfo(field_type).ErrorUnion.payload;
}
pub fn get(self: QueuedActionResult, comptime kind: Tag) ?fieldType(kind) {
return switch (self) {
kind => |v| v catch null,
else => null
};
}
};
comptime {
assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len);
}
name: []const u8,
action_queue: std.ArrayList(QueuedAction),
task: ?*CharacterTask = null,
pub fn init(allocator: Allocator, name: []const u8) !CharacterBrain {
return CharacterBrain{
.name = try allocator.dupe(u8, name),
.action_queue = std.ArrayList(QueuedAction).init(allocator),
};
}
pub fn deinit(self: CharacterBrain) void {
const allocator = self.action_queue.allocator;
allocator.free(self.name);
self.action_queue.deinit();
}
fn currentTime() f64 {
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
return timestamp / std.time.ms_per_s;
}
pub fn performNextAction(self: *CharacterBrain, api: *Server) !void {
const log = std.log.default;
assert(self.action_queue.items.len > 0);
const APIError = Server.APIError;
const retry_delay = 0.5; // 500ms
var character = api.findCharacterPtr(self.name).?;
const next_action = self.action_queue.items[0];
const action_result = try next_action.perform(api, self.name);
switch (action_result) {
.fight => |result| {
const FightError = Server.FightError;
_ = result catch |err| switch (err) {
FightError.CharacterInCooldown,
FightError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry fighting", .{self.name});
return;
},
FightError.CharacterIsFull,
FightError.MonsterNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
FightError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.move => |result| {
const MoveError = Server.MoveError;
_ = result catch |err| switch (err) {
MoveError.CharacterIsBusy,
MoveError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry moving", .{self.name});
return;
},
MoveError.CharacterAtDestination => {
// Not great, but I guess the goal achieved? The character is at the desired location.
log.warn("[{s}] tried to move, but already at destination", .{self.name});
},
MoveError.MapNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
MoveError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.deposit_gold => |result| {
const BankDepositGoldError = Server.BankDepositGoldError;
_ = result catch |err| switch (err) {
BankDepositGoldError.BankIsBusy,
BankDepositGoldError.CharacterIsBusy,
BankDepositGoldError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry depositing gold", .{self.name});
return;
},
BankDepositGoldError.NotEnoughGold,
BankDepositGoldError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankDepositGoldError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.deposit_item => |result| {
const BankDepositItemError = Server.BankDepositItemError;
_ = result catch |err| switch (err) {
BankDepositItemError.BankIsBusy,
BankDepositItemError.CharacterIsBusy,
BankDepositItemError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry depositing item", .{self.name});
return;
},
BankDepositItemError.ItemNotFound,
BankDepositItemError.NotEnoughItems,
BankDepositItemError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankDepositItemError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.withdraw_item => |result| {
const BankWithdrawItemError = Server.BankWithdrawItemError;
_ = result catch |err| switch (err) {
BankWithdrawItemError.CharacterIsBusy,
BankWithdrawItemError.CharacterInCooldown,
BankWithdrawItemError.BankIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
BankWithdrawItemError.ItemNotFound,
BankWithdrawItemError.NotEnoughItems,
BankWithdrawItemError.CharacterIsFull,
BankWithdrawItemError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankWithdrawItemError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.gather => |result| {
const GatherError = Server.GatherError;
_ = result catch |err| switch (err) {
GatherError.CharacterInCooldown,
GatherError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
GatherError.NotEnoughSkill,
GatherError.CharacterIsFull,
GatherError.ResourceNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
GatherError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.craft_item => |result| {
const CraftError = Server.CraftError;
_ = result catch |err| switch (err) {
CraftError.CharacterInCooldown,
CraftError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
CraftError.RecipeNotFound,
CraftError.NotEnoughItems,
CraftError.NotEnoughSkill,
CraftError.CharacterIsFull,
CraftError.WorkshopNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
CraftError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
}
}
_ = self.action_queue.orderedRemove(0);
self.onActionCompleted(action_result);
}
fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void {
if (self.task == null) return;
switch (self.task.?.*) {
.fight => |*args| {
if (result.get(.fight)) |r| {
const fight_result: Server.FightResult = r;
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.until.item.id);
}
},
.gather => |*args| {
if (result.get(.gather)) |r| {
const gather_resutl: Server.GatherResult = r;
const items = gather_resutl.details.items;
args.progress += items.getQuantity(args.until.item.id);
}
},
.craft => |*args| {
if (result.get(.craft_item)) |r| {
const craft_result: Server.CraftResult = r;
const items = craft_result.details.items;
args.progress += items.getQuantity(args.target.id);
}
}
}
}
pub fn isTaskFinished(self: *CharacterBrain) bool {
if (self.task == null) {
return false;
}
return self.task.?.isComplete();
}
pub fn performTask(self: *CharacterBrain, api: *Server) !void {
if (self.task == null) {
std.log.debug("[{s}] idle", .{self.name});
return;
}
switch (self.task.?.*) {
.fight => |args| {
try self.fightRoutine(api, args.at);
},
.gather => |args| {
try self.gatherRoutine(api, args.at);
},
.craft => |args| {
try self.craftRoutine(api, args.at, args.target.id, args.target.quantity);
}
}
}
fn moveIfNeeded(self: *CharacterBrain, api: *Server, pos: Position) !bool {
const character = api.findCharacter(self.name).?;
if (character.position.eql(pos)) {
return false;
}
try self.action_queue.append(.{ .move = pos });
return true;
}
pub fn depositItemsToBank(self: *CharacterBrain, api: *Server) !bool {
var character = api.findCharacter(self.name).?;
const action_queue = &self.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() == 0) {
return false;
}
if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slice()) |slot| {
try action_queue.append(.{
.deposit_item = .{ .id = slot.id, .quantity = slot.quantity }
});
}
return true;
}
fn depositIfFull(self: *CharacterBrain, api: *Server) !bool {
const character = api.findCharacter(self.name).?;
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
_ = try self.depositItemsToBank(api);
if (character.gold > 0) {
try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
return true;
}
fn fightRoutine(self: *CharacterBrain, api: *Server, enemy_position: Position) !void {
if (try self.depositIfFull(api)) {
return;
}
if (try self.moveIfNeeded(api, enemy_position)) {
return;
}
try self.action_queue.append(.{ .fight = {} });
}
fn gatherRoutine(self: *CharacterBrain, api: *Server, resource_position: Position) !void {
if (try self.depositIfFull(api)) {
return;
}
if (try self.moveIfNeeded(api, resource_position)) {
return;
}
try self.action_queue.append(.{ .gather = {} });
}
fn withdrawFromBank(self: *CharacterBrain, api: *Server, items: []const Server.Slot) !bool {
var character = api.findCharacter(self.name).?;
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
}
}
if (has_all_items) return false;
if (try self.moveIfNeeded(api, bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try self.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
}
}
return true;
}
fn craftItem(self: *CharacterBrain, api: *Server, workstation: Position, id: Server.ItemId, quantity: u64) !bool {
var character = api.findCharacter(self.name).?;
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
return false;
}
if (try self.moveIfNeeded(api, workstation)) {
return true;
}
try self.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
return true;
}
fn craftRoutine(self: *CharacterBrain, api: *Server, workstation: Position, id: Server.ItemId, quantity: u64) !void {
var character = api.findCharacter(self.name).?;
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
if (try self.depositItemsToBank(api)) {
return;
}
}
const code = api.getItemCode(id) orelse return error.InvalidItemId;
const target_item = try api.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) {
return error.NotCraftable;
}
const recipe = target_item.craft.?;
var needed_items = recipe.items;
for (needed_items.slice()) |*needed_item| {
needed_item.quantity *= quantity;
}
if (try self.withdrawFromBank(api, needed_items.slice())) {
return;
}
if (try self.craftItem(api, workstation, id, quantity)) {
return;
}
}