From cdb3bcbcdd82a98dbe1d46f49a3d70e343dbf6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Bayo=C3=A1n=20Santiago=20Calder=C3=B3n?= Date: Sat, 2 Oct 2021 10:45:04 -0400 Subject: [PATCH 1/2] Initial Draft to get feedback First pass at API --- kits/julia/README.md | 49 ++++ kits/julia/simple/lux/annotate.jl | 42 ++++ kits/julia/simple/lux/constants.jl | 63 +++++ kits/julia/simple/lux/game.jl | 108 +++++++++ kits/julia/simple/lux/game_objects.jl | 331 ++++++++++++++++++++++++++ kits/julia/simple/main.jl | 79 ++++++ kits/julia/simple/main.py | 68 ++++++ package_all_kits.sh | 3 +- tests/kit.spec.ts | 19 ++ tests/match.ts | 1 + 10 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 kits/julia/README.md create mode 100644 kits/julia/simple/lux/annotate.jl create mode 100644 kits/julia/simple/lux/constants.jl create mode 100644 kits/julia/simple/lux/game.jl create mode 100644 kits/julia/simple/lux/game_objects.jl create mode 100644 kits/julia/simple/main.jl create mode 100644 kits/julia/simple/main.py diff --git a/kits/julia/README.md b/kits/julia/README.md new file mode 100644 index 00000000..d1ad50b0 --- /dev/null +++ b/kits/julia/README.md @@ -0,0 +1,49 @@ +# Julia Kit + +This is the folder for the Julia kit. Please make sure to read the instructions as they are important regarding how you will write a bot and submit it to the competition servers. + +Make sure to check our [Discord](https://discord.gg/aWJt3UAcgn) or the [Kaggle forums](https://www.kaggle.com/c/lux-ai-2021/discussion) for announcements if there are any breaking changes. + +## Getting Started + +To get started, download the `simple` folder from this repository. + +Then navigate to that folder via command line e.g. `cd simple` or for windows `chdir simple`. + +Your main code will go into `bot/src/main/julia/XXX.jl` and you can use create other files to help you as well. You should leave `main.py` and the `lux` package alone in `bot/src/main/julia/`. Read the `Bot.jl` file to get an idea of how a bot is programmed and a feel for the Julia API. + +Make sure you have Julia v1.6 (something about competition server). + +To confirm your setup, in your bot folder run + +To then test run your bot, run + +``` +lux-ai-2021 run.sh run.sh --out=replay.json +``` + +which should produce no errors. + +To debug your bot locally start a game with `debug.sh` script (also increase the bot timeout): + +```lux-ai-2021 run.sh debug.sh --maxtime=9999999``` + +Then, connect the debugger to `localhost:5005`. Now you can use breakpoints and other debug features! + +If you find some bugs or unfixable errors, please let an admin know via Discord, the forums, or email us. + +## Developing + +Now that you have some code and you checked that your code works by trying to submit something, you are now ready to start programming your bot and having fun! + +If you haven't read it already, take a look at the [design specifications for the competition](https://lux-ai.org/specs-2021). This will go through the rules and objectives of the competition. + +All of our kits follow a common API through which you can use to access various functions and properties that will help you develop your strategy and bot. The markdown version is here: https://github.com/Lux-AI-Challenge/Lux-Design-2021/blob/master/kits/README.md + +## Submitting to Kaggle + +You can just create submission archive using `./pack.sh`. Upload `submission.tar.gz` to Kaggle "Submission" section. + +## FAQ + +As questions come up, this will be populated with frequently asked questions regarding the Julia kit. diff --git a/kits/julia/simple/lux/annotate.jl b/kits/julia/simple/lux/annotate.jl new file mode 100644 index 00000000..08eb0afb --- /dev/null +++ b/kits/julia/simple/lux/annotate.jl @@ -0,0 +1,42 @@ +""" + circle(x::Integer, y::Integer) :: String + +Returns the draw circle annotation action. +Will draw a unit sized circle on the visualizer at the current turn centered at the `Cell` at the given x, y coordinates. +""" +circle(x::Integer, y::Integer) = "dc $x $y" + +""" + x(x::Integer, y::Integer) :: String + +Returns the draw X annotation action. +Will draw a unit sized X on the visualizer at the current turn centered at the `Cell` at the given x, y coordinates. +""" +x(x::Integer, y::Integer) = "dx $x $y" + + +""" + line(x1::Integer, y1::Integer, x2::Integer, y2::Integer) :: String + +Returns the draw line annotation action. +Will draw a line from the center of the `Cell` at (x1, y1) to the center of the `Cell` at (x2, y2). +""" +line(x1::Integer, y1::Integer, x2::Integer, y2::Integer) = "dl $x1 $y1 $x2 $y2" + +""" + text(x::Integer, y::Integer, message::AbstractString, + fontsize::Integer = 16) :: String + +Returns the draw text annotation action. +Will write text on top of the tile at (x, y) with the particular message and fontsize. +""" +text(x::Integer, y::Integer, message::AbstractString, fontsize::Integer = 16) = + "dt $x $y '$message' $fontsize" + +""" + sidetext(message::AbstractString) :: String + +Returns the draw side text annotation action. +Will write text that is displayed on that turn on the side of the visualizer. +""" +sidetext(message::AbstractString) = "dst '$message'" diff --git a/kits/julia/simple/lux/constants.jl b/kits/julia/simple/lux/constants.jl new file mode 100644 index 00000000..ff34cb72 --- /dev/null +++ b/kits/julia/simple/lux/constants.jl @@ -0,0 +1,63 @@ +""" + GameConstants(;DAY_LENGTH = 30, NIGHT_LENGTH = 10, MAX_DAYS = 360, + LIGHT_UPKEEP = (CITY = 23, WORKER = 4, CART = 10), + WOOD_GROWTH_RATE = 1.025, MAX_WOOD_AMOUNT = 500, + CITY_BUILD_COST = 100, CITY_ADJACENCY_BONUS = 5, + RESOURCE_CAPACITY = (WORKER = 100, CART = 2000), + WORKER_COLLECTION_RATE = (WOOD = 20, COAL = 5, URANIUM = 2), + RESOURCE_TO_FUEL_RATE = (WOOD = 1, COAL = 10, URANIUM = 40), + RESEARCH_REQUIREMENTS = (COAL = 50, URANIUM = 200), + CITY_ACTION_COOLDOWN = 10, + UNIT_ACTION_COOLDOWN = (WORKER = 2, CART = 3), + MAX_ROAD = 6, MIN_ROAD = 0, + CART_ROAD_DEVELOPMENT_RATE = 0.75, + PILLAGE_RATE = 0.5) + +This will contain constants on all game parameters like the max turns, the light upkeep of CityTiles etc. +If there are any crucial changes to the starter kits, typically only this object will change. +""" +struct GameConstants + DAY_LENGTH :: Int + NIGHT_LENGTH :: Int + MAX_DAYS :: Int + LIGHT_UPKEEP :: NamedTuple{(:CITY, :WORKER, :CART), NTuple{3, Int}} + WOOD_GROWTH_RATE :: Float64 + MAX_WOOD_AMOUNT :: Int + CITY_BUILD_COST :: Int + CITY_ADJACENCY_BONUS :: Int + RESOURCE_CAPACITY :: NamedTuple{(:WORKER, :CART), NTuple{2, Int}} + WORKER_COLLECTION_RATE :: NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}} + RESOURCE_TO_FUEL_RATE :: NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}} + RESEARCH_REQUIREMENTS :: NamedTuple{(:COAL, :URANIUM), NTuple{2, Int}} + CITY_ACTION_COOLDOWN :: Int + UNIT_ACTION_COOLDOWN :: NamedTuple{(:WORKER, :CART), NTuple{2, Int}} + MAX_ROAD :: Int + MIN_ROAD :: Int + CART_ROAD_DEVELOPMENT_RATE :: Float64 + PILLAGE_RATE :: Float64 + function GameConstants(;DAY_LENGTH = 30, + NIGHT_LENGTH = 10, + MAX_DAYS = 360, + LIGHT_UPKEEP = (CITY = 23, WORKER = 4, CART = 10), + WOOD_GROWTH_RATE = 1.025, + MAX_WOOD_AMOUNT = 500, + CITY_BUILD_COST = 100, + CITY_ADJACENCY_BONUS = 5, + RESOURCE_CAPACITY = (WORKER = 100, CART = 2000), + WORKER_COLLECTION_RATE = (WOOD = 20, COAL = 5, URANIUM = 2), + RESOURCE_TO_FUEL_RATE = (WOOD = 1, COAL = 10, URANIUM = 40), + RESEARCH_REQUIREMENTS = (COAL = 50, URANIUM = 200), + CITY_ACTION_COOLDOWN = 10, + UNIT_ACTION_COOLDOWN = (WORKER = 2, CART = 3), + MAX_ROAD = 6, + MIN_ROAD = 0, + CART_ROAD_DEVELOPMENT_RATE = 0.75, + PILLAGE_RATE = 0.5) + new(DAY_LENGTH, NIGHT_LENGTH, MAX_DAYS, LIGHT_UPKEEP, WOOD_GROWTH_RATE, MAX_WOOD_AMOUNT, + CITY_BUILD_COST, CITY_ADJACENCY_BONUS, RESOURCE_CAPACITY, WORKER_COLLECTION_RATE, + RESOURCE_TO_FUEL_RATE, RESEARCH_REQUIREMENTS, CITY_ACTION_COOLDOWN, + UNIT_ACTION_COOLDOWN, MAX_ROAD, MIN_ROAD, CART_ROAD_DEVELOPMENT_RATE, PILLAGE_RATE) + end +end + +const GAME_CONSTANTS = GameConstants() diff --git a/kits/julia/simple/lux/game.jl b/kits/julia/simple/lux/game.jl new file mode 100644 index 00000000..33084ea0 --- /dev/null +++ b/kits/julia/simple/lux/game.jl @@ -0,0 +1,108 @@ +const INPUT_CONSTANTS = Dict("RESEARCH_POINTS" => "rp", + "RESOURCES" => "r", + "UNITS" => "u", + "CITY" => "c", + "CITY_TILES" => "ct", + "ROADS" => "ccd", + "DONE" => "D_DONE") +const DIRECTIONS_OUTPUT = Dict(north => "n", + west => "w", + south => "s", + east => "e", + center => "c") + +mutable struct Game + id :: Int + turn :: Int + map :: GameMap + players :: NTuple{2, Player} + function Game(messages::AbstractVector{<:AbstractString}) + id = parse(Int, messages[1]) + turn = -1 + # get some other necessary initial input + mapinfo = split(messages[2], " ") + dim = parse(Int, mapinfo[1]) + game_map = GameMap(dim) + players = (Player(0), Player(1)) + new(id, turn, game_map, players) + end +end + +_end_turn(::Game) = print("D_FINISH") + +function _reset_player_states!(obj::Game) + for player in obj.players + empty!(player.units) + empty!(player.cities) + end + nothing +end + +""" + _update!(obj::Game, messages) + +Update state. +""" +function _update!(obj::Game, messages::AbstractVector{<:AbstractString}) + obj.map .= GameMap(size(obj.map, 1)) + obj.turn += 1 + _reset_player_states!(obj) + for update in messages + if update == "D_DONE" + break + end + strs = split(update, " ") + input_identifier = strs[1] + if input_identifier == INPUT_CONSTANTS["RESEARCH_POINTS"] + team = parse(Int, strs[2]) + obj.players[team + 1].research_points = parse(Int, strs[3]) + elseif input_identifier == INPUT_CONSTANTS["RESOURCES"] + r_type = strs[2] + x = parse(Int, strs[3]) + y = parse(Int, strs[4]) + amt = parse(Float64, strs[5]) + pos = Position(x, y) + resource = Resource(r_type, amt) + obj.map[x + 1, y + 1] = Cell(pos, resource) + elseif input_identifier == INPUT_CONSTANTS["UNITS"] + unittype = parse(Int, strs[2]) + team = parse(Int, strs[3]) + unitid = strs[4] + x = parse(Int, strs[5]) + y = parse(Int, strs[6]) + pos = Position(x, y) + cooldown = parse(Float64, strs[7]) + wood = parse(Int, strs[8]) + coal = parse(Int, strs[9]) + uranium = parse(Int, strs[10]) + cargo = Cargo(wood, coal, uranium) + append!(obj.players[team + 1].units, Unit(team, unittype, unitid, pos, cooldown, cargo)) + elseif input_identifier == INPUT_CONSTANTS["CITY"] + team = parse(Int, strs[2]) + cityid = strs[3] + fuel = parse(Float64, strs[4]) + lightupkeep = parse(Float64, strs[5]) + obj.players[team + 1].cities[cityid] = City(team, cityid, fuel, lightupkeep) + elseif input_identifier == INPUT_CONSTANTS["CITY_TILES"] + team = parse(Int, strs[2]) + cityid = strs[3] + x = parse(Int, strs[4]) + y = parse(Int, strs[5]) + pos = Position(x, y) + cooldown = parse(Float64, strs[6]) + city = obj.players[team + 1].cities[cityid] + citytile = CityTile(cityid, team, pos, cooldown) + append!(city.citytiles, citytile) + cell = get_cell(obj.map, pos) + cell.citytile = citytile + obj.players[team + 1].city_tile_count += 1 + elseif input_identifier == INPUT_CONSTANTS["ROADS"] + x = parse(Int, strs[2]) + y = parse(Int, strs[3]) + pos = Position(x, y) + road = parse(Float64, strs[4]) + cell = get_cell(obj.map, pos) + cell.road = road + end + end +end diff --git a/kits/julia/simple/lux/game_objects.jl b/kits/julia/simple/lux/game_objects.jl new file mode 100644 index 00000000..892bfa6c --- /dev/null +++ b/kits/julia/simple/lux/game_objects.jl @@ -0,0 +1,331 @@ +@enum RESOURCES wood coal uranium +@enum DIRECTIONS north west south east center +@enum UNITS worker cart + +""" + Position(x::Integer, y::Integer) :: Position + +""" +struct Position + x :: Int + y :: Int +end + +""" + is_adjacent(obj::Position, pos::Position) :: Bool + +Returns true if this `Position` is adjacent to pos. False otherwise. +""" +function is_adjacent(obj::Position, pos::Position) + Δx = obj.x - pos.x + Δy = obj.y - pos.y + Δx^2 + Δy^2 ≤ 2 +end + +""" + is_adjacent(obj::Position, pos::Position) :: Bool + +Returns true if this `Position` is equal to the other pos object by checking x, y coordinates. False otherwise. +""" +equals(obj::Position, pos::Position) = obj.x == pos.x && obj.y == pos.y + +""" + translate(obj::Position, direction::DIRECTIONS, units::Integer) :: Position + +Returns the `Position` equal to going in a direction units number of times from this Position. +""" +function translate(obj::Position, direction::DIRECTIONS, units::Integer) + x = obj.x + y = obj.y + if direction == north + x -= units + elseif direction == south + x += units + elseif direction == west + y -= units + elseif direction == east + y += units + elseif direction == center + end + Position(x, y) +end + +""" + distance_to(obj::Position, pos::Position) :: Float64 + +Returns the Manhattan (rectilinear) distance from this Position to pos, +""" +distance_to(obj::Position, pos::Position) = abs(obj.x - pos.x) + abs(obj.y - pos.y) + +""" + direction_to(obj::Position, target_pos::Position) :: DIRECTIONS + +Returns the direction that would move you closest to target_pos from this Position if you took a single step. In particular, will return DIRECTIONS.CENTER if this Position is equal to the target_pos. Note that this does not check for potential collisions with other units but serves as a basic pathfinding method. +""" +function direction_to(obj::Position, target_pos::Position) + Δx = obj.x - target_pos.x + Δy = obj.y - target_pos.y + if Δx == Δy == 0 + center + elseif abs(Δx) ≥ abs(Δy) + obj.x > target_pos.x ? west : east + else + obj.y < target_pos.y ? south : north + end +end + +""" + CityTile(cityid::AbstractString, team::Integer, pos::Position, cooldown::Real) :: CityTile + +""" +struct CityTile + cityid :: String + team :: Int + pos :: Position + cooldown :: Float64 +end + +""" + research(obj::CityTile) :: String + +Returns the research action. +""" +function research(obj::CityTile) + x, y = obj.pos + "r $x $y" +end + +""" + build_worker(obj::CityTile) :: String + +Returns the build worker action. When applied and requirements are met, a worker will be built at the `City`. +""" +function build_worker(obj::CityTile) + x, y = obj.pos + "bw $x $y" +end + +""" + build_cart(obj::CityTile) :: String + +Returns the build cart action. When applied and requirements are met, a cart will be built at the `City`. +""" +function build_cart(obj::CityTile) + x, y = obj.pos + "bc $x $y" +end + +""" + Resource(r_type::AbstractString, amount::Integer) :: Resource + +""" +mutable struct Resource + r_type :: RESOURCES + amount :: Int +end + +""" + Cell(pos::Position, + resource::Union{Nothing, Resource} = nothing, + citytile::Union{Nothing, CityTile} = nothing, + road::Real = 0) +""" +mutable struct Cell + pos :: Position + resource :: Union{Nothing, Resource} + citytile :: Union{Nothing, CityTile} + road :: Float64 + Cell(pos::Position, + resource::Union{Nothing, Resource} = nothing, + citytile::Union{Nothing, CityTile} = nothing, + road::Real = 0) = + new(pos, resource, citytile, road) +end + +""" + has_resource(obj::Cell) :: Bool + +Returns true if this Cell has a non-depleted Resource, false otherwise. +""" +has_resource(obj::Cell) :: Bool = isa(obj.resource, Resource) && obj.resource.amount > 0 + +""" + GameMap(dim::Integer) + +The map is organized such that the top left corner of the map is at (0, 0) and the bottom right is at (width - 1, height - 1). The map is always square. +""" +struct GameMap + map :: Matrix{Cell} + function GameMap(dim::Integer) :: GameMap + new([ Cell(Position(i, j)) for i in 0:dim - 1, j in 0:dim - 1]) + end +end + +""" + get_cell_by_pos(obj::GameMap, pos::Position) :: Cell + +Returns the Cell at the given pos. +""" +get_cell_by_pos(obj::GameMap, pos::Position) :: Cell = get_cell(obj, pos.x, pos.y) + +""" + get_cell(obj::GameMap, x::Integer, y::Integer) :: Cell + +Returns the Cell at the given pos. +""" +get_cell(obj::GameMap, x::Integer, y::Integer) :: Cell = obj.map[x + 1, y + 1] + +""" + Cargo(;wood::Integer = 0, + coal::Integer = 0, + uranium::Integer = 0) :: Cargo +""" +struct Cargo + wood :: Int + coal :: Int + uranium :: Int + Cargo(;wood::Integer = 0, + coal::Integer = 0, + uranium::Integer = 0) = + new(wood, coal, uranium) +end + +""" + City(teamid::Integer, cityid::AbstractString, + fuel::Real, lightupkeep::Real, citytiles::AbstractVector{CityTile} = CityTile[]) :: City +""" +struct City + cityid :: String + team :: Int + fuel :: Float64 + lightupkeep :: Float64 + citytiles :: Vector{CityTile} + function City(teamid::Integer, cityid::AbstractString, + fuel::Real, lightupkeep::Real, + citytiles::AbstractVector{CityTile} = CityTile[]) + new(teamid, cityid, fuel, lightupkeep, citytiles) + end +end + +""" + get_light_upkeep(obj::City) :: Float64 + +Returns the light upkeep per turn of the City. Fuel in the City is subtracted by the light upkeep each turn of night. +""" +get_light_upkeep(obj::City) :: Float64 = obj.lightupkeep + +""" + Unit(id::String, team::Int, pos::Position, unit_type::UNITS, + cooldown::Real, cargo::Cargo) :: Unit +""" +struct Unit + pos :: Position + team :: Int + id :: String + cooldown :: Float64 + cargo :: Cargo + # Internal + unit_type :: UNITS + function Unit(id::String, team::Int, pos::Position, unit_type::UNITS, + cooldown::Real, cargo::Cargo) + new(pos, team, id, cooldown, cargo, unit_type) + end +end + +""" + can_act(obj::Union{CityTile, Unit}) :: Bool + +Whether this City or Unit can perform an action this turn, which is when the Cooldown is less than 1. +""" +can_act(obj::Union{CityTile, Unit}) :: Bool = obj.cooldown < 1 + +""" + get_cargo_space_left(obj::Unit, gameconstants::GameConstants = GAME_CONSTANTS) :: Int + +Returns the amount of space left in the cargo of this Unit. +Note that any Resource takes up the same space, e.g. 70 wood takes up as much space as 70 uranium, but 70 uranium would produce much more fuel than wood when deposited at a City. +""" +function get_cargo_space_left(obj::Unit, + gameconstants::GameConstants = GAME_CONSTANTS) :: Bool + space_used = obj.cargo.wood + obj.cargo.coal + obj.cargo.uranium + rc = gameconstants.RESOURCE_CAPACITY + space_capacity = obj.unit_type == worker ? rc.worker : rc.cart + space_capacity - space_used +end + +function can_build(obj::Unit, game_map::GameMap, + gameconstants::GameConstants = GAME_CONSTANTS) :: Bool + cell = get_cell_by_pos(game_map, obj.pos) + !has_resource(cell) && + can_act(obj) && + (obj.cargo.wood + obj.cargo.coal + obj.cargo.uranium) ≥ + gameconstants.CITY_BUILD_COST +end + +""" + move(obj::Unit, dir::DIRECTIONS) :: String + +Returns the move action. When applied, `Unit` will move in the specified direction by one `Unit`, provided there are no other units in the way or opposition cities. (Units can stack on top of each other however when over a friendly `City`). +""" +move(obj::Unit, dir::DIRECTIONS) :: String = "m $(obj.id) $(DIRECTIONS_OUTPUT[dir])" + +""" + transfer(obj::Unit, + dest_id::AbstractString, resourceType::RESOURCES, amount::Integer) :: String + +Returns the transfer action. Will transfer from this `Unit` the selected Resource type by the desired amount to the `Unit` with id dest_id given that both units are adjacent at the start of the turn. (This means that a destination Unit can receive a transfer of resources by another `Unit` but also move away from that Unit) +""" +transfer(obj::Unit, dest_id::AbstractString, resourceType::RESOURCES, amount::Integer) :: String = + "t $(obj.id) $dest_id $resourceType $amount" + +""" + build_city(obj::Unit) :: String + +Returns the build City action. When applied, Unit will try to build a City right under itself provided it is an empty tile with no City or resources and the worker is carrying 100 units of resources. All resources are consumed if the city is succesfully built. +""" +build_city(obj::Unit) :: String = "bcity $(obj.id)" + +""" + pillage(obj::Unit) :: String + +Returns the pillage action. When applied, `Unit` will pillage the tile it is currently on top of and remove 0.5 of the road level. +""" +pillage(obj::Unit) :: String = "p $(obj.id)" + +""" + Player(team::Integer, + research_points::Integer = 0, + units::Vector{Unit} = Unit[], + cities::Dict{String, City} = Dict{String, City}()) + +This contains information on a particular player of a particular team. +""" +mutable struct Player + team :: Int + research_points :: Int + units :: Vector{Unit} + cities :: Dict{String, City} + city_tile_count :: Int + Player(team::Integer, + research_points::Integer = 0, + units::Vector{Unit} = Unit[], + cities::Dict{String, City} = Dict{String, City}(), + city_tile_count::Integer = 0) = + new(team, research_points, units, cities, city_tile_count) +end + +""" + research_coal(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool + +Whether or not this player's team has researched coal and can mine coal. +""" +research_coal(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool = + obj.research_points ≥ gameconstants.RESEARCH_REQUIREMENTS.coal + +""" + researched_uranium(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool + +Whether or not this player's team has researched coal and can mine uranium. +""" +researched_uranium(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool = + obj.research_points ≥ gameconstants.RESEARCH_REQUIREMENTS.uranium diff --git a/kits/julia/simple/main.jl b/kits/julia/simple/main.jl new file mode 100644 index 00000000..6507bce2 --- /dev/null +++ b/kits/julia/simple/main.jl @@ -0,0 +1,79 @@ +include(joinpath("kits", "julia", "simple", "lux", "annotate.jl")) +include(joinpath("kits", "julia", "simple", "lux", "constants.jl")) +include(joinpath("kits", "julia", "simple", "lux", "game_objects.jl")) +include(joinpath("kits", "julia", "simple", "lux", "game.jl")) + +""" + Agent(observation, configuration) :: Vector{String} +""" +struct Agent(observation, configuration) + ### Do not edit ### + if observation["step"] == 0 + game_state = Game(observation["updates"][1:2]) + _update!(game_state, messages["updates"][3:end]) + game_state.id = observation.player + else + _update!(game_state, observation["updates"]) + end + actions = String[] + + player = game_state.players[observation.player] + opponent = game_state.players[(observation.player + 1) % 2] + width, height = size(game_state.map) + + resource_tiles = Cell[] + for y in width + for x in height + cell = get_cell(game_state.map, Position(x, y)) + if has_resource(cell) + push!(resource_tiles, cell) + end + end + end + + # we iterate over all our units and do something with them + for unit in player.units + if unit.unit_type == worker && can_act(unit) + closest_dist = Inf + closest_resource_tile = nothing + if get_cargo_space_left(unit) > 0 + # if the unit is a worker and we have space in cargo, lets find the nearest resource tile and try to mine it + for resource_tile in resource_tiles + resource_tile.resource.r_type == coal && !researched_coal(player) && continue + resource_tile.resource.r_type == uranium && !researched_uranium(player) && continue + dist = distance_to(resource_tile.pos, unit.pos) + if dist < closest_dist + closest_dist = dist + closest_resource_tile = resource_tile + end + end + if !ismissing(closest_resource_tile) + append!(actions, move(unit, direction_to(unit.pos, closest_resource_tile.pos))) + end + else + # if unit is a worker and there is no cargo space left, and we have cities, lets return to them + if length(player.cities) > 0 + closest_dist = Inf + closest_city_tile = nothing + for city in values(player.cities) + for city_tile in city.citytiles + dist = distance_to(city_tile.pos, unit.pos) + if dist < closest_dist + closest_dist = dist + closest_city_tile = city_tile + end + end + end + if !isnothing(closest_city_tile) + move_dir = direction_to(unit.pos, closest_city_tile.pos) + append!(actions, move(unit, move_dir)) + end + end + end + end + end + + # you can add debug annotations using the functions in the annotate object + # append!(actions, circle(0, 0)) + actions +end diff --git a/kits/julia/simple/main.py b/kits/julia/simple/main.py new file mode 100644 index 00000000..e83f8751 --- /dev/null +++ b/kits/julia/simple/main.py @@ -0,0 +1,68 @@ +from subprocess import Popen, PIPE +from threading import Thread +from queue import Queue, Empty + +import atexit +import os +import sys +agent_processes = [None, None] +t = None +q = None +def cleanup_process(): + global agent_processes + for proc in agent_processes: + if proc is not None: + proc.kill() +def enqueue_output(out, queue): + for line in iter(out.readline, b''): + queue.put(line) + out.close() +def julia_agent(observation, configuration): + """ + a wrapper around a Julia agent + """ + global agent_processes, t, q + + agent_process = agent_processes[observation.player] + ### Do not edit ### + if agent_process is None: + if "__raw_path__" in configuration: + cwd = os.path.dirname(configuration["__raw_path__"]) + else: + cwd = os.path.dirname(__file__) + agent_process = Popen(["julia", "main.jl"], stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) + agent_processes[observation.player] = agent_process + atexit.register(cleanup_process) + + # following 4 lines from https://stackoverflow.com/questions/375427/a-non-blocking-read-on-a-subprocess-pipe-in-python + q = Queue() + t = Thread(target=enqueue_output, args=(agent_process.stderr, q)) + t.daemon = True # thread dies with the program + t.start() + if observation.step == 0: + # fixes bug where updates array is shared, but the first update is agent dependent actually + observation["updates"][0] = f"{observation.player}" + + # print observations to agent + agent_process.stdin.write(("\n".join(observation["updates"]) + "\n").encode()) + agent_process.stdin.flush() + + # wait for data written to stdout + agent1res = (agent_process.stdout.readline()).decode() + _end_res = (agent_process.stdout.readline()).decode() + + while True: + try: line = q.get_nowait() + except Empty: + # no standard error received, break + break + else: + # standard error output received, print it out + print(line.decode(), file=sys.stderr, end='') + + outputs = agent1res.split("\n")[0].split(",") + actions = [] + for cmd in outputs: + if cmd != "": + actions.append(cmd) + return actions diff --git a/package_all_kits.sh b/package_all_kits.sh index 6f63488f..e8693212 100644 --- a/package_all_kits.sh +++ b/package_all_kits.sh @@ -8,4 +8,5 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" (cd $SCRIPT_DIR/kits/python/simple && tar -czvf simple.tar.gz *) (cd $SCRIPT_DIR/kits/java/simple && tar -czvf simple.tar.gz *) (cd $SCRIPT_DIR/kits/js/simple && tar -czvf simple.tar.gz *) -(cd $SCRIPT_DIR/kits/ts/simple && tar -czvf simple.tar.gz *) \ No newline at end of file +(cd $SCRIPT_DIR/kits/julia/simple && tar -czvf simple.tar.gz *) +(cd $SCRIPT_DIR/kits/ts/simple && tar -czvf simple.tar.gz *) diff --git a/tests/kit.spec.ts b/tests/kit.spec.ts index 513794a6..e6e0bdda 100644 --- a/tests/kit.spec.ts +++ b/tests/kit.spec.ts @@ -32,6 +32,10 @@ describe('Test kits', () => { file: './kits/java/simple/Bot.java', name: 'java', }, + julia: { + file: './kits/julia/simple/main.jl', + name: 'julia', + }, py: { file: './kits/python/simple/main.py', name: 'py', @@ -124,6 +128,21 @@ describe('Test kits', () => { verifyCommands(cmds1, cmds2); }).timeout(10000); + it('should run julia', async () => { + let botList = [bots.js, bots.julia]; + const match = await luxdim.createMatch(botList, options); + const res = await match.run(); + + botList = [bots.julia, bots.julia]; + const match2 = await luxdim.createMatch(botList, options); + const res2 = await match.run(); + const state: LuxMatchState = match.state; + const state2: LuxMatchState = match.state; + const cmds1 = state.game.replay.data.allCommands; + const cmds2 = state2.game.replay.data.allCommands; + verifyCommands(cmds1, cmds2); + }).timeout(10000); + after(async () => { await luxdim.cleanup(); }); diff --git a/tests/match.ts b/tests/match.ts index 43f80dbd..97d10265 100644 --- a/tests/match.ts +++ b/tests/match.ts @@ -21,6 +21,7 @@ const pySimple = './kits/python/simple/main.py'; const cppSimple = './kits/cpp/simple/main.cpp'; const cppOrganic = './tests/bots/cpporganic/main.cpp'; const javaSimple = './kits/java/simple/Bot.java'; +const juliaSimple = './kits/julia/simple/main.jl'; const cppSimpleTranspiled = './kits/cpp/simple/main.js'; const cppOrganicTranspiled = './kits/cpp/organic/main.js'; const botList = [ From 289216409ab257f0089b1daa600240ca7a204407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Bayo=C3=A1n=20Santiago=20Calder=C3=B3n?= Date: Sat, 9 Oct 2021 17:13:36 -0400 Subject: [PATCH 2/2] Almost ready Fixed some issues and moving to the testing. --- kits/julia/simple/lux/Project.toml | 9 ++ kits/julia/simple/lux/constants.jl | 63 ---------- kits/julia/simple/lux/game.jl | 108 ---------------- kits/julia/simple/lux/src/LuxAI.jl | 14 +++ kits/julia/simple/lux/{ => src}/annotate.jl | 5 - kits/julia/simple/lux/src/constants.jl | 15 +++ kits/julia/simple/lux/src/game.jl | 118 ++++++++++++++++++ kits/julia/simple/lux/src/game_constants.json | 68 ++++++++++ .../simple/lux/{ => src}/game_objects.jl | 107 +++++++--------- kits/julia/simple/main.py | 109 +++++++--------- 10 files changed, 314 insertions(+), 302 deletions(-) create mode 100644 kits/julia/simple/lux/Project.toml delete mode 100644 kits/julia/simple/lux/constants.jl delete mode 100644 kits/julia/simple/lux/game.jl create mode 100644 kits/julia/simple/lux/src/LuxAI.jl rename kits/julia/simple/lux/{ => src}/annotate.jl (99%) create mode 100644 kits/julia/simple/lux/src/constants.jl create mode 100644 kits/julia/simple/lux/src/game.jl create mode 100644 kits/julia/simple/lux/src/game_constants.json rename kits/julia/simple/lux/{ => src}/game_objects.jl (71%) diff --git a/kits/julia/simple/lux/Project.toml b/kits/julia/simple/lux/Project.toml new file mode 100644 index 00000000..5870dc8c --- /dev/null +++ b/kits/julia/simple/lux/Project.toml @@ -0,0 +1,9 @@ +name = "LuxAI" +uuid = "2dcf1b2f-c367-41fa-9531-ffe19c618c81" +authors = ["José Bayoán Santiago Calderón "] +version = "0.1.0" + +[deps] +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" diff --git a/kits/julia/simple/lux/constants.jl b/kits/julia/simple/lux/constants.jl deleted file mode 100644 index ff34cb72..00000000 --- a/kits/julia/simple/lux/constants.jl +++ /dev/null @@ -1,63 +0,0 @@ -""" - GameConstants(;DAY_LENGTH = 30, NIGHT_LENGTH = 10, MAX_DAYS = 360, - LIGHT_UPKEEP = (CITY = 23, WORKER = 4, CART = 10), - WOOD_GROWTH_RATE = 1.025, MAX_WOOD_AMOUNT = 500, - CITY_BUILD_COST = 100, CITY_ADJACENCY_BONUS = 5, - RESOURCE_CAPACITY = (WORKER = 100, CART = 2000), - WORKER_COLLECTION_RATE = (WOOD = 20, COAL = 5, URANIUM = 2), - RESOURCE_TO_FUEL_RATE = (WOOD = 1, COAL = 10, URANIUM = 40), - RESEARCH_REQUIREMENTS = (COAL = 50, URANIUM = 200), - CITY_ACTION_COOLDOWN = 10, - UNIT_ACTION_COOLDOWN = (WORKER = 2, CART = 3), - MAX_ROAD = 6, MIN_ROAD = 0, - CART_ROAD_DEVELOPMENT_RATE = 0.75, - PILLAGE_RATE = 0.5) - -This will contain constants on all game parameters like the max turns, the light upkeep of CityTiles etc. -If there are any crucial changes to the starter kits, typically only this object will change. -""" -struct GameConstants - DAY_LENGTH :: Int - NIGHT_LENGTH :: Int - MAX_DAYS :: Int - LIGHT_UPKEEP :: NamedTuple{(:CITY, :WORKER, :CART), NTuple{3, Int}} - WOOD_GROWTH_RATE :: Float64 - MAX_WOOD_AMOUNT :: Int - CITY_BUILD_COST :: Int - CITY_ADJACENCY_BONUS :: Int - RESOURCE_CAPACITY :: NamedTuple{(:WORKER, :CART), NTuple{2, Int}} - WORKER_COLLECTION_RATE :: NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}} - RESOURCE_TO_FUEL_RATE :: NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}} - RESEARCH_REQUIREMENTS :: NamedTuple{(:COAL, :URANIUM), NTuple{2, Int}} - CITY_ACTION_COOLDOWN :: Int - UNIT_ACTION_COOLDOWN :: NamedTuple{(:WORKER, :CART), NTuple{2, Int}} - MAX_ROAD :: Int - MIN_ROAD :: Int - CART_ROAD_DEVELOPMENT_RATE :: Float64 - PILLAGE_RATE :: Float64 - function GameConstants(;DAY_LENGTH = 30, - NIGHT_LENGTH = 10, - MAX_DAYS = 360, - LIGHT_UPKEEP = (CITY = 23, WORKER = 4, CART = 10), - WOOD_GROWTH_RATE = 1.025, - MAX_WOOD_AMOUNT = 500, - CITY_BUILD_COST = 100, - CITY_ADJACENCY_BONUS = 5, - RESOURCE_CAPACITY = (WORKER = 100, CART = 2000), - WORKER_COLLECTION_RATE = (WOOD = 20, COAL = 5, URANIUM = 2), - RESOURCE_TO_FUEL_RATE = (WOOD = 1, COAL = 10, URANIUM = 40), - RESEARCH_REQUIREMENTS = (COAL = 50, URANIUM = 200), - CITY_ACTION_COOLDOWN = 10, - UNIT_ACTION_COOLDOWN = (WORKER = 2, CART = 3), - MAX_ROAD = 6, - MIN_ROAD = 0, - CART_ROAD_DEVELOPMENT_RATE = 0.75, - PILLAGE_RATE = 0.5) - new(DAY_LENGTH, NIGHT_LENGTH, MAX_DAYS, LIGHT_UPKEEP, WOOD_GROWTH_RATE, MAX_WOOD_AMOUNT, - CITY_BUILD_COST, CITY_ADJACENCY_BONUS, RESOURCE_CAPACITY, WORKER_COLLECTION_RATE, - RESOURCE_TO_FUEL_RATE, RESEARCH_REQUIREMENTS, CITY_ACTION_COOLDOWN, - UNIT_ACTION_COOLDOWN, MAX_ROAD, MIN_ROAD, CART_ROAD_DEVELOPMENT_RATE, PILLAGE_RATE) - end -end - -const GAME_CONSTANTS = GameConstants() diff --git a/kits/julia/simple/lux/game.jl b/kits/julia/simple/lux/game.jl deleted file mode 100644 index 33084ea0..00000000 --- a/kits/julia/simple/lux/game.jl +++ /dev/null @@ -1,108 +0,0 @@ -const INPUT_CONSTANTS = Dict("RESEARCH_POINTS" => "rp", - "RESOURCES" => "r", - "UNITS" => "u", - "CITY" => "c", - "CITY_TILES" => "ct", - "ROADS" => "ccd", - "DONE" => "D_DONE") -const DIRECTIONS_OUTPUT = Dict(north => "n", - west => "w", - south => "s", - east => "e", - center => "c") - -mutable struct Game - id :: Int - turn :: Int - map :: GameMap - players :: NTuple{2, Player} - function Game(messages::AbstractVector{<:AbstractString}) - id = parse(Int, messages[1]) - turn = -1 - # get some other necessary initial input - mapinfo = split(messages[2], " ") - dim = parse(Int, mapinfo[1]) - game_map = GameMap(dim) - players = (Player(0), Player(1)) - new(id, turn, game_map, players) - end -end - -_end_turn(::Game) = print("D_FINISH") - -function _reset_player_states!(obj::Game) - for player in obj.players - empty!(player.units) - empty!(player.cities) - end - nothing -end - -""" - _update!(obj::Game, messages) - -Update state. -""" -function _update!(obj::Game, messages::AbstractVector{<:AbstractString}) - obj.map .= GameMap(size(obj.map, 1)) - obj.turn += 1 - _reset_player_states!(obj) - for update in messages - if update == "D_DONE" - break - end - strs = split(update, " ") - input_identifier = strs[1] - if input_identifier == INPUT_CONSTANTS["RESEARCH_POINTS"] - team = parse(Int, strs[2]) - obj.players[team + 1].research_points = parse(Int, strs[3]) - elseif input_identifier == INPUT_CONSTANTS["RESOURCES"] - r_type = strs[2] - x = parse(Int, strs[3]) - y = parse(Int, strs[4]) - amt = parse(Float64, strs[5]) - pos = Position(x, y) - resource = Resource(r_type, amt) - obj.map[x + 1, y + 1] = Cell(pos, resource) - elseif input_identifier == INPUT_CONSTANTS["UNITS"] - unittype = parse(Int, strs[2]) - team = parse(Int, strs[3]) - unitid = strs[4] - x = parse(Int, strs[5]) - y = parse(Int, strs[6]) - pos = Position(x, y) - cooldown = parse(Float64, strs[7]) - wood = parse(Int, strs[8]) - coal = parse(Int, strs[9]) - uranium = parse(Int, strs[10]) - cargo = Cargo(wood, coal, uranium) - append!(obj.players[team + 1].units, Unit(team, unittype, unitid, pos, cooldown, cargo)) - elseif input_identifier == INPUT_CONSTANTS["CITY"] - team = parse(Int, strs[2]) - cityid = strs[3] - fuel = parse(Float64, strs[4]) - lightupkeep = parse(Float64, strs[5]) - obj.players[team + 1].cities[cityid] = City(team, cityid, fuel, lightupkeep) - elseif input_identifier == INPUT_CONSTANTS["CITY_TILES"] - team = parse(Int, strs[2]) - cityid = strs[3] - x = parse(Int, strs[4]) - y = parse(Int, strs[5]) - pos = Position(x, y) - cooldown = parse(Float64, strs[6]) - city = obj.players[team + 1].cities[cityid] - citytile = CityTile(cityid, team, pos, cooldown) - append!(city.citytiles, citytile) - cell = get_cell(obj.map, pos) - cell.citytile = citytile - obj.players[team + 1].city_tile_count += 1 - elseif input_identifier == INPUT_CONSTANTS["ROADS"] - x = parse(Int, strs[2]) - y = parse(Int, strs[3]) - pos = Position(x, y) - road = parse(Float64, strs[4]) - cell = get_cell(obj.map, pos) - cell.road = road - end - end -end diff --git a/kits/julia/simple/lux/src/LuxAI.jl b/kits/julia/simple/lux/src/LuxAI.jl new file mode 100644 index 00000000..1302b975 --- /dev/null +++ b/kits/julia/simple/lux/src/LuxAI.jl @@ -0,0 +1,14 @@ +module LuxAI + +using JSON3: JSON3, StructType, Struct + +for file in ["annotate", "constants", "game_objects", "game"] + include(joinpath("lux", "$file.jl")) +end + +# function julia_main()::Cint +# # do something based on ARGS? +# return 0 # if things finished successfully +# end + +end diff --git a/kits/julia/simple/lux/annotate.jl b/kits/julia/simple/lux/src/annotate.jl similarity index 99% rename from kits/julia/simple/lux/annotate.jl rename to kits/julia/simple/lux/src/annotate.jl index 08eb0afb..d999b46e 100644 --- a/kits/julia/simple/lux/annotate.jl +++ b/kits/julia/simple/lux/src/annotate.jl @@ -5,7 +5,6 @@ Returns the draw circle annotation action. Will draw a unit sized circle on the visualizer at the current turn centered at the `Cell` at the given x, y coordinates. """ circle(x::Integer, y::Integer) = "dc $x $y" - """ x(x::Integer, y::Integer) :: String @@ -13,8 +12,6 @@ Returns the draw X annotation action. Will draw a unit sized X on the visualizer at the current turn centered at the `Cell` at the given x, y coordinates. """ x(x::Integer, y::Integer) = "dx $x $y" - - """ line(x1::Integer, y1::Integer, x2::Integer, y2::Integer) :: String @@ -22,7 +19,6 @@ Returns the draw line annotation action. Will draw a line from the center of the `Cell` at (x1, y1) to the center of the `Cell` at (x2, y2). """ line(x1::Integer, y1::Integer, x2::Integer, y2::Integer) = "dl $x1 $y1 $x2 $y2" - """ text(x::Integer, y::Integer, message::AbstractString, fontsize::Integer = 16) :: String @@ -32,7 +28,6 @@ Will write text on top of the tile at (x, y) with the particular message and fon """ text(x::Integer, y::Integer, message::AbstractString, fontsize::Integer = 16) = "dt $x $y '$message' $fontsize" - """ sidetext(message::AbstractString) :: String diff --git a/kits/julia/simple/lux/src/constants.jl b/kits/julia/simple/lux/src/constants.jl new file mode 100644 index 00000000..165ccd63 --- /dev/null +++ b/kits/julia/simple/lux/src/constants.jl @@ -0,0 +1,15 @@ +""" + GameConstants(jsonfile::AbstractString = joinpath(pkgdir(LuxAI), "src", "lux", "game_constants.json")) + +This will contain constants on all game parameters like the max turns, the light upkeep of CityTiles etc. +If there are any crucial changes to the starter kits, typically only this object will change. +""" +struct GameConstants + UNIT_TYPES :: NamedTuple{(:WORKER, :CART), NTuple{2, Int8}} + RESOURCE_TYPES :: NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, String}} + INPUTS :: NamedTuple{(:RESEARCH_POINTS, :RESOURCES, :UNITS, :CITY, :CITY_TILES, :ROADS, :DONE), NTuple{7, String}} + DIRECTIONS :: NamedTuple{(:NORTH, :WEST, :EAST, :SOUTH, :CENTER), NTuple{5, String}} + PARAMETERS :: NamedTuple{(:DAY_LENGTH, :NIGHT_LENGTH, :MAX_DAYS, :LIGHT_UPKEEP, :WOOD_GROWTH_RATE, :MAX_WOOD_AMOUNT, :CITY_BUILD_COST, :CITY_ADJACENCY_BONUS, :RESOURCE_CAPACITY, :WORKER_COLLECTION_RATE, :RESOURCE_TO_FUEL_RATE, :RESEARCH_REQUIREMENTS, :CITY_ACTION_COOLDOWN, :UNIT_ACTION_COOLDOWN, :MAX_ROAD, :MIN_ROAD, :CART_ROAD_DEVELOPMENT_RATE, :PILLAGE_RATE), Tuple{Int, Int, Int, NamedTuple{(:CITY, :WORKER, :CART), NTuple{3, Int}}, Float64, Float64, Int, Int, NamedTuple{(:WORKER, :CART), NTuple{2, Int}}, NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}}, NamedTuple{(:WOOD, :COAL, :URANIUM), NTuple{3, Int}}, NamedTuple{(:COAL, :URANIUM), NTuple{2, Int}}, Int, NamedTuple{(:WORKER, :CART), NTuple{2, Int}}, Int, Int, Float64, Float64}} +end + +StructType(::Type{GameConstants}) = Struct() diff --git a/kits/julia/simple/lux/src/game.jl b/kits/julia/simple/lux/src/game.jl new file mode 100644 index 00000000..4cb5e5f8 --- /dev/null +++ b/kits/julia/simple/lux/src/game.jl @@ -0,0 +1,118 @@ +""" + Game(messages::AbstractDict, + configuration::GameConstants = JSON3.read(read(joinpath(pkgdir(LuxAI), "src", "lux", "game_constants.json"), String), + GameConstants)) + +Struct for the state of the game. +""" +struct Game + id :: Int + turn :: Ref{Int} + map :: GameMap + players :: NTuple{2, Player} + configuration :: GameConstants + function Game(observations::AbstractDict, + configuration::GameConstants = JSON3.read(read(joinpath(pkgdir(LuxAI), "src", "lux", "game_constants.json"), String), GameConstants)) + updates = observations["updates"] + id = parse(Int, updates[1]) + turn = Ref(observations["step"]) + # get some other necessary initial input + game_map = GameMap(parse(Int, match.(r"\d+", updates[2]).match)) + players = (Player(0), Player(1)) + new(id, turn, game_map, players, configuration) + end +end +""" + _end_turn(::Game) :: Nothing + +Prints the signal for end of turn. +""" +_end_turn(::Game) = print("D_FINISH") +""" + _reset_player_states!(obj::Game) :: Nothing + +Resets the state of the players after finishing their turn. +""" +function _reset_player_states!(obj::Game) + for player in obj.players + empty!(player.units) + empty!(player.cities) + end + nothing +end +""" + _update!(obj::Game, messages) + +Update state of the game. +""" +function _update!(obj::Game, observation::AbstractVector{<:AbstractString}) + for col in axes(obj.map.map, 2) + for row in axes(obj.map.map, 1) + obj.map.map[row, col] = Cell(Position(row - 1, col - 1)) + end + end + obj.turn.x += 1 + _reset_player_states!(obj) + for update in observation + if update == "D_DONE" + break + end + observation = observation["updates"] + update = observation[end - 8] + strs = split(update, " ") + input_identifier = strs[1] + if input_identifier == obj.configuration.INPUTS.RESEARCH_POINTS + team = parse(Int, strs[2]) + obj.players[team + 1].research_points = parse(Int, strs[3]) + elseif input_identifier == obj.configuration.INPUTS.RESOURCES + type = strs[2] + x = parse(Int, strs[3]) + y = parse(Int, strs[4]) + amt = parse(Float64, strs[5]) + pos = Position(x, y) + resource = Resource(type, amt) + obj.map.map[x + 1, y + 1] = Cell(pos, resource) + elseif input_identifier == obj.configuration.INPUTS.UNITS + unittype = parse(Int, strs[2]) + unittype = findfirst(isequal(unittype), values(obj.configuration.UNIT_TYPES)) + unittype = string(keys(obj.configuration.UNIT_TYPES)[unittype]) + team = parse(Int, strs[3]) + unitid = strs[4] + x = parse(Int, strs[5]) + y = parse(Int, strs[6]) + pos = Position(x, y) + cooldown = parse(Float64, strs[7]) + wood = parse(Int, strs[8]) + coal = parse(Int, strs[9]) + uranium = parse(Int, strs[10]) + cargo = Cargo(;wood, coal, uranium) + push!(obj.players[team + 1].units, Unit(unitid, team, pos, unittype, cooldown, cargo)) + elseif input_identifier == obj.configuration.INPUTS.CITY + team = parse(Int, strs[2]) + cityid = strs[3] + fuel = parse(Float64, strs[4]) + lightupkeep = parse(Float64, strs[5]) + obj.players[team + 1].cities[cityid] = City(cityid, team, fuel, lightupkeep) + elseif input_identifier == obj.configuration.INPUTS.CITY_TILES + team = parse(Int, strs[2]) + cityid = strs[3] + x = parse(Int, strs[4]) + y = parse(Int, strs[5]) + pos = Position(x, y) + cooldown = parse(Float64, strs[6]) + city = obj.players[team + 1].cities[cityid] + citytile = CityTile(cityid, team, pos, cooldown) + push!(city.citytiles, citytile) + cell = get_cell_by_pos(obj.map, pos) + cell.citytile = citytile + obj.players[team + 1].city_tile_count += 1 + elseif input_identifier == obj.configuration.INPUTS.ROADS + x = parse(Int, strs[2]) + y = parse(Int, strs[3]) + pos = Position(x, y) + road = parse(Float64, strs[4]) + cell = get_cell_by_pos(obj.map, pos) + cell.road = road + end + end +end diff --git a/kits/julia/simple/lux/src/game_constants.json b/kits/julia/simple/lux/src/game_constants.json new file mode 100644 index 00000000..cd6a026f --- /dev/null +++ b/kits/julia/simple/lux/src/game_constants.json @@ -0,0 +1,68 @@ +{ + "UNIT_TYPES": { + "WORKER": 0, + "CART": 1 + }, + "RESOURCE_TYPES": { + "WOOD": "wood", + "COAL": "coal", + "URANIUM": "uranium" + }, + "INPUTS": { + "RESEARCH_POINTS": "rp", + "RESOURCES": "r", + "UNITS": "u", + "CITY": "c", + "CITY_TILES": "ct", + "ROADS": "ccd", + "DONE": "D_DONE" + }, + "DIRECTIONS": { + "NORTH": "n", + "WEST": "w", + "EAST": "e", + "SOUTH": "s", + "CENTER": "c" + }, + "PARAMETERS": { + "DAY_LENGTH": 30, + "NIGHT_LENGTH": 10, + "MAX_DAYS": 360, + "LIGHT_UPKEEP": { + "CITY": 23, + "WORKER": 4, + "CART": 10 + }, + "WOOD_GROWTH_RATE": 1.025, + "MAX_WOOD_AMOUNT": 500, + "CITY_BUILD_COST": 100, + "CITY_ADJACENCY_BONUS": 5, + "RESOURCE_CAPACITY": { + "WORKER": 100, + "CART": 2000 + }, + "WORKER_COLLECTION_RATE": { + "WOOD": 20, + "COAL": 5, + "URANIUM": 2 + }, + "RESOURCE_TO_FUEL_RATE": { + "WOOD": 1, + "COAL": 10, + "URANIUM": 40 + }, + "RESEARCH_REQUIREMENTS": { + "COAL": 50, + "URANIUM": 200 + }, + "CITY_ACTION_COOLDOWN": 10, + "UNIT_ACTION_COOLDOWN": { + "CART": 3, + "WORKER": 2 + }, + "MAX_ROAD": 6, + "MIN_ROAD": 0, + "CART_ROAD_DEVELOPMENT_RATE": 0.75, + "PILLAGE_RATE": 0.5 + } +} diff --git a/kits/julia/simple/lux/game_objects.jl b/kits/julia/simple/lux/src/game_objects.jl similarity index 71% rename from kits/julia/simple/lux/game_objects.jl rename to kits/julia/simple/lux/src/game_objects.jl index 892bfa6c..984e8969 100644 --- a/kits/julia/simple/lux/game_objects.jl +++ b/kits/julia/simple/lux/src/game_objects.jl @@ -1,40 +1,35 @@ -@enum RESOURCES wood coal uranium -@enum DIRECTIONS north west south east center -@enum UNITS worker cart - +@enum Directions north south west east center """ Position(x::Integer, y::Integer) :: Position +Position on the map. Starts with (0, 0) for the top left corner. """ struct Position x :: Int y :: Int end - """ is_adjacent(obj::Position, pos::Position) :: Bool -Returns true if this `Position` is adjacent to pos. False otherwise. +Returns true if this Position is adjacent to pos. False otherwise. """ function is_adjacent(obj::Position, pos::Position) Δx = obj.x - pos.x Δy = obj.y - pos.y Δx^2 + Δy^2 ≤ 2 end - """ - is_adjacent(obj::Position, pos::Position) :: Bool + equals(obj::Position, pos::Position) :: Bool -Returns true if this `Position` is equal to the other pos object by checking x, y coordinates. False otherwise. +Returns true if this Position is equal to the other pos object by checking x, y coordinates. False otherwise. """ -equals(obj::Position, pos::Position) = obj.x == pos.x && obj.y == pos.y - +equals(obj::Position, pos::Position) :: Bool = obj.x == pos.x && obj.y == pos.y """ - translate(obj::Position, direction::DIRECTIONS, units::Integer) :: Position + translate(obj::Position, direction::Directions, units::Integer) :: Position -Returns the `Position` equal to going in a direction units number of times from this Position. +Returns the Position equal to going in a direction units number of times from this Position. """ -function translate(obj::Position, direction::DIRECTIONS, units::Integer) +function translate(obj::Position, direction::Directions, units::Integer) x = obj.x y = obj.y if direction == north @@ -49,16 +44,14 @@ function translate(obj::Position, direction::DIRECTIONS, units::Integer) end Position(x, y) end - """ distance_to(obj::Position, pos::Position) :: Float64 Returns the Manhattan (rectilinear) distance from this Position to pos, """ distance_to(obj::Position, pos::Position) = abs(obj.x - pos.x) + abs(obj.y - pos.y) - """ - direction_to(obj::Position, target_pos::Position) :: DIRECTIONS + direction_to(obj::Position, target_pos::Position) :: Directions Returns the direction that would move you closest to target_pos from this Position if you took a single step. In particular, will return DIRECTIONS.CENTER if this Position is equal to the target_pos. Note that this does not check for potential collisions with other units but serves as a basic pathfinding method. """ @@ -73,7 +66,6 @@ function direction_to(obj::Position, target_pos::Position) obj.y < target_pos.y ? south : north end end - """ CityTile(cityid::AbstractString, team::Integer, pos::Position, cooldown::Real) :: CityTile @@ -84,7 +76,6 @@ struct CityTile pos :: Position cooldown :: Float64 end - """ research(obj::CityTile) :: String @@ -94,41 +85,38 @@ function research(obj::CityTile) x, y = obj.pos "r $x $y" end - """ build_worker(obj::CityTile) :: String -Returns the build worker action. When applied and requirements are met, a worker will be built at the `City`. +Returns the build worker action. When applied and requirements are met, a worker will be built at the City. """ function build_worker(obj::CityTile) x, y = obj.pos "bw $x $y" end - """ build_cart(obj::CityTile) :: String -Returns the build cart action. When applied and requirements are met, a cart will be built at the `City`. +Returns the build cart action. When applied and requirements are met, a cart will be built at the City. """ function build_cart(obj::CityTile) x, y = obj.pos "bc $x $y" end - """ - Resource(r_type::AbstractString, amount::Integer) :: Resource + Resource(type::AbstractString, amount::Integer) :: Resource """ mutable struct Resource - r_type :: RESOURCES + type :: String amount :: Int end - """ Cell(pos::Position, resource::Union{Nothing, Resource} = nothing, citytile::Union{Nothing, CityTile} = nothing, road::Real = 0) + """ mutable struct Cell pos :: Position @@ -141,14 +129,12 @@ mutable struct Cell road::Real = 0) = new(pos, resource, citytile, road) end - """ has_resource(obj::Cell) :: Bool Returns true if this Cell has a non-depleted Resource, false otherwise. """ has_resource(obj::Cell) :: Bool = isa(obj.resource, Resource) && obj.resource.amount > 0 - """ GameMap(dim::Integer) @@ -160,21 +146,18 @@ struct GameMap new([ Cell(Position(i, j)) for i in 0:dim - 1, j in 0:dim - 1]) end end - """ get_cell_by_pos(obj::GameMap, pos::Position) :: Cell Returns the Cell at the given pos. """ get_cell_by_pos(obj::GameMap, pos::Position) :: Cell = get_cell(obj, pos.x, pos.y) - """ get_cell(obj::GameMap, x::Integer, y::Integer) :: Cell Returns the Cell at the given pos. """ get_cell(obj::GameMap, x::Integer, y::Integer) :: Cell = obj.map[x + 1, y + 1] - """ Cargo(;wood::Integer = 0, coal::Integer = 0, @@ -189,10 +172,10 @@ struct Cargo uranium::Integer = 0) = new(wood, coal, uranium) end - """ - City(teamid::Integer, cityid::AbstractString, + City(cityid::AbstractString, teamid::Integer, fuel::Real, lightupkeep::Real, citytiles::AbstractVector{CityTile} = CityTile[]) :: City + """ struct City cityid :: String @@ -200,45 +183,42 @@ struct City fuel :: Float64 lightupkeep :: Float64 citytiles :: Vector{CityTile} - function City(teamid::Integer, cityid::AbstractString, + function City(cityid::AbstractString, teamid::Integer, fuel::Real, lightupkeep::Real, citytiles::AbstractVector{CityTile} = CityTile[]) - new(teamid, cityid, fuel, lightupkeep, citytiles) + new(cityid, teamid, fuel, lightupkeep, citytiles) end end - """ get_light_upkeep(obj::City) :: Float64 Returns the light upkeep per turn of the City. Fuel in the City is subtracted by the light upkeep each turn of night. """ get_light_upkeep(obj::City) :: Float64 = obj.lightupkeep - """ - Unit(id::String, team::Int, pos::Position, unit_type::UNITS, + Unit(id::AbstractString, team::Int, pos::Position, unit_type::AbstractString, cooldown::Real, cargo::Cargo) :: Unit + """ struct Unit + id :: String pos :: Position team :: Int - id :: String cooldown :: Float64 cargo :: Cargo # Internal - unit_type :: UNITS - function Unit(id::String, team::Int, pos::Position, unit_type::UNITS, + unit_type :: String + function Unit(id::AbstractString, team::Int, pos::Position, unit_type::AbstractString, cooldown::Real, cargo::Cargo) - new(pos, team, id, cooldown, cargo, unit_type) + new(id, pos, team, cooldown, cargo, unit_type) end end - """ can_act(obj::Union{CityTile, Unit}) :: Bool Whether this City or Unit can perform an action this turn, which is when the Cooldown is less than 1. """ can_act(obj::Union{CityTile, Unit}) :: Bool = obj.cooldown < 1 - """ get_cargo_space_left(obj::Unit, gameconstants::GameConstants = GAME_CONSTANTS) :: Int @@ -248,50 +228,51 @@ Note that any Resource takes up the same space, e.g. 70 wood takes up as much sp function get_cargo_space_left(obj::Unit, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool space_used = obj.cargo.wood + obj.cargo.coal + obj.cargo.uranium - rc = gameconstants.RESOURCE_CAPACITY - space_capacity = obj.unit_type == worker ? rc.worker : rc.cart + rc = gameconstants.PARAMETERS.RESOURCE_CAPACITY + space_capacity = rc[obj.unit_type] space_capacity - space_used end +""" + can_build(obj::Unit, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool +Returns true if the Unit can build a City on the tile it is on now. False otherwise. +Checks that the tile does not have a Resource over it still and the Unit has a Cooldown of less than 1. +""" function can_build(obj::Unit, game_map::GameMap, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool cell = get_cell_by_pos(game_map, obj.pos) !has_resource(cell) && can_act(obj) && (obj.cargo.wood + obj.cargo.coal + obj.cargo.uranium) ≥ - gameconstants.CITY_BUILD_COST + gameconstants.PARAMETERS.CITY_BUILD_COST end - """ - move(obj::Unit, dir::DIRECTIONS) :: String + move(obj::Unit, dir::Directions, gameconstants::GameConstants = game.configuration) :: String -Returns the move action. When applied, `Unit` will move in the specified direction by one `Unit`, provided there are no other units in the way or opposition cities. (Units can stack on top of each other however when over a friendly `City`). +Returns the move action. When applied, Unit will move in the specified direction by one Unit, provided there are no other units in the way or opposition cities. (Units can stack on top of each other however when over a friendly City). """ -move(obj::Unit, dir::DIRECTIONS) :: String = "m $(obj.id) $(DIRECTIONS_OUTPUT[dir])" - +move(obj::Unit, dir::Directions, gameconstants::GameConstants = game.configuration) :: String = + "m $(obj.id) $(gameconstants[string(dir)])" """ transfer(obj::Unit, - dest_id::AbstractString, resourceType::RESOURCES, amount::Integer) :: String + dest_id::AbstractString, resourceType::AbstractString, amount::Integer) :: String -Returns the transfer action. Will transfer from this `Unit` the selected Resource type by the desired amount to the `Unit` with id dest_id given that both units are adjacent at the start of the turn. (This means that a destination Unit can receive a transfer of resources by another `Unit` but also move away from that Unit) +Returns the transfer action. Will transfer from this Unit the selected Resource type by the desired amount to the Unit with id dest_id given that both units are adjacent at the start of the turn. (This means that a destination Unit can receive a transfer of resources by another Unit but also move away from that Unit) """ -transfer(obj::Unit, dest_id::AbstractString, resourceType::RESOURCES, amount::Integer) :: String = +transfer(obj::Unit, dest_id::AbstractString, resourceType::AbstractString, amount::Integer) :: String = "t $(obj.id) $dest_id $resourceType $amount" - """ build_city(obj::Unit) :: String Returns the build City action. When applied, Unit will try to build a City right under itself provided it is an empty tile with no City or resources and the worker is carrying 100 units of resources. All resources are consumed if the city is succesfully built. """ build_city(obj::Unit) :: String = "bcity $(obj.id)" - """ pillage(obj::Unit) :: String -Returns the pillage action. When applied, `Unit` will pillage the tile it is currently on top of and remove 0.5 of the road level. +Returns the pillage action. When applied, Unit will pillage the tile it is currently on top of and remove 0.5 of the road level. """ pillage(obj::Unit) :: String = "p $(obj.id)" - """ Player(team::Integer, research_points::Integer = 0, @@ -313,19 +294,17 @@ mutable struct Player city_tile_count::Integer = 0) = new(team, research_points, units, cities, city_tile_count) end - """ research_coal(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool Whether or not this player's team has researched coal and can mine coal. """ research_coal(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool = - obj.research_points ≥ gameconstants.RESEARCH_REQUIREMENTS.coal - + obj.research_points ≥ gameconstants.PARAMETERS.RESEARCH_REQUIREMENTS.coal """ researched_uranium(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool Whether or not this player's team has researched coal and can mine uranium. """ researched_uranium(obj::Player, gameconstants::GameConstants = GAME_CONSTANTS) :: Bool = - obj.research_points ≥ gameconstants.RESEARCH_REQUIREMENTS.uranium + obj.research_points ≥ gameconstants.PARAMETERS.RESEARCH_REQUIREMENTS.uranium diff --git a/kits/julia/simple/main.py b/kits/julia/simple/main.py index e83f8751..31469856 100644 --- a/kits/julia/simple/main.py +++ b/kits/julia/simple/main.py @@ -1,68 +1,53 @@ -from subprocess import Popen, PIPE -from threading import Thread -from queue import Queue, Empty - -import atexit -import os import sys -agent_processes = [None, None] -t = None -q = None -def cleanup_process(): - global agent_processes - for proc in agent_processes: - if proc is not None: - proc.kill() -def enqueue_output(out, queue): - for line in iter(out.readline, b''): - queue.put(line) - out.close() -def julia_agent(observation, configuration): - """ - a wrapper around a Julia agent - """ - global agent_processes, t, q +from agent import agent - agent_process = agent_processes[observation.player] - ### Do not edit ### - if agent_process is None: - if "__raw_path__" in configuration: - cwd = os.path.dirname(configuration["__raw_path__"]) - else: - cwd = os.path.dirname(__file__) - agent_process = Popen(["julia", "main.jl"], stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) - agent_processes[observation.player] = agent_process - atexit.register(cleanup_process) +import julia +julia.install() - # following 4 lines from https://stackoverflow.com/questions/375427/a-non-blocking-read-on-a-subprocess-pipe-in-python - q = Queue() - t = Thread(target=enqueue_output, args=(agent_process.stderr, q)) - t.daemon = True # thread dies with the program - t.start() - if observation.step == 0: - # fixes bug where updates array is shared, but the first update is agent dependent actually - observation["updates"][0] = f"{observation.player}" - - # print observations to agent - agent_process.stdin.write(("\n".join(observation["updates"]) + "\n").encode()) - agent_process.stdin.flush() +from julia.api import Julia +jl = Julia(compiled_modules=False) - # wait for data written to stdout - agent1res = (agent_process.stdout.readline()).decode() - _end_res = (agent_process.stdout.readline()).decode() +from julia import Base +from julia import Main - while True: - try: line = q.get_nowait() - except Empty: - # no standard error received, break - break - else: - # standard error output received, print it out - print(line.decode(), file=sys.stderr, end='') +from julia import Pkg +Pkg.add(url = "./lux") # Repo with agent implementation - outputs = agent1res.split("\n")[0].split(",") - actions = [] - for cmd in outputs: - if cmd != "": - actions.append(cmd) - return actions +Main.eval("using LuxAI") +from julia import LuxAI + +if __name__ == "__main__": + + def read_input(): + """ + Reads input from stdin + """ + try: + return input() + except EOFError as eof: + raise SystemExit(eof) + step = 0 + class Observation(Dict[str, any]): + def __init__(self, player=0) -> None: + self.player = player + # self.updates = [] + # self.step = 0 + observation = Observation() + observation["updates"] = [] + observation["step"] = 0 + player_id = 0 + while True: + inputs = read_input() + observation["updates"].append(inputs) + + if step == 0: + player_id = int(observation["updates"][0]) + observation.player = player_id + agent = LuxAI.agent(observation) + if inputs == "D_DONE": + actions = agent.update(observation) + observation["updates"] = [] + step += 1 + observation["step"] = step + print(",".join(actions)) + print("D_FINISH")