From 3e00506459ad3e7a837fa44376afe9ca64b53d7f Mon Sep 17 00:00:00 2001 From: Anders Lyngby Bregendahl Date: Fri, 22 Oct 2021 16:57:28 +0200 Subject: [PATCH 1/3] Added FSharp port of Lux API --- kits/fsharp/simple/Bot.sln | 37 +++ kits/fsharp/simple/Bot/Bot.fs | 74 ++++++ kits/fsharp/simple/Bot/Bot.fsproj | 22 ++ kits/fsharp/simple/Bot/Lux/Agent.fs | 119 ++++++++++ kits/fsharp/simple/Bot/Lux/Annotate.fs | 18 ++ kits/fsharp/simple/Bot/Lux/Constants.fs | 44 ++++ kits/fsharp/simple/Bot/Lux/GameConstants.fs | 7 + kits/fsharp/simple/Bot/Lux/GameObjects.fs | 219 ++++++++++++++++++ .../fsharp/simple/Bot/Lux/game_constants.json | 59 +++++ kits/fsharp/simple/Bot/Main.fs | 22 ++ 10 files changed, 621 insertions(+) create mode 100644 kits/fsharp/simple/Bot.sln create mode 100644 kits/fsharp/simple/Bot/Bot.fs create mode 100644 kits/fsharp/simple/Bot/Bot.fsproj create mode 100644 kits/fsharp/simple/Bot/Lux/Agent.fs create mode 100644 kits/fsharp/simple/Bot/Lux/Annotate.fs create mode 100644 kits/fsharp/simple/Bot/Lux/Constants.fs create mode 100644 kits/fsharp/simple/Bot/Lux/GameConstants.fs create mode 100644 kits/fsharp/simple/Bot/Lux/GameObjects.fs create mode 100644 kits/fsharp/simple/Bot/Lux/game_constants.json create mode 100644 kits/fsharp/simple/Bot/Main.fs diff --git a/kits/fsharp/simple/Bot.sln b/kits/fsharp/simple/Bot.sln new file mode 100644 index 00000000..08e170e7 --- /dev/null +++ b/kits/fsharp/simple/Bot.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Bot", "Bot/Bot.fsproj", "{536AC455-03F9-4666-B5DE-98E94B5EBA69}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x64.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x64.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x86.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x86.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|Any CPU.Build.0 = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x64.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x64.Build.0 = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x86.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5FDB8BFE-ED7D-443E-AEBE-CF5AE39EA4E7} + EndGlobalSection +EndGlobal diff --git a/kits/fsharp/simple/Bot/Bot.fs b/kits/fsharp/simple/Bot/Bot.fs new file mode 100644 index 00000000..20f33259 --- /dev/null +++ b/kits/fsharp/simple/Bot/Bot.fs @@ -0,0 +1,74 @@ +module Bot + +open System +open Lux.GameObjects +open Lux.GameConstants +open Lux.Constants + +let makeActions (gameState: Game) : seq = + let player = gameState.Players.[gameState.Id]; + let opponent = gameState.Players.[(gameState.Id + 1) % 2]; + let gameMap = gameState.GameMap; + let resourceTiles = seq { + for y in 0..gameState.GameMap.height do + for x in 0..gameState.GameMap.width do + let cell = gameMap.GetCell(x, y) + if (cell.HasResource()) then + yield cell + } + seq { + // we iterate over all our units and do something with them + for unit in player.Units do + if (unit.IsWorker() && unit.CanAct()) then + if (unit.GetCargoSpaceLeft() > 0) then + // if the unit is a worker and we have space in cargo, lets find the nearest resource tile and try to mine it + let mutable closestResourceTile = None; + let mutable closestDist = Int32.MaxValue; + for cell in resourceTiles do + match cell, cell.Resource with + | _,None -> () + | cell, Some resource -> + match cell, resource.Type with + | _, s when + s = GAME_CONSTANTS.ResourceTypes.Coal && not (player.ResearchedCoal()) -> + () + | _, s when + s = GAME_CONSTANTS.ResourceTypes.Uranium && not (player.ResearchedUranium()) -> + () + | cell, _ -> + let dist = cell.Pos.DistanceTo(unit.Pos); + if (dist < closestDist) then + closestDist <- dist + closestResourceTile <- Some cell + match closestResourceTile with + | Some tile -> + let dir = unit.Pos.DirectionTo(tile.Pos); + // move the unit in the direction towards the closest resource tile's position. + yield unit.Move(dir) + | None -> () + else + // if unit is a worker and there is no cargo space left, and we have cities, lets return to them + if player.Cities.Count > 0 then + let mutable closestDist = Int32.MaxValue; + let mutable closestCityTile: option = None; + for city in player.Cities.Values do + for cityTile in city.CityTiles do + let dist = cityTile.Pos.DistanceTo(unit.Pos); + if (dist < closestDist) then + closestCityTile <- Some cityTile + closestDist <- dist + match closestCityTile with + | Some tile -> + let dir = unit.Pos.DirectionTo(tile.Pos); + yield unit.Move(dir); + | None -> () + // you can add debug annotations using the static methods of the Annotate class. + // yield Annotate.Circle(0, 0); + } + +let makeSimpleAction (gameState: Game) = + let me = gameState.Players.[0] + seq { + for worker in me.Units do + yield worker.Move(DIRECTIONS.NORTH) + } \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Bot.fsproj b/kits/fsharp/simple/Bot/Bot.fsproj new file mode 100644 index 00000000..7ffd3572 --- /dev/null +++ b/kits/fsharp/simple/Bot/Bot.fsproj @@ -0,0 +1,22 @@ + + + Exe + net5.0 + fsharp_simple + true + 3390;$(WarnOn) + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/Agent.fs b/kits/fsharp/simple/Bot/Lux/Agent.fs new file mode 100644 index 00000000..4c1e7ca1 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Agent.fs @@ -0,0 +1,119 @@ +module Lux.Agent + +open System +open GameObjects +open System.Collections.Generic + +let private updateUnfold (gameState:Game) : option = + let updateInfo = Console.ReadLine() + if updateInfo = Constants.INPUT_CONSTANTS.DONE then + None + else + let updates = updateInfo.Split(" "); + let inputIdentifier : string = updates.[0]; + match inputIdentifier with + | s when s = Constants.INPUT_CONSTANTS.RESEARCH_POINTS -> + let team: int = Int32.Parse(updates.[1]); + let newPoints = Int32.Parse(updates.[2]) + gameState.Players.[team].research_points <- newPoints + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.RESOURCES -> + let r_type = updates.[1]; + let x = Int32.Parse(updates.[2]); + let y = Int32.Parse(updates.[3]); + let amt = (int)(Double.Parse(updates.[4])) + gameState.GameMap._setResource(r_type, x, y, amt) + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.UNITS -> + let unittype = Int32.Parse(updates.[1]); + let team = Int32.Parse(updates.[2]); + let unitid = updates.[3]; + let x = Int32.Parse(updates.[4]); + let y = Int32.Parse(updates.[5]); + let cooldown = Double.Parse(updates.[6]); + let wood = Int32.Parse(updates.[7]); + let coal = Int32.Parse(updates.[8]); + let uranium = Int32.Parse(updates.[9]); + let unit = new Unit(team, Constants.ParseUnitType unittype, unitid, x, y, cooldown, wood, coal, uranium); + let newGameState = + match gameState.Players with + | [player; otherPlayer] -> + player.Units <- unit::player.Units + { gameState with + Players = [player; otherPlayer] + } + | _ -> failwith "More than two players present, do ghosts exits?" + Some(newGameState, newGameState) + | s when s = Constants.INPUT_CONSTANTS.CITY -> + let i = 1; + let team = Int32.Parse(updates.[1]); + let cityid = updates.[2]; + let fuel = Double.Parse(updates.[3]); + let lightUpkeep = Double.Parse(updates.[4]); + gameState.Players.[team].Cities.Add(cityid, new City(team, cityid, fuel, lightUpkeep)); + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.CITY_TILES -> + let i = 1; + let team = Int32.Parse(updates.[1]); + let cityid = updates.[2]; + let x = Int32.Parse(updates.[3]); + let y = Int32.Parse(updates.[4]); + let cooldown = Double.Parse(updates.[5]); + let city = gameState.Players.[team].Cities.Item cityid + let citytile = city._add_city_tile(x, y, cooldown); + gameState.GameMap.GetCell(x, y).Citytile <- citytile; + gameState.Players.[team].city_tile_count <- gameState.Players.[team].city_tile_count + 1; + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.ROADS -> + let i = 1; + let x = Int32.Parse(updates.[1]); + let y = Int32.Parse(updates.[2]); + let road = Double.Parse(updates.[3]); + let cell = gameState.GameMap.GetCell(x, y); + cell.Road <- road; + Some(gameState, gameState) + | _ -> failwith "Unknown input string" + +type IGameStateIO = + abstract member update : unit -> Game + abstract member endTurn: unit -> unit + +type GameStateIO = + { + gameState : Game + } + static member initialize() : Game = + let id = Int32.Parse(Console.ReadLine()) + let mapInfo: string = Console.ReadLine() + let mapInfoSplit: string[] = mapInfo.Split(" ") + let mapWidth = Int32.Parse(mapInfoSplit.[0]) + let mapHeight = Int32.Parse(mapInfoSplit.[1]) + let map = new GameMap(mapWidth, mapHeight) + let gameState : Game = + { + Id = id + GameMap = map; + Turn = 0; + Players = [Player(0); Player(1)] + } + gameState + + member self.update() = + let newPlayer0 = + Player(0, [], new Dictionary(), self.gameState.Players.[0].city_tile_count) + let newPlayer1 = + Player(1, [], new Dictionary(), self.gameState.Players.[1].city_tile_count) + let newGameState = + { self.gameState with + Turn = self.gameState.Turn + 1; + Players = [newPlayer0; newPlayer1] + } + |> Seq.unfold (fun state -> + updateUnfold state + ) + |> Seq.last + { self with gameState = newGameState } + + /// Ends turn + member self.endTurn() = + Console.WriteLine("D_FINISH") diff --git a/kits/fsharp/simple/Bot/Lux/Annotate.fs b/kits/fsharp/simple/Bot/Lux/Annotate.fs new file mode 100644 index 00000000..78eefbfd --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Annotate.fs @@ -0,0 +1,18 @@ +module Lux.Annotate + +let Circle(x: int, y: int) : string = + $"dc {x} {y}" + +let x(x: int, y: int) : string = + $"dx {x} {y}" + +let Line(x1: int, y1: int, x2: int, y2: int) : string = + $"dl {x1} {y1} {x2} {y2}" + +/// text at cell on map +let Text (x: int, y: int, message: string, fontsize: option) : string = + $"dt {x} {y} '{message}' {defaultArg fontsize 16}" + +/// text besides map +let Sidetext(message: string) : string = + $"dst '{message}'" diff --git a/kits/fsharp/simple/Bot/Lux/Constants.fs b/kits/fsharp/simple/Bot/Lux/Constants.fs new file mode 100644 index 00000000..173c3129 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Constants.fs @@ -0,0 +1,44 @@ +module Lux.Constants + +module INPUT_CONSTANTS = + let RESEARCH_POINTS = "rp" + let RESOURCES = "r" + let UNITS = "u" + let CITY = "c" + let CITY_TILES = "ct" + let ROADS = "ccd" + let DONE = "D_DONE" + +type DIRECTIONS = + | NORTH + | WEST + | SOUTH + | EAST + | CENTER + override this.ToString() = + match this with + | NORTH -> "n" + | WEST -> "w" + | SOUTH -> "s" + | EAST -> "e" + | CENTER -> "c" + +type UNIT_TYPES = + | WORKER = 0 + | CART = 1 + +let ParseUnitType value = + match value with + | 0 -> UNIT_TYPES.WORKER + | 1 -> UNIT_TYPES.CART + | _ -> failwith "Unexpected unit type encountered." + +type RESOURCE_TYPES = + | WOOD + | URANIUM + | COAL + override this.ToString() = + match this with + | WOOD -> "wood" + | URANIUM -> "uranium" + | COAL -> "coal" diff --git a/kits/fsharp/simple/Bot/Lux/GameConstants.fs b/kits/fsharp/simple/Bot/Lux/GameConstants.fs new file mode 100644 index 00000000..6b944c6b --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/GameConstants.fs @@ -0,0 +1,7 @@ +namespace Lux + +open FSharp.Data + +module GameConstants = + type private jsonFormat = JsonProvider<"./Lux/game_constants.json"> + let GAME_CONSTANTS = jsonFormat.Parse("./Lux/game_constants.json") \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/GameObjects.fs b/kits/fsharp/simple/Bot/Lux/GameObjects.fs new file mode 100644 index 00000000..0932cd33 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/GameObjects.fs @@ -0,0 +1,219 @@ +module Lux.GameObjects + +open Constants +open System.Collections.Generic +open GameConstants + +// GameMap: +type Resource(r_type: string, amount: int) = + member self.Type = r_type + member self.amount = amount + +type Position(_x, _y) = + member self.x = _x + member self.y = _y + static member (-) (self: Position, pos: Position) : int = + abs(pos.x - self.x) + abs(pos.y - self.y) + + /// Returns Manhattan (L1/grid) distance to pos + member self.DistanceTo(pos: Position) = + self - pos + + member self.IsAdjacent(pos: Position) = + (self - pos) <= 1 + + static member op_Equality (self: Position, pos: Position) : bool = + self.x = pos.x && self.y = pos.y + + member self.translate(direction, units): Position = + match direction with + | NORTH -> + Position(self.x, self.y - units) + | EAST -> + Position(self.x + units, self.y) + | SOUTH -> + Position(self.x, self.y + units) + | WEST -> + Position(self.x - units, self.y) + | CENTER -> + Position(self.x, self.y) + + /// Return closest position to target_pos from this position + member self.DirectionTo(target_pos: 'Position') : DIRECTIONS = + let check_dirs = [ + NORTH; + EAST; + SOUTH; + WEST; + ] + let closest_dist_init, closest_dir_init = (self.DistanceTo(target_pos), DIRECTIONS.CENTER) + let closest_dist, closest_dir = + check_dirs + |> List.fold (fun (closest_dist, closest_dir) direction -> + let newpos = self.translate(direction, 1) + let dist = target_pos.DistanceTo(newpos) + if dist < closest_dist then + dist, direction + else + closest_dist, closest_dir + ) (closest_dist_init, closest_dir_init) + closest_dir + + override self.ToString() = + sprintf "(%d, %d)" self.x self.y + +// GameObjects: +type CityTile(teamid, cityid, x, y, cooldown: float) = + member self.Cityid: string = cityid + member self.Team = teamid + member self.Pos = Position(x, y) + member self.Cooldown = cooldown + /// Whether or not this unit can research or build + member self.CanAct() : bool = + self.Cooldown < 1.0 + /// returns command to ask this tile to research this turn + member self.Research() : string = + sprintf "r %d %d" self.Pos.x self.Pos.y + /// returns command to ask this tile to build a worker this turn + member self.BuildWorker() : string = + sprintf "bw %d %d" self.Pos.x self.Pos.y + + /// returns command to ask this tile to build a cart this turn + member self.BuildCart() : string = + sprintf "bc %d %d" self.Pos.x self.Pos.y + +type Cell(x, y) = + member self.Pos: Position = Position(x, y) + member val Resource: option = None with get, set + member val Citytile: option = None with get, set + member val Road = 0.0 with get, set + member self.HasResource() = + match self.Resource with + | None -> + false + | Some resource -> + resource.amount > 0 + +type City(teamid: int, cityid: string, fuel: float, light_upkeep_init: float) = + member _.cityid = cityid + member _.team = teamid + member _.fuel = fuel + member _.CityTiles: List = new List() + member _.light_upkeep = light_upkeep_init + member self._add_city_tile(x, y, cooldown) = + let ct = CityTile(self.team, self.cityid, x, y, cooldown) + self.CityTiles.Add(ct) + Some ct + //member self.get_light_upkeep() = + // self.light_upkeep + +type GameMap(width, height) = + member self.height = height + member self.width = width + // member self.map: List[List[Cell]] = [None] * height + member self.map: Cell[,] = + Array2D.init width height (fun x y -> Cell(x,y)) + // for y in 0 .. self.height) do + // self.map.[y] = [None] * width + // for x in range(0, self.width) do + // self.map[y][x] = Cell(x, y) + + member self.GetCellByPos(pos: Position) : Cell = + self.map.[pos.y, pos.x] + + member self.GetCell(x, y) : Cell = + self.map.[y, x] + + /// do not use this function, this is for internal tracking of state + member self._setResource(r_type, x, y, amount) = + self.GetCell(x, y).Resource <- Some(Resource(r_type, amount)) + () + +type Cargo(wood, coal, uranium) = + member self.wood = wood + member self.coal = coal + member self.uranium = uranium + new() = + Cargo(0, 0, 0) + + member self.__str__() : string = + sprintf "Cargo | Wood: %d, Coal: %d, Uranium: %d" self.wood self.coal self.uranium + +type Unit(teamid, u_type, unitid, x, y, cooldown, wood, coal, uranium) = + member self.Pos = Position(x, y) + member self.team: int = teamid + member self.id: string = unitid + member private self.u_type = u_type + member self.cooldown = cooldown + member self.cargo : Cargo = new Cargo(wood, coal, uranium) + member self.IsWorker() : bool = + self.u_type = UNIT_TYPES.WORKER + + member self.IsCart() : bool = + self.u_type = UNIT_TYPES.CART + + /// get cargo space left in this unit + member self.GetCargoSpaceLeft() = + let spaceused = self.cargo.wood + self.cargo.coal + self.cargo.uranium + if self.u_type = UNIT_TYPES.WORKER then + GAME_CONSTANTS.Parameters.ResourceCapacity.Worker - spaceused + //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["WORKER"] - spaceused + else + GAME_CONSTANTS.Parameters.ResourceCapacity.Cart - spaceused + //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["CART"] - spaceused + + /// whether or not the unit can build where it is right now + member self.CanBuild(game_map: GameMap) : bool = + let cell = game_map.GetCellByPos(self.Pos) + not (cell.HasResource()) + && self.CanAct() + && (self.cargo.wood + self.cargo.coal + self.cargo.uranium) >= GAME_CONSTANTS.Parameters.CityBuildCost + + /// whether or not the unit can move or not. This does not check for potential collisions into other units or enemy cities + member self.CanAct() : bool = + self.cooldown < 1.0 + + /// return the command to move unit in the given direction + member self.Move(dir: DIRECTIONS) : string = + sprintf "m %s %s" self.id (dir.ToString()) + // $"m {self.id} {dir}" + + /// return the command to transfer a resource from a source unit to a destination unit as specified by their ids + member self.Transfer(dest_id, resourceType, amount) : string = + sprintf "t %s %d %d %d" self.id dest_id resourceType amount + + /// return the command to build a city right under the worker + member self.BuildCity() : string = + sprintf "bcity %s" self.id + + /// return the command to pillage whatever is underneath the worker + member self.pillage() : string = + sprintf "p %s" self.id + +type Player(team, units, cities, tilecount) = + new(teamId) = + Player(teamId, [], new Dictionary(), 0) + member val team: int = team with get, set + member val research_points = 0 with get, set + member val Units: list = units with get, set + member val Cities: Dictionary = cities + member val city_tile_count: int = tilecount with get, set + member self.ResearchedCoal() : bool = + self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Coal + member self.ResearchedUranium() : bool = + self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Uranium +type Game = + { + Id: int + Turn: int + GameMap: GameMap + Players: list + } + member private self._reset_player_states() = + { self with + Players = + [ + Player(0, [], new Dictionary(), 0); + Player(1, [], new Dictionary(), 0); + ] + } \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/game_constants.json b/kits/fsharp/simple/Bot/Lux/game_constants.json new file mode 100644 index 00000000..9f750757 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/game_constants.json @@ -0,0 +1,59 @@ +{ + "UNIT_TYPES": { + "WORKER": 0, + "CART": 1 + }, + "RESOURCE_TYPES": { + "WOOD": "wood", + "COAL": "coal", + "URANIUM": "uranium" + }, + "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/fsharp/simple/Bot/Main.fs b/kits/fsharp/simple/Bot/Main.fs new file mode 100644 index 00000000..8a407d25 --- /dev/null +++ b/kits/fsharp/simple/Bot/Main.fs @@ -0,0 +1,22 @@ +open System +open Lux.Agent + +[] +let main args = + let agent: GameStateIO = { gameState = GameStateIO.initialize() } + Threading.Thread.Sleep(10000) + while true do + let game = agent.update(); + + (* Set actions *) + let actions : seq = + Bot.makeSimpleAction game.gameState + + (* AI Code Goes Above! *) + + (* Do not edit *) + let command = + String.concat "," actions + Console.WriteLine(command); + agent.endTurn(); + 0 \ No newline at end of file From 7199f416f0cab68c39062be05ed4a04a691e9ea3 Mon Sep 17 00:00:00 2001 From: Anders Lyngby Bregendahl Date: Fri, 22 Oct 2021 16:57:28 +0200 Subject: [PATCH 2/3] Added FSharp port of Lux API --- kits/fsharp/simple/Bot.sln | 37 +++ kits/fsharp/simple/Bot/Bot.fs | 79 +++++++ kits/fsharp/simple/Bot/Bot.fsproj | 24 ++ kits/fsharp/simple/Bot/Lux/Agent.fs | 118 ++++++++++ kits/fsharp/simple/Bot/Lux/Annotate.fs | 18 ++ kits/fsharp/simple/Bot/Lux/Constants.fs | 44 ++++ kits/fsharp/simple/Bot/Lux/GameConstants.fs | 75 ++++++ kits/fsharp/simple/Bot/Lux/GameObjects.fs | 218 ++++++++++++++++++ .../fsharp/simple/Bot/Lux/game_constants.json | 59 +++++ kits/fsharp/simple/Bot/Main.fs | 39 ++++ 10 files changed, 711 insertions(+) create mode 100644 kits/fsharp/simple/Bot.sln create mode 100644 kits/fsharp/simple/Bot/Bot.fs create mode 100644 kits/fsharp/simple/Bot/Bot.fsproj create mode 100644 kits/fsharp/simple/Bot/Lux/Agent.fs create mode 100644 kits/fsharp/simple/Bot/Lux/Annotate.fs create mode 100644 kits/fsharp/simple/Bot/Lux/Constants.fs create mode 100644 kits/fsharp/simple/Bot/Lux/GameConstants.fs create mode 100644 kits/fsharp/simple/Bot/Lux/GameObjects.fs create mode 100644 kits/fsharp/simple/Bot/Lux/game_constants.json create mode 100644 kits/fsharp/simple/Bot/Main.fs diff --git a/kits/fsharp/simple/Bot.sln b/kits/fsharp/simple/Bot.sln new file mode 100644 index 00000000..08e170e7 --- /dev/null +++ b/kits/fsharp/simple/Bot.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Bot", "Bot/Bot.fsproj", "{536AC455-03F9-4666-B5DE-98E94B5EBA69}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x64.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x64.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x86.ActiveCfg = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Debug|x86.Build.0 = Debug|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|Any CPU.Build.0 = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x64.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x64.Build.0 = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x86.ActiveCfg = Release|Any CPU + {536AC455-03F9-4666-B5DE-98E94B5EBA69}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5FDB8BFE-ED7D-443E-AEBE-CF5AE39EA4E7} + EndGlobalSection +EndGlobal diff --git a/kits/fsharp/simple/Bot/Bot.fs b/kits/fsharp/simple/Bot/Bot.fs new file mode 100644 index 00000000..face7beb --- /dev/null +++ b/kits/fsharp/simple/Bot/Bot.fs @@ -0,0 +1,79 @@ +module Bot + +open System +open Lux.GameObjects +open Lux.GameConstants +open Lux.Constants +open Lux + +let IsCoal (s) = + s = GAME_CONSTANTS.ResourceTypes.Coal +let IsUranium (s) = + s = GAME_CONSTANTS.ResourceTypes.Uranium + +let makeActions (gameState: Game) : seq = + let player = gameState.Players.[gameState.Id]; + let opponent = gameState.Players.[(gameState.Id + 1) % 2]; + let gameMap = gameState.GameMap; + let resourceTiles = + seq { + for y in 0 .. gameState.GameMap.height-1 do + for x in 0 .. gameState.GameMap.width-1 do + let cell = gameMap.GetCell(x, y) + if (cell.HasResource()) then + yield cell + } + |> List.ofSeq + seq { + // we iterate over all our units and do something with them + for unit in player.Units do + if (unit.IsWorker() && unit.CanAct()) then + // if the unit is a worker and we have space in cargo, lets find the nearest resource tile and try to mine it + let mutable closestDist = Int32.MaxValue; + let mutable closestResourceTile = None; + if unit.GetCargoSpaceLeft() > 0 then + for resourceTile in resourceTiles do + match resourceTile, resourceTile.Resource with + | _, None -> () + | cell, Some resource -> + match cell, resource.Type with + | _, s when + IsCoal(s) && not (player.ResearchedCoal()) -> + () + | _, s when + IsUranium(s) && not (player.ResearchedUranium()) -> + () + | cell, _ -> + let dist = cell.Pos.DistanceTo(unit.Pos); + if (dist < closestDist) then + closestDist <- dist + closestResourceTile <- Some cell + match closestResourceTile with + | None -> () + | Some tile -> + let dir = unit.Pos.DirectionTo(tile.Pos); + // move the unit in the direction towards the closest resource tile's position. + yield Annotate.Line(unit.Pos.x, unit.Pos.y, tile.Pos.x, tile.Pos.y) + yield Annotate.Sidetext($"Moves to resource {player.team}") + yield unit.Move(dir) + else + // if unit is a worker and there is no cargo space left, and we have cities, lets return to them + if player.Cities.Count > 0 then + let mutable closestDist = Int32.MaxValue; + let mutable closestCityTile: option = None; + for city in player.Cities.Values do + for cityTile in city.CityTiles do + let dist = cityTile.Pos.DistanceTo(unit.Pos); + if (dist < closestDist) then + closestCityTile <- Some cityTile + closestDist <- dist + match closestCityTile with + | None -> () + | Some tile -> + let dir = unit.Pos.DirectionTo(tile.Pos); + yield Annotate.Line(unit.Pos.x, unit.Pos.y, tile.Pos.x, tile.Pos.y) + yield Annotate.Sidetext($"Moves toward city {player.team}") + yield unit.Move(dir); + // you can add debug annotations using the static methods of the Annotate class. + // yield Annotate.Circle(0, 0); + } \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Bot.fsproj b/kits/fsharp/simple/Bot/Bot.fsproj new file mode 100644 index 00000000..4e10da99 --- /dev/null +++ b/kits/fsharp/simple/Bot/Bot.fsproj @@ -0,0 +1,24 @@ + + + Exe + net5.0 + fsharp_simple + true + 3390;$(WarnOn) + + + + PreserveNewest + + + + + + + + + + + + + \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/Agent.fs b/kits/fsharp/simple/Bot/Lux/Agent.fs new file mode 100644 index 00000000..62be23da --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Agent.fs @@ -0,0 +1,118 @@ +module Lux.Agent + +open System +open GameObjects +open System.Collections.Generic +open System.IO + +let private updateUnfold (gameState:Game) (input: TextReader) : option = + let updateInfo = input.ReadLine() + if updateInfo = Constants.INPUT_CONSTANTS.DONE then + None + else + let updates = updateInfo.Split(" "); + let inputIdentifier : string = updates.[0]; + match inputIdentifier with + | s when s = Constants.INPUT_CONSTANTS.RESEARCH_POINTS -> + let team: int = Int32.Parse(updates.[1]); + let newPoints = Int32.Parse(updates.[2]) + gameState.Players.[team].research_points <- newPoints + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.RESOURCES -> + let r_type = updates.[1]; + let x = Int32.Parse(updates.[2]); + let y = Int32.Parse(updates.[3]); + let amt = (int)(Double.Parse(updates.[4])) + gameState.GameMap._setResource(r_type, x, y, amt) + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.UNITS -> + let unittype = Int32.Parse(updates.[1]); + let team = Int32.Parse(updates.[2]); + let unitid = updates.[3]; + let x = Int32.Parse(updates.[4]); + let y = Int32.Parse(updates.[5]); + let cooldown = Double.Parse(updates.[6]); + let wood = Int32.Parse(updates.[7]); + let coal = Int32.Parse(updates.[8]); + let uranium = Int32.Parse(updates.[9]); + let unit = new Unit(team, Constants.ParseUnitType unittype, unitid, x, y, cooldown, wood, coal, uranium); + let newGameState = + match gameState.Players with + | players -> + players.[team].Units <- unit::players.[team].Units + { gameState with + Players = players + } + | _ -> failwith "More than two players present, do ghosts exits?" + Some(newGameState, newGameState) + | s when s = Constants.INPUT_CONSTANTS.CITY -> + let team = Int32.Parse(updates.[1]); + let cityid = updates.[2]; + let fuel = Double.Parse(updates.[3]); + let lightUpkeep = Double.Parse(updates.[4]); + gameState.Players.[team].Cities.Add(cityid, new City(team, cityid, fuel, lightUpkeep)); + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.CITY_TILES -> + let team = Int32.Parse(updates.[1]); + let cityid = updates.[2]; + let x = Int32.Parse(updates.[3]); + let y = Int32.Parse(updates.[4]); + let cooldown = Double.Parse(updates.[5]); + let city = gameState.Players.[team].Cities.Item cityid + let citytile = city._add_city_tile(x, y, cooldown); + gameState.GameMap.GetCell(x, y).Citytile <- citytile; + gameState.Players.[team].city_tile_count <- gameState.Players.[team].city_tile_count + 1; + Some(gameState, gameState) + | s when s = Constants.INPUT_CONSTANTS.ROADS -> + let x = Int32.Parse(updates.[1]); + let y = Int32.Parse(updates.[2]); + let road = Double.Parse(updates.[3]); + let cell = gameState.GameMap.GetCell(x, y); + cell.Road <- road; + Some(gameState, gameState) + | _ -> failwith "Unknown input string" + +type IGameStateIO = + abstract member update : unit -> Game + abstract member endTurn: unit -> unit + +type GameStateIO = + { + gameState : Game + } + static member initialize(input: TextReader) : Game = + let id = Int32.Parse(input.ReadLine()) + let mapInfo: string = input.ReadLine() + let mapInfoSplit: string[] = mapInfo.Split(" ") + let mapWidth = Int32.Parse(mapInfoSplit.[0]) + let mapHeight = Int32.Parse(mapInfoSplit.[1]) + let map = new GameMap(mapWidth, mapHeight) + let gameState : Game = + { + Id = id + GameMap = map; + Turn = 0; + Players = [Player(0); Player(1)] + } + gameState + + member self.update(input: TextReader) = + let newPlayer0 = + Player(0, [], new Dictionary(), self.gameState.Players.[0].city_tile_count) + let newPlayer1 = + Player(1, [], new Dictionary(), self.gameState.Players.[1].city_tile_count) + let newGameState = + { self.gameState with + Turn = self.gameState.Turn + 1; + Players = [newPlayer0; newPlayer1] + GameMap = GameMap(self.gameState.GameMap.height, self.gameState.GameMap.width) + } + |> Seq.unfold (fun state -> + updateUnfold state input + ) + |> Seq.last + { self with gameState = newGameState } + + /// Ends turn + member self.endTurn() = + Console.WriteLine("D_FINISH") diff --git a/kits/fsharp/simple/Bot/Lux/Annotate.fs b/kits/fsharp/simple/Bot/Lux/Annotate.fs new file mode 100644 index 00000000..78eefbfd --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Annotate.fs @@ -0,0 +1,18 @@ +module Lux.Annotate + +let Circle(x: int, y: int) : string = + $"dc {x} {y}" + +let x(x: int, y: int) : string = + $"dx {x} {y}" + +let Line(x1: int, y1: int, x2: int, y2: int) : string = + $"dl {x1} {y1} {x2} {y2}" + +/// text at cell on map +let Text (x: int, y: int, message: string, fontsize: option) : string = + $"dt {x} {y} '{message}' {defaultArg fontsize 16}" + +/// text besides map +let Sidetext(message: string) : string = + $"dst '{message}'" diff --git a/kits/fsharp/simple/Bot/Lux/Constants.fs b/kits/fsharp/simple/Bot/Lux/Constants.fs new file mode 100644 index 00000000..173c3129 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/Constants.fs @@ -0,0 +1,44 @@ +module Lux.Constants + +module INPUT_CONSTANTS = + let RESEARCH_POINTS = "rp" + let RESOURCES = "r" + let UNITS = "u" + let CITY = "c" + let CITY_TILES = "ct" + let ROADS = "ccd" + let DONE = "D_DONE" + +type DIRECTIONS = + | NORTH + | WEST + | SOUTH + | EAST + | CENTER + override this.ToString() = + match this with + | NORTH -> "n" + | WEST -> "w" + | SOUTH -> "s" + | EAST -> "e" + | CENTER -> "c" + +type UNIT_TYPES = + | WORKER = 0 + | CART = 1 + +let ParseUnitType value = + match value with + | 0 -> UNIT_TYPES.WORKER + | 1 -> UNIT_TYPES.CART + | _ -> failwith "Unexpected unit type encountered." + +type RESOURCE_TYPES = + | WOOD + | URANIUM + | COAL + override this.ToString() = + match this with + | WOOD -> "wood" + | URANIUM -> "uranium" + | COAL -> "coal" diff --git a/kits/fsharp/simple/Bot/Lux/GameConstants.fs b/kits/fsharp/simple/Bot/Lux/GameConstants.fs new file mode 100644 index 00000000..9c5e190f --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/GameConstants.fs @@ -0,0 +1,75 @@ +namespace Lux + +open FSharp.Data + +module GameConstants = + type private jsonFormat = JsonProvider<"./Lux/game_constants.json"> + let private STATIC_CONST = """ +{ + "UNIT_TYPES": { + "WORKER": 0, + "CART": 1 + }, + "RESOURCE_TYPES": { + "WOOD": "wood", + "COAL": "coal", + "URANIUM": "uranium" + }, + "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 + } +} +""" + let GAME_CONSTANTS = + let path = "./Lux/game_constants.json" + let content = + if System.IO.File.Exists(path) then + System.IO.File.ReadAllText(path) + else + STATIC_CONST + jsonFormat.Parse(content) \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/GameObjects.fs b/kits/fsharp/simple/Bot/Lux/GameObjects.fs new file mode 100644 index 00000000..7bff7186 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/GameObjects.fs @@ -0,0 +1,218 @@ +module Lux.GameObjects + +open Constants +open System.Collections.Generic +open GameConstants + +// GameMap: +type Resource(r_type: string, amount: int) = + member self.Type = r_type + member self.amount = amount + +type Position(_x, _y) = + member self.x = _x + member self.y = _y + static member (-) (self: Position, pos: Position) : int = + abs(pos.x - self.x) + abs(pos.y - self.y) + + /// Returns Manhattan (L1/grid) distance to pos + member self.DistanceTo(pos: Position) = + self - pos + + member self.IsAdjacent(pos: Position) = + (self - pos) <= 1 + + static member op_Equality (self: Position, pos: Position) : bool = + self.x = pos.x && self.y = pos.y + + member self.translate(direction, units): Position = + match direction with + | NORTH -> + Position(self.x, self.y - units) + | EAST -> + Position(self.x + units, self.y) + | SOUTH -> + Position(self.x, self.y + units) + | WEST -> + Position(self.x - units, self.y) + | CENTER -> + Position(self.x, self.y) + + /// Return closest position to target_pos from this position + member self.DirectionTo(target_pos: 'Position') : DIRECTIONS = + let check_dirs = [ + NORTH; + EAST; + SOUTH; + WEST; + ] + let closest_dist_init, closest_dir_init = (self.DistanceTo(target_pos), DIRECTIONS.CENTER) + let closest_dist, closest_dir = + check_dirs + |> List.fold (fun (closest_dist, closest_dir) direction -> + let newpos = self.translate(direction, 1) + let dist = target_pos.DistanceTo(newpos) + if dist < closest_dist then + dist, direction + else + closest_dist, closest_dir + ) (closest_dist_init, closest_dir_init) + closest_dir + + override self.ToString() = + sprintf "(%d, %d)" self.x self.y + +// GameObjects: +type CityTile(teamid, cityid, x, y, cooldown: float) = + member self.Cityid: string = cityid + member self.Team = teamid + member self.Pos = Position(x, y) + member self.Cooldown = cooldown + /// Whether or not this unit can research or build + member self.CanAct() : bool = + self.Cooldown < 1.0 + /// returns command to ask this tile to research this turn + member self.Research() : string = + sprintf "r %d %d" self.Pos.x self.Pos.y + /// returns command to ask this tile to build a worker this turn + member self.BuildWorker() : string = + sprintf "bw %d %d" self.Pos.x self.Pos.y + + /// returns command to ask this tile to build a cart this turn + member self.BuildCart() : string = + sprintf "bc %d %d" self.Pos.x self.Pos.y + +type Cell(x, y) = + member self.Pos: Position = Position(x, y) + member val Resource: option = None with get, set + member val Citytile: option = None with get, set + member val Road = 0.0 with get, set + member self.HasResource() = + match self.Resource with + | None -> + false + | Some resource -> + resource.amount > 0 + +type City(teamid: int, cityid: string, fuel: float, light_upkeep_init: float) = + member self.cityid = cityid + member self.team = teamid + member self.fuel = fuel + member val CityTiles: List = new List() with get, set + member self.light_upkeep = light_upkeep_init + member self._add_city_tile(x, y, cooldown) = + let ct = CityTile(self.team, self.cityid, x, y, cooldown) + self.CityTiles.Add(ct) + Some ct + //member self.get_light_upkeep() = + // self.light_upkeep + +type GameMap(width, height) = + member self.height = height + member self.width = width + // member self.map: List[List[Cell]] = [None] * height + member val map: Cell[,] = Array2D.init width height (fun x y -> Cell(x,y)) + // for y in 0 .. self.height) do + // self.map.[y] = [None] * width + // for x in range(0, self.width) do + // self.map[y][x] = Cell(x, y) + + member self.GetCellByPos(pos: Position) : Cell = + self.map.[pos.x, pos.y] + + member self.GetCell(x, y) : Cell = + self.map.[x, y] + + /// do not use this function, this is for internal tracking of state + member self._setResource(r_type, x, y, amount) = + self.GetCell(x, y).Resource <- Some(Resource(r_type, amount)) + +type Cargo(wood, coal, uranium) = + member self.wood = wood + member self.coal = coal + member self.uranium = uranium + new() = + Cargo(0, 0, 0) + + member self.__str__() : string = + sprintf "Cargo | Wood: %d, Coal: %d, Uranium: %d" self.wood self.coal self.uranium + +type Unit(teamid, u_type, unitid, x, y, cooldown, wood, coal, uranium) = + member val Pos = Position(x, y) + member val team: int = teamid with get, set + member val id: string = unitid with get, set + member private self.u_type = u_type + member self.cooldown = cooldown + member self.cargo : Cargo = new Cargo(wood, coal, uranium) + member self.IsWorker() : bool = + self.u_type = UNIT_TYPES.WORKER + + member self.IsCart() : bool = + self.u_type = UNIT_TYPES.CART + + /// get cargo space left in this unit + member self.GetCargoSpaceLeft() = + let spaceused = self.cargo.wood + self.cargo.coal + self.cargo.uranium + if self.u_type = UNIT_TYPES.WORKER then + GAME_CONSTANTS.Parameters.ResourceCapacity.Worker - spaceused + //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["WORKER"] - spaceused + else + GAME_CONSTANTS.Parameters.ResourceCapacity.Cart - spaceused + //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["CART"] - spaceused + + /// whether or not the unit can build where it is right now + member self.CanBuild(game_map: GameMap) : bool = + let cell = game_map.GetCellByPos(self.Pos) + not (cell.HasResource()) + && self.CanAct() + && (self.cargo.wood + self.cargo.coal + self.cargo.uranium) >= GAME_CONSTANTS.Parameters.CityBuildCost + + /// whether or not the unit can move or not. This does not check for potential collisions into other units or enemy cities + member self.CanAct() : bool = + self.cooldown < 1.0 + + /// return the command to move unit in the given direction + member self.Move(dir: DIRECTIONS) : string = + sprintf "m %s %s" self.id (dir.ToString()) + // $"m {self.id} {dir}" + + /// return the command to transfer a resource from a source unit to a destination unit as specified by their ids + member self.Transfer(dest_id, resourceType, amount) : string = + sprintf "t %s %d %d %d" self.id dest_id resourceType amount + + /// return the command to build a city right under the worker + member self.BuildCity() : string = + sprintf "bcity %s" self.id + + /// return the command to pillage whatever is underneath the worker + member self.pillage() : string = + sprintf "p %s" self.id + +type Player(team, units, cities, tilecount) = + new(teamId) = + Player(teamId, [], new Dictionary(), 0) + member val team: int = team with get, set + member val research_points = 0 with get, set + member val Units: list = units with get, set + member val Cities: Dictionary = cities + member val city_tile_count: int = tilecount with get, set + member self.ResearchedCoal() : bool = + self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Coal + member self.ResearchedUranium() : bool = + self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Uranium + +type Game = + { + Id: int + Turn: int + GameMap: GameMap + Players: list + } + member private self._reset_player_states() = + { self with + Players = + [ + Player(0, [], new Dictionary(), 0); + Player(1, [], new Dictionary(), 0); + ] + } \ No newline at end of file diff --git a/kits/fsharp/simple/Bot/Lux/game_constants.json b/kits/fsharp/simple/Bot/Lux/game_constants.json new file mode 100644 index 00000000..9f750757 --- /dev/null +++ b/kits/fsharp/simple/Bot/Lux/game_constants.json @@ -0,0 +1,59 @@ +{ + "UNIT_TYPES": { + "WORKER": 0, + "CART": 1 + }, + "RESOURCE_TYPES": { + "WOOD": "wood", + "COAL": "coal", + "URANIUM": "uranium" + }, + "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/fsharp/simple/Bot/Main.fs b/kits/fsharp/simple/Bot/Main.fs new file mode 100644 index 00000000..6c8acdb5 --- /dev/null +++ b/kits/fsharp/simple/Bot/Main.fs @@ -0,0 +1,39 @@ +open System +open System.IO +open Lux.Agent + +type PipedReader(input: TextReader, output: StreamWriter) = + inherit TextReader() + member val reader = input + member val writer = output + with + override self.ReadLine() = + let input = self.reader.ReadLine() + self.writer.WriteLine(input) + input + +[] +let main args = + let inputStream = Console.In + let logStream = StreamWriter($"logInput_{Random().Next()}.txt") + let pipedStream = PipedReader(inputStream, logStream) + let agent: GameStateIO = { gameState = GameStateIO.initialize(pipedStream) } + //System.Threading.Thread.Sleep(10000) + List.unfold (fun (agent: GameStateIO) -> + let game = agent.update(pipedStream) + + (* Set actions *) + let actions : seq = + Bot.makeActions game.gameState + + (* AI Code Goes Above! *) + + (* Do not edit *) + let command = + String.concat "," actions + Console.WriteLine(command) + agent.endTurn() + Some ((), agent) + ) agent + |> ignore + 0 \ No newline at end of file From 2df6de9e0c4a1801da0ca5dd92b8742a9cc4e25f Mon Sep 17 00:00:00 2001 From: Anders Lyngby Bregendahl Date: Fri, 22 Oct 2021 23:53:26 +0200 Subject: [PATCH 3/3] Removed code for debugging --- kits/fsharp/simple/Bot/Bot.fs | 27 ++----- kits/fsharp/simple/Bot/Lux/Agent.fs | 25 +++--- kits/fsharp/simple/Bot/Lux/GameObjects.fs | 92 +++++++++++------------ kits/fsharp/simple/Bot/Main.fs | 26 +------ 4 files changed, 70 insertions(+), 100 deletions(-) diff --git a/kits/fsharp/simple/Bot/Bot.fs b/kits/fsharp/simple/Bot/Bot.fs index face7beb..37d466a8 100644 --- a/kits/fsharp/simple/Bot/Bot.fs +++ b/kits/fsharp/simple/Bot/Bot.fs @@ -3,27 +3,20 @@ open System open Lux.GameObjects open Lux.GameConstants -open Lux.Constants open Lux -let IsCoal (s) = - s = GAME_CONSTANTS.ResourceTypes.Coal -let IsUranium (s) = - s = GAME_CONSTANTS.ResourceTypes.Uranium - -let makeActions (gameState: Game) : seq = - let player = gameState.Players.[gameState.Id]; - let opponent = gameState.Players.[(gameState.Id + 1) % 2]; - let gameMap = gameState.GameMap; +let takeActions (gameState: Game) : seq = + let player = gameState.Players.[gameState.Id] + let opponent = gameState.Players.[(gameState.Id + 1) % 2] + let gameMap = gameState.GameMap let resourceTiles = seq { - for y in 0 .. gameState.GameMap.height-1 do - for x in 0 .. gameState.GameMap.width-1 do + for y in 0 .. gameState.GameMap.Height-1 do + for x in 0 .. gameState.GameMap.Width-1 do let cell = gameMap.GetCell(x, y) if (cell.HasResource()) then yield cell } - |> List.ofSeq seq { // we iterate over all our units and do something with them for unit in player.Units do @@ -38,10 +31,10 @@ let makeActions (gameState: Game) : seq = | cell, Some resource -> match cell, resource.Type with | _, s when - IsCoal(s) && not (player.ResearchedCoal()) -> + s = GAME_CONSTANTS.ResourceTypes.Coal && not (player.ResearchedCoal()) -> () | _, s when - IsUranium(s) && not (player.ResearchedUranium()) -> + s = GAME_CONSTANTS.ResourceTypes.Uranium && not (player.ResearchedUranium()) -> () | cell, _ -> let dist = cell.Pos.DistanceTo(unit.Pos); @@ -53,8 +46,6 @@ let makeActions (gameState: Game) : seq = | Some tile -> let dir = unit.Pos.DirectionTo(tile.Pos); // move the unit in the direction towards the closest resource tile's position. - yield Annotate.Line(unit.Pos.x, unit.Pos.y, tile.Pos.x, tile.Pos.y) - yield Annotate.Sidetext($"Moves to resource {player.team}") yield unit.Move(dir) else // if unit is a worker and there is no cargo space left, and we have cities, lets return to them @@ -71,8 +62,6 @@ let makeActions (gameState: Game) : seq = | None -> () | Some tile -> let dir = unit.Pos.DirectionTo(tile.Pos); - yield Annotate.Line(unit.Pos.x, unit.Pos.y, tile.Pos.x, tile.Pos.y) - yield Annotate.Sidetext($"Moves toward city {player.team}") yield unit.Move(dir); // you can add debug annotations using the static methods of the Annotate class. // yield Annotate.Circle(0, 0); diff --git a/kits/fsharp/simple/Bot/Lux/Agent.fs b/kits/fsharp/simple/Bot/Lux/Agent.fs index 62be23da..cf7179f8 100644 --- a/kits/fsharp/simple/Bot/Lux/Agent.fs +++ b/kits/fsharp/simple/Bot/Lux/Agent.fs @@ -3,10 +3,9 @@ module Lux.Agent open System open GameObjects open System.Collections.Generic -open System.IO -let private updateUnfold (gameState:Game) (input: TextReader) : option = - let updateInfo = input.ReadLine() +let private updateUnfold (gameState:Game) : option = + let updateInfo = Console.ReadLine() if updateInfo = Constants.INPUT_CONSTANTS.DONE then None else @@ -16,7 +15,7 @@ let private updateUnfold (gameState:Game) (input: TextReader) : option let team: int = Int32.Parse(updates.[1]); let newPoints = Int32.Parse(updates.[2]) - gameState.Players.[team].research_points <- newPoints + gameState.Players.[team].ResearchPoints <- newPoints Some(gameState, gameState) | s when s = Constants.INPUT_CONSTANTS.RESOURCES -> let r_type = updates.[1]; @@ -61,7 +60,7 @@ let private updateUnfold (gameState:Game) (input: TextReader) : option let x = Int32.Parse(updates.[1]); @@ -80,9 +79,9 @@ type GameStateIO = { gameState : Game } - static member initialize(input: TextReader) : Game = - let id = Int32.Parse(input.ReadLine()) - let mapInfo: string = input.ReadLine() + static member initialize() : Game = + let id = Int32.Parse(Console.ReadLine()) + let mapInfo: string = Console.ReadLine() let mapInfoSplit: string[] = mapInfo.Split(" ") let mapWidth = Int32.Parse(mapInfoSplit.[0]) let mapHeight = Int32.Parse(mapInfoSplit.[1]) @@ -96,19 +95,19 @@ type GameStateIO = } gameState - member self.update(input: TextReader) = + member self.update() = let newPlayer0 = - Player(0, [], new Dictionary(), self.gameState.Players.[0].city_tile_count) + Player(0, [], new Dictionary(), self.gameState.Players.[0].CityTileCount) let newPlayer1 = - Player(1, [], new Dictionary(), self.gameState.Players.[1].city_tile_count) + Player(1, [], new Dictionary(), self.gameState.Players.[1].CityTileCount) let newGameState = { self.gameState with Turn = self.gameState.Turn + 1; Players = [newPlayer0; newPlayer1] - GameMap = GameMap(self.gameState.GameMap.height, self.gameState.GameMap.width) + GameMap = GameMap(self.gameState.GameMap.Height, self.gameState.GameMap.Width) } |> Seq.unfold (fun state -> - updateUnfold state input + updateUnfold state ) |> Seq.last { self with gameState = newGameState } diff --git a/kits/fsharp/simple/Bot/Lux/GameObjects.fs b/kits/fsharp/simple/Bot/Lux/GameObjects.fs index 7bff7186..0dfc80c5 100644 --- a/kits/fsharp/simple/Bot/Lux/GameObjects.fs +++ b/kits/fsharp/simple/Bot/Lux/GameObjects.fs @@ -25,7 +25,7 @@ type Position(_x, _y) = static member op_Equality (self: Position, pos: Position) : bool = self.x = pos.x && self.y = pos.y - member self.translate(direction, units): Position = + member self.Translate(direction, units): Position = match direction with | NORTH -> Position(self.x, self.y - units) @@ -50,7 +50,7 @@ type Position(_x, _y) = let closest_dist, closest_dir = check_dirs |> List.fold (fun (closest_dist, closest_dir) direction -> - let newpos = self.translate(direction, 1) + let newpos = self.Translate(direction, 1) let dist = target_pos.DistanceTo(newpos) if dist < closest_dist then dist, direction @@ -68,12 +68,15 @@ type CityTile(teamid, cityid, x, y, cooldown: float) = member self.Team = teamid member self.Pos = Position(x, y) member self.Cooldown = cooldown + /// Whether or not this unit can research or build member self.CanAct() : bool = self.Cooldown < 1.0 + /// returns command to ask this tile to research this turn member self.Research() : string = sprintf "r %d %d" self.Pos.x self.Pos.y + /// returns command to ask this tile to build a worker this turn member self.BuildWorker() : string = sprintf "bw %d %d" self.Pos.x self.Pos.y @@ -95,111 +98,108 @@ type Cell(x, y) = resource.amount > 0 type City(teamid: int, cityid: string, fuel: float, light_upkeep_init: float) = - member self.cityid = cityid - member self.team = teamid - member self.fuel = fuel + member self.CityId = cityid + member self.Team = teamid + member self.Fuel = fuel member val CityTiles: List = new List() with get, set - member self.light_upkeep = light_upkeep_init + member self.LightUpkeep = light_upkeep_init member self._add_city_tile(x, y, cooldown) = - let ct = CityTile(self.team, self.cityid, x, y, cooldown) + let ct = CityTile(self.Team, self.CityId, x, y, cooldown) self.CityTiles.Add(ct) Some ct - //member self.get_light_upkeep() = - // self.light_upkeep + member self.GetLightUpkeep() = + self.LightUpkeep type GameMap(width, height) = - member self.height = height - member self.width = width + member self.Height = height + member self.Width = width // member self.map: List[List[Cell]] = [None] * height - member val map: Cell[,] = Array2D.init width height (fun x y -> Cell(x,y)) + member val Map: Cell[,] = Array2D.init width height (fun x y -> Cell(x,y)) // for y in 0 .. self.height) do // self.map.[y] = [None] * width // for x in range(0, self.width) do // self.map[y][x] = Cell(x, y) member self.GetCellByPos(pos: Position) : Cell = - self.map.[pos.x, pos.y] + self.Map.[pos.x, pos.y] member self.GetCell(x, y) : Cell = - self.map.[x, y] + self.Map.[x, y] /// do not use this function, this is for internal tracking of state member self._setResource(r_type, x, y, amount) = self.GetCell(x, y).Resource <- Some(Resource(r_type, amount)) type Cargo(wood, coal, uranium) = - member self.wood = wood - member self.coal = coal - member self.uranium = uranium + member self.Wood = wood + member self.Coal = coal + member self.Uranium = uranium new() = Cargo(0, 0, 0) - member self.__str__() : string = - sprintf "Cargo | Wood: %d, Coal: %d, Uranium: %d" self.wood self.coal self.uranium + override self.ToString() : string = + sprintf "Cargo | Wood: %d, Coal: %d, Uranium: %d" self.Wood self.Coal self.Uranium -type Unit(teamid, u_type, unitid, x, y, cooldown, wood, coal, uranium) = +type Unit(teamid, uType, unitid, x, y, cooldown, wood, coal, uranium) = member val Pos = Position(x, y) - member val team: int = teamid with get, set - member val id: string = unitid with get, set - member private self.u_type = u_type - member self.cooldown = cooldown - member self.cargo : Cargo = new Cargo(wood, coal, uranium) + member val Team: int = teamid with get, set + member val Id: string = unitid with get, set + member self.Cooldown = cooldown + member self.Cargo : Cargo = new Cargo(wood, coal, uranium) + member private self.UnitType = uType member self.IsWorker() : bool = - self.u_type = UNIT_TYPES.WORKER + self.UnitType = UNIT_TYPES.WORKER member self.IsCart() : bool = - self.u_type = UNIT_TYPES.CART + self.UnitType = UNIT_TYPES.CART /// get cargo space left in this unit member self.GetCargoSpaceLeft() = - let spaceused = self.cargo.wood + self.cargo.coal + self.cargo.uranium - if self.u_type = UNIT_TYPES.WORKER then + let spaceused = self.Cargo.Wood + self.Cargo.Coal + self.Cargo.Uranium + if self.UnitType = UNIT_TYPES.WORKER then GAME_CONSTANTS.Parameters.ResourceCapacity.Worker - spaceused - //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["WORKER"] - spaceused else GAME_CONSTANTS.Parameters.ResourceCapacity.Cart - spaceused - //GAME_CONSTANTS["PARAMETERS"]["RESOURCE_CAPACITY"]["CART"] - spaceused /// whether or not the unit can build where it is right now - member self.CanBuild(game_map: GameMap) : bool = - let cell = game_map.GetCellByPos(self.Pos) + member self.CanBuild(gameMap: GameMap) : bool = + let cell = gameMap.GetCellByPos(self.Pos) not (cell.HasResource()) && self.CanAct() - && (self.cargo.wood + self.cargo.coal + self.cargo.uranium) >= GAME_CONSTANTS.Parameters.CityBuildCost + && (self.Cargo.Wood + self.Cargo.Coal + self.Cargo.Uranium) >= GAME_CONSTANTS.Parameters.CityBuildCost /// whether or not the unit can move or not. This does not check for potential collisions into other units or enemy cities member self.CanAct() : bool = - self.cooldown < 1.0 + self.Cooldown < 1.0 /// return the command to move unit in the given direction member self.Move(dir: DIRECTIONS) : string = - sprintf "m %s %s" self.id (dir.ToString()) - // $"m {self.id} {dir}" + sprintf "m %s %s" self.Id (dir.ToString()) /// return the command to transfer a resource from a source unit to a destination unit as specified by their ids member self.Transfer(dest_id, resourceType, amount) : string = - sprintf "t %s %d %d %d" self.id dest_id resourceType amount + sprintf "t %s %d %d %d" self.Id dest_id resourceType amount /// return the command to build a city right under the worker member self.BuildCity() : string = - sprintf "bcity %s" self.id + sprintf "bcity %s" self.Id /// return the command to pillage whatever is underneath the worker - member self.pillage() : string = - sprintf "p %s" self.id + member self.Pillage() : string = + sprintf "p %s" self.Id type Player(team, units, cities, tilecount) = new(teamId) = Player(teamId, [], new Dictionary(), 0) - member val team: int = team with get, set - member val research_points = 0 with get, set + member val Team: int = team with get, set + member val ResearchPoints = 0 with get, set member val Units: list = units with get, set member val Cities: Dictionary = cities - member val city_tile_count: int = tilecount with get, set + member val CityTileCount: int = tilecount with get, set member self.ResearchedCoal() : bool = - self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Coal + self.ResearchPoints >= GAME_CONSTANTS.Parameters.ResearchRequirements.Coal member self.ResearchedUranium() : bool = - self.research_points >= GAME_CONSTANTS.Parameters.ResearchRequirements.Uranium + self.ResearchPoints >= GAME_CONSTANTS.Parameters.ResearchRequirements.Uranium type Game = { diff --git a/kits/fsharp/simple/Bot/Main.fs b/kits/fsharp/simple/Bot/Main.fs index 6c8acdb5..963ce9fb 100644 --- a/kits/fsharp/simple/Bot/Main.fs +++ b/kits/fsharp/simple/Bot/Main.fs @@ -1,30 +1,15 @@ open System -open System.IO open Lux.Agent -type PipedReader(input: TextReader, output: StreamWriter) = - inherit TextReader() - member val reader = input - member val writer = output - with - override self.ReadLine() = - let input = self.reader.ReadLine() - self.writer.WriteLine(input) - input - [] let main args = - let inputStream = Console.In - let logStream = StreamWriter($"logInput_{Random().Next()}.txt") - let pipedStream = PipedReader(inputStream, logStream) - let agent: GameStateIO = { gameState = GameStateIO.initialize(pipedStream) } - //System.Threading.Thread.Sleep(10000) - List.unfold (fun (agent: GameStateIO) -> - let game = agent.update(pipedStream) + let agent: GameStateIO = { gameState = GameStateIO.initialize() } + while true do + let game = agent.update() (* Set actions *) let actions : seq = - Bot.makeActions game.gameState + Bot.takeActions game.gameState (* AI Code Goes Above! *) @@ -33,7 +18,4 @@ let main args = String.concat "," actions Console.WriteLine(command) agent.endTurn() - Some ((), agent) - ) agent - |> ignore 0 \ No newline at end of file