artificer/src/main.zig

290 lines
9.3 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const ArtifactsAPI = @import("artifacts.zig");
const Allocator = std.mem.Allocator;
// pub const std_options = .{ .log_level = .debug };
const Position = struct {
x: i64,
y: i64,
fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
};
}
fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}
};
const QueuedAction = union(enum) {
move: Position,
attack,
gather,
depositGold: u64,
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
};
const ActionQueue = std.ArrayList(QueuedAction);
const ManagedCharacter = struct {
character: ArtifactsAPI.Character,
action_queue: ActionQueue,
cooldown_expires_at: f64,
};
fn currentTime() f64 {
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
return timestamp / std.time.ms_per_s;
}
const Manager = struct {
allocator: Allocator,
characters: std.ArrayList(ManagedCharacter),
api: *ArtifactsAPI,
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
.api = api,
.characters = std.ArrayList(ManagedCharacter).init(allocator),
};
}
fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void {
try self.characters.append(ManagedCharacter{
.character = character,
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
.cooldown_expires_at = character.cooldown_expiration
});
}
fn poll(self: *Manager) ?*ManagedCharacter {
if (self.characters.items.len == 0) return null;
var earliest_expiration = self.characters.items[0].cooldown_expires_at;
var now = currentTime();
for (self.characters.items) |managed_character| {
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at);
}
if (earliest_expiration > now) {
const duration_s = earliest_expiration - now;
const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
std.log.debug("waiting for {d:.3}s", .{duration_s});
std.time.sleep(std.time.ns_per_ms * duration_ms);
}
const cooldown_margin = 0.1; // 100ms
now = currentTime();
for (self.characters.items) |*managed_character| {
if (now - managed_character.cooldown_expires_at >= -cooldown_margin) {
return managed_character;
}
}
return null;
}
fn deinit(self: Manager) void {
for (self.characters.items) |managed_character| {
managed_character.action_queue.deinit();
}
self.characters.deinit();
}
};
fn depositIfFull(managed_char: *ManagedCharacter) !bool {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
var character_pos = Position.init(character.x, character.y);
const bank_pos = Position{ .x = 4, .y = 1 };
if (!character_pos.eql(bank_pos)) {
try action_queue.append(.{ .move = bank_pos });
}
for (character.inventory.slots) |slot| {
if (slot.quantity == 0) continue;
if (slot.id) |item_id| {
try action_queue.append(.{
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
});
}
}
if (character.gold > 0) {
try action_queue.append(.{ .depositGold = @intCast(character.gold) });
}
return true;
}
fn attackChickenRoutine(managed_char: *ManagedCharacter) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
const chicken_pos = Position{ .x = 0, .y = 1 };
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
return;
}
// Go to chickens
if (!character_pos.eql(chicken_pos)) {
try action_queue.append(.{ .move = chicken_pos });
return;
}
// Attack chickens
try action_queue.append(.{ .attack = {} });
}
fn gatherResourceRoutine(managed_char: *ManagedCharacter, resource_pos: Position) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
return;
}
if (!character_pos.eql(resource_pos)) {
try action_queue.append(.{ .move = resource_pos });
return;
}
try action_queue.append(.{ .gather = {} });
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var api = try ArtifactsAPI.init(allocator);
defer api.deinit();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len >= 2) {
const filename = args[1];
const cwd = std.fs.cwd();
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
try api.setToken(std.mem.trim(u8,token,"\n\t "));
}
if (api.token == null) {
return error.MissingToken;
}
var manager = Manager.init(allocator, &api);
defer manager.deinit();
const characters = try api.listMyCharacters();
defer characters.deinit();
for (characters.items) |character| {
try manager.addCharacter(character);
}
const status = try api.getServerStatus();
defer status.deinit();
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
std.log.info("Characters online: {}", .{ status.characters_online });
std.log.info("Starting main loop", .{});
while (manager.poll()) |char| {
if (char.action_queue.items.len > 0) {
const action = char.action_queue.items[0];
var cooldown: ArtifactsAPI.Cooldown = undefined;
switch (action) {
.attack => {
std.log.debug("{s} attacks", .{char.character.name});
var result = try api.actionFight(char.character.name);
cooldown = result.cooldown;
char.character.gold += result.fight.gold;
for (result.fight.drops.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
}
},
.move => |pos| {
std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y});
var result = try api.actionMove(char.character.name, pos.x, pos.y);
defer result.deinit();
cooldown = result.cooldown;
char.character.x = pos.x;
char.character.y = pos.y;
},
.depositGold => |quantity| {
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
var result = try api.actionBankDepositGold(char.character.name, quantity);
defer result.deinit();
cooldown = result.cooldown;
char.character.gold -= @intCast(quantity);
assert(char.character.gold >= 0);
},
.depositItem => |item| {
std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name});
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
var result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
defer result.deinit();
cooldown = result.cooldown;
char.character.inventory.removeItem(item.id, item.quantity);
},
.gather => {
std.log.debug("{s} gathers", .{char.character.name});
var result = try api.actionGather(char.character.name);
cooldown = result.cooldown;
for (result.details.items.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
}
}
}
char.cooldown_expires_at = cooldown.expiration;
_ = char.action_queue.orderedRemove(0);
continue;
}
if (std.mem.eql(u8, char.character.name, "Devin")) {
try gatherResourceRoutine(char, .{ .x = -1, .y = 0 }); // Ash trees
} else if (std.mem.eql(u8, char.character.name, "Dawn")) {
try gatherResourceRoutine(char, .{ .x = 2, .y = 0 }); // Copper ore
} else if (std.mem.eql(u8, char.character.name, "Diana")) {
try gatherResourceRoutine(char, .{ .x = 4, .y = 2 }); // Gudgeon fish
} else {
try attackChickenRoutine(char);
}
}
}