summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LivingVillage.sln34
-rw-r--r--src/LivingVillage.Desktop/Game.fs149
-rw-r--r--src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj21
-rw-r--r--src/LivingVillage.Desktop/Program.fs9
-rw-r--r--src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj11
-rw-r--r--src/LivingVillage.Kernel/Sim.fs46
6 files changed, 270 insertions, 0 deletions
diff --git a/LivingVillage.sln b/LivingVillage.sln
new file mode 100644
index 0000000..4e8f210
--- /dev/null
+++ b/LivingVillage.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LivingVillage.Kernel", "src\LivingVillage.Kernel\LivingVillage.Kernel.fsproj", "{504BBDD4-106D-4423-A3BE-9A82848D1656}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LivingVillage.Desktop", "src\LivingVillage.Desktop\LivingVillage.Desktop.fsproj", "{CC15DA38-E36A-45A2-A031-5392C8A6BF0D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {504BBDD4-106D-4423-A3BE-9A82848D1656} = {DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D} = {DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}
+ EndGlobalSection
+EndGlobal
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
new file mode 100644
index 0000000..960a8c6
--- /dev/null
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -0,0 +1,149 @@
+namespace LivingVillage.Desktop
+
+open System
+open Microsoft.Xna.Framework
+open Microsoft.Xna.Framework.Graphics
+open Microsoft.Xna.Framework.Input
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+
+type TileKind =
+ | Grass = 0
+ | Path = 1
+ | Water = 2
+ | Flower = 3
+
+module MapGen =
+ let generate (width: int) (height: int) : TileKind[,] =
+ let rnd = Random(20260918)
+ let map = Array2D.zeroCreate<TileKind> width height
+ for y in 0 .. height - 1 do
+ for x in 0 .. width - 1 do
+ let border = x = 0 || y = 0 || x = width - 1 || y = height - 1
+ map.[x, y] <-
+ if border then TileKind.Water
+ elif rnd.NextDouble() < 0.04 then TileKind.Flower
+ else TileKind.Grass
+ let midX = width / 2
+ let midY = height / 2
+ for x in 1 .. width - 2 do
+ map.[x, midY] <- TileKind.Path
+ for y in 1 .. height - 2 do
+ map.[midX, y] <- TileKind.Path
+ map
+
+module Atlas =
+ let tilePixels = 32
+
+ let build (device: GraphicsDevice) : Texture2D =
+ let rnd = Random(7)
+ let c (r: int) (g: int) (b: int) = Color(r, g, b)
+ let tex = new Texture2D(device, tilePixels * 4, tilePixels)
+ let colors = Array.zeroCreate<Color> (tilePixels * tilePixels * 4)
+ for t in 0 .. 3 do
+ let baseColor, variance, isFlower =
+ match t with
+ | 1 -> (c 148 118 84), 12, false
+ | 2 -> (c 52 92 168), 10, false
+ | 3 -> (c 56 118 48), 14, true
+ | _ -> (c 56 118 48), 14, false
+ for y in 0 .. tilePixels - 1 do
+ for x in 0 .. tilePixels - 1 do
+ let jitter = rnd.Next(variance + 1) - variance / 2
+ colors.[t * tilePixels * tilePixels + y * tilePixels + x] <-
+ c (int baseColor.R + jitter) (int baseColor.G + jitter) (int baseColor.B + jitter)
+ if isFlower then
+ for _ in 1 .. 6 do
+ let fx = rnd.Next(tilePixels - 2)
+ let fy = rnd.Next(tilePixels - 2)
+ let fc = if rnd.NextDouble() < 0.5 then c 240 220 80 else c 235 240 245
+ for dy in 0 .. 1 do
+ for dx in 0 .. 1 do
+ colors.[t * tilePixels * tilePixels + (fy + dy) * tilePixels + fx + dx] <- fc
+ tex.SetData(colors)
+ tex
+
+type LivingVillageGame() as this =
+ inherit Game()
+
+ let graphics = new GraphicsDeviceManager(this)
+ let mutable spriteBatch = Unchecked.defaultof<SpriteBatch>
+ let mutable atlas = Unchecked.defaultof<Texture2D>
+ let mutable pixel = Unchecked.defaultof<Texture2D>
+ let mutable map = MapGen.generate Sim.mapWidthTiles Sim.mapHeightTiles
+ let mutable world = Sim.initialWorld ()
+ let mutable camera = Vector2.Zero
+ let mutable fpsFrames = 0
+ let mutable fpsSeconds = 0.0
+
+ do
+ graphics.PreferredBackBufferWidth <- 1280
+ graphics.PreferredBackBufferHeight <- 720
+ graphics.SynchronizeWithVerticalRetrace <- true
+ this.IsFixedTimeStep <- true
+ this.TargetElapsedTime <- TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 60L)
+ this.Window.Title <- "Living Village M0"
+
+ member private this.CenterCamera() =
+ let viewport = this.GraphicsDevice.Viewport
+ let vw = float32 viewport.Width
+ let vh = float32 viewport.Height
+ let worldW = float32 (Sim.mapWidthTiles * Sim.tilePixels)
+ let worldH = float32 (Sim.mapHeightTiles * Sim.tilePixels)
+ let half = float32 Sim.tilePixels / 2.0f
+ let x = MathHelper.Clamp(world.Avatar.Pos.X + half - vw / 2.0f, 0.0f, max 0.0f (worldW - vw))
+ let y = MathHelper.Clamp(world.Avatar.Pos.Y + half - vh / 2.0f, 0.0f, max 0.0f (worldH - vh))
+ camera <- Vector2(x, y)
+
+ override _.LoadContent() =
+ spriteBatch <- new SpriteBatch(this.GraphicsDevice)
+ atlas <- Atlas.build this.GraphicsDevice
+ pixel <- new Texture2D(this.GraphicsDevice, 1, 1)
+ pixel.SetData([| Color.White |])
+
+ override this.Update(gameTime: GameTime) =
+ let kb = Keyboard.GetState()
+ if kb.IsKeyDown(Keys.Escape) then this.Exit()
+ let input =
+ { MoveX =
+ (if kb.IsKeyDown(Keys.D) then 1.0f
+ elif kb.IsKeyDown(Keys.A) then -1.0f
+ else 0.0f)
+ MoveY =
+ (if kb.IsKeyDown(Keys.S) then 1.0f
+ elif kb.IsKeyDown(Keys.W) then -1.0f
+ else 0.0f) }
+ let ts =
+ { Dt = float32 gameTime.ElapsedGameTime.TotalSeconds
+ Input = input }
+ world <- Sim.step ts world
+ this.CenterCamera()
+ fpsFrames <- fpsFrames + 1
+ fpsSeconds <- fpsSeconds + gameTime.ElapsedGameTime.TotalSeconds
+ if fpsSeconds >= 1.0 then
+ let fps = float fpsFrames / fpsSeconds
+ fpsFrames <- 0
+ fpsSeconds <- 0.0
+ this.Window.Title <- $"Living Village M0 | fps {fps:F1} | tick {world.Tick}"
+ printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})"
+
+ override this.Draw(gameTime: GameTime) =
+ this.GraphicsDevice.Clear(Color.CornflowerBlue)
+ let viewport = this.GraphicsDevice.Viewport
+ let camX = int camera.X
+ let camY = int camera.Y
+ let tx0 = max 0 (camX / Sim.tilePixels)
+ let ty0 = max 0 (camY / Sim.tilePixels)
+ let tx1 = min (Sim.mapWidthTiles - 1) ((camX + viewport.Width) / Sim.tilePixels + 1)
+ let ty1 = min (Sim.mapHeightTiles - 1) ((camY + viewport.Height) / Sim.tilePixels + 1)
+ spriteBatch.Begin()
+ for y in ty0 .. ty1 do
+ for x in tx0 .. tx1 do
+ let src = Rectangle(int map.[x, y] * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
+ let dst =
+ Rectangle(x * Sim.tilePixels - camX, y * Sim.tilePixels - camY, Sim.tilePixels, Sim.tilePixels)
+ spriteBatch.Draw(atlas, dst, src, Color.White)
+ let avatarDst =
+ Rectangle(int world.Avatar.Pos.X - camX, int world.Avatar.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels)
+ spriteBatch.Draw(pixel, avatarDst, Color.IndianRed)
+ spriteBatch.End()
diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
new file mode 100644
index 0000000..9af59ac
--- /dev/null
+++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="Game.fs" />
+ <Compile Include="Program.fs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.5.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LivingVillage.Kernel\LivingVillage.Kernel.fsproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/LivingVillage.Desktop/Program.fs b/src/LivingVillage.Desktop/Program.fs
new file mode 100644
index 0000000..0e2adcf
--- /dev/null
+++ b/src/LivingVillage.Desktop/Program.fs
@@ -0,0 +1,9 @@
+namespace LivingVillage.Desktop
+
+module Program =
+
+ [<EntryPoint>]
+ let main _ =
+ use game = new LivingVillageGame()
+ game.Run()
+ 0
diff --git a/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj b/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj
new file mode 100644
index 0000000..a8ed312
--- /dev/null
+++ b/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="Sim.fs" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs
new file mode 100644
index 0000000..d1b2e57
--- /dev/null
+++ b/src/LivingVillage.Kernel/Sim.fs
@@ -0,0 +1,46 @@
+namespace LivingVillage.Kernel
+
+module Sim =
+
+ type Vec2 =
+ { X: float32
+ Y: float32 }
+
+ type Avatar =
+ { Pos: Vec2 }
+
+ type Input =
+ { MoveX: float32
+ MoveY: float32 }
+
+ type TimeStep =
+ { Dt: float32
+ Input: Input }
+
+ type World =
+ { Tick: int64
+ Avatar: Avatar }
+
+ let mapWidthTiles = 64
+ let mapHeightTiles = 48
+ let tilePixels = 32
+ let avatarSpeed = 160.0f
+
+ let initialWorld () : World =
+ let centerX = float32 (mapWidthTiles * tilePixels / 2 - tilePixels / 2)
+ let centerY = float32 (mapHeightTiles * tilePixels / 2 - tilePixels / 2)
+ { Tick = 0L
+ Avatar = { Pos = { X = centerX; Y = centerY } } }
+
+ let clamp v lo hi = if v < lo then lo elif v > hi then hi else v
+
+ let step (ts: TimeStep) (world: World) : World =
+ let maxX = float32 (mapWidthTiles * tilePixels - tilePixels)
+ let maxY = float32 (mapHeightTiles * tilePixels - tilePixels)
+ let dx = ts.Input.MoveX * avatarSpeed * ts.Dt
+ let dy = ts.Input.MoveY * avatarSpeed * ts.Dt
+ { Tick = world.Tick + 1L
+ Avatar =
+ { Pos =
+ { X = clamp (world.Avatar.Pos.X + dx) 0.0f maxX
+ Y = clamp (world.Avatar.Pos.Y + dy) 0.0f maxY } } }