add gathering of resources

This commit is contained in:
Rokas Puzonas 2024-08-08 01:57:56 +03:00
parent d97b26a586
commit 0ffe638691
3 changed files with 164 additions and 47 deletions

View File

@ -27,10 +27,13 @@ pub fn build(b: *std.Build) void {
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.root_source_file = b.path("src/artifacts.zig"),
.target = target,
.optimize = optimize,
});
exe_unit_tests.linkLibC();
exe_unit_tests.addIncludePath(b.path("src"));
exe_unit_tests.addCSourceFile(.{ .file = b.path("src/timegm.c") });
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");

View File

@ -208,23 +208,60 @@ pub const FightResult = struct {
};
cooldown: Cooldown,
character: Character,
fight: Details,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !FightResult {
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !FightResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty;
return FightResult{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(character, allocator, api),
.fight = try Details.parse(api, fight)
};
}
};
pub fn deinit(self: *FightResult) void {
self.character.deinit();
pub const GatheringResult = struct {
const Details = struct {
const Item = struct {
id: ItemId,
quantity: i64
};
xp: i64,
items: std.BoundedArray(Item, 8),
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Details {
var items = std.BoundedArray(Item, 8).init(0) catch unreachable;
const items_obj = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
for (items_obj.items) |item_value| {
const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty;
const code = try json_utils.getStringRequired(item_obj, "code");
try items.append(Item{
.id = try api.getItemId(code),
.quantity = try json_utils.getIntegerRequired(item_obj, "quantity")
});
}
return Details{
.xp = try json_utils.getIntegerRequired(obj, "xp"),
.items = items,
};
}
};
cooldown: Cooldown,
details: Details,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatheringResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty;
return GatheringResult{
.cooldown = try Cooldown.parse(cooldown),
.details = try Details.parse(api, details)
};
}
};
@ -315,6 +352,7 @@ pub fn deinit(self: *ArtifactsAPI) void {
}
fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult {
std.log.debug("fetch {} {s}", .{method, path});
var uri = self.server_uri;
uri.path = .{ .raw = path };
@ -341,6 +379,8 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload
const result = try self.client.fetch(opts);
const response_body = response_storage.items;
std.log.debug("fetch result {}", .{result.status});
if (result.status != .ok) {
return ArtifactsFetchResult{
.arena = arena,
@ -476,7 +516,14 @@ pub fn actionFight(self: *ArtifactsAPI, name: []const u8) !FightResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
defer self.allocator.free(path);
return try self.fetchAndParseObject(FightResult, .POST, path, null, .{ self.allocator });
return try self.fetchAndParseObject(FightResult, .POST, path, null, .{ });
}
pub fn actionGathering(self: *ArtifactsAPI, name: []const u8) !GatheringResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name});
defer self.allocator.free(path);
return try self.fetchAndParseObject(GatheringResult, .POST, path, null, .{ });
}
pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult {
@ -508,3 +555,7 @@ pub fn actionBankDepositItem(self: *ArtifactsAPI, name: []const u8, code: []cons
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload, .{});
}
test "parse date time" {
try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?);
}

View File

@ -3,6 +3,8 @@ 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,
@ -22,13 +24,16 @@ const Position = struct {
const QueuedAction = union(enum) {
move: Position,
attack,
gathering,
depositGold: u64,
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
};
const ActionQueue = std.ArrayList(QueuedAction);
const ManagedCharacter = struct {
character: ArtifactsAPI.Character,
action_queue: std.ArrayList(QueuedAction),
action_queue: ActionQueue,
cooldown_expires_at: f64,
};
@ -72,12 +77,14 @@ const Manager = struct {
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 + 100));
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) {
if (now - managed_character.cooldown_expires_at >= -cooldown_margin) {
return managed_character;
}
}
@ -93,6 +100,81 @@ const Manager = struct {
}
};
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(.{ .gathering = {} });
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
@ -133,9 +215,6 @@ pub fn main() !void {
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
std.log.info("Characters online: {}", .{ status.characters_online });
const chicken_pos = Position{ .x = 0, .y = 1 };
const bank_pos = Position{ .x = 4, .y = 1 };
std.log.info("Starting main loop", .{});
while (manager.poll()) |char| {
if (char.action_queue.items.len > 0) {
@ -146,7 +225,6 @@ pub fn main() !void {
.attack => {
std.log.debug("{s} attacks", .{char.character.name});
var result = try api.actionFight(char.character.name);
defer result.deinit();
cooldown = result.cooldown;
char.character.gold += result.fight.gold;
@ -180,6 +258,15 @@ pub fn main() !void {
cooldown = result.cooldown;
char.character.inventory.removeItem(item.id, item.quantity);
},
.gathering => {
std.log.debug("{s} gathers", .{char.character.name});
var result = try api.actionGathering(char.character.name);
cooldown = result.cooldown;
for (result.details.items.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
}
}
}
@ -189,38 +276,14 @@ pub fn main() !void {
continue;
}
var character_pos = Position.init(char.character.x, char.character.y);
// Deposit items and gold to bank if full
if (char.character.getItemCount() == char.character.inventory_max_items) {
if (!character_pos.eql(bank_pos)) {
try char.action_queue.append(.{ .move = bank_pos });
}
for (char.character.inventory.slots) |slot| {
if (slot.quantity == 0) continue;
if (slot.id) |item_id| {
try char.action_queue.append(.{
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
});
}
}
if (char.character.gold > 0) {
try char.action_queue.append(.{ .depositGold = @intCast(char.character.gold) });
}
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);
}
// Go to chickens
if (!character_pos.eql(chicken_pos)) {
try char.action_queue.append(.{ .move = chicken_pos });
continue;
}
// Attack chickens
try char.action_queue.append(.{ .attack = {} });
}
}