Creating an Initial Game Board

(Part 5 of the Panzer General Portable Project)

The first game-like task I wanted to accomplish was to set up the base game board – like I did here seven years ago.  I fired up the F# Xamarin Forms (XF) project I was working on last post and open the app.fs file.  Following the “Full-On Monty” Elm pattern, I am not going to do any of this app in markup so there are no XAML files to deal with.

Inside the app.fs file, I first created a record type to keep track of the tiles and units for each hex (later in this series, I will create a proper domain model – for now this is a just a spike to see what it takes to render the game board – I named it ContentData).  Notice that ContentData always had a Tile, Row and Column, but it does not always have a unit on it – so I used an option type. for Unit.  I also needed a type for the Tile, the Unit, and the Equipment – starting with image then moving out to additional data to make the game work.  Enter type providers – all I needed to do was to point to the files that were already part of the solution to get the types

1 type TileContext = JsonProvider<"Data//Scenario_Tile.json"> 2 type UnitContext = JsonProvider<"Data//Scenario_Unit.json"> 3 type EquipmentContext = JsonProvider<"Data//Equipment.json"> 4 type ContentData = {TileId: int; ColumnNumber: int; RowNumber: int; UnitId: int option} 5

I then created three functions to get the data for each instance of those types

1 let getTiles (scenarioId:int) = 2 let assembly = IntrospectionExtensions.GetTypeInfo(typeof<App>).Assembly 3 let stream = assembly.GetManifestResourceStream("scenariotile"); 4 let reader = new StreamReader(stream) 5 let json = reader.ReadToEnd() 6 let scenarioTile = TileContext.Parse(json) 7 scenarioTile.Dataroot.ScenarioTile 8 |> Array.filter(fun st -> st.ScenarioId = scenarioId) 9 10 let getUnits (scenarioId: int) = 11 let assembly = IntrospectionExtensions.GetTypeInfo(typeof<App>).Assembly 12 let stream = assembly.GetManifestResourceStream("scenariounit"); 13 let reader = new StreamReader(stream) 14 let json = reader.ReadToEnd() 15 let unit = UnitContext.Parse(json) 16 unit.Dataroot.ScenarioUnit 17 |> Array.filter(fun su -> su.ScenarioId = scenarioId) 18 19 let getEquipments = 20 let assembly = IntrospectionExtensions.GetTypeInfo(typeof<App>).Assembly 21 let stream = assembly.GetManifestResourceStream("equipment"); 22 let reader = new StreamReader(stream) 23 let json = reader.ReadToEnd() 24 let equipment = EquipmentContext.Parse(json) 25 equipment.Dataroot.Equipment

You first thought is “this screams refactoring to a high order function” and I would agree.  I will do that in next iteration

In any event, I needed a way to create the actual image from the file path and then a way of hydrating the ContentData

1 let createImage path = 2 let image = new Image() 3 image.Source <- ImageSource.FromResource(path) 4 image 5 6 let createTileContentData (tile:TileContext.ScenarioTile) (units: UnitContext.ScenarioUnit seq) (equipments: EquipmentContext.Equipment seq) = 7 let scenarioUnit = 8 units 9 |> Seq.tryFind(fun u -> u.StartingScenarioTileId = tile.ScenarioTileId) 10 let unitId = 11 match scenarioUnit with 12 | Some u -> 13 let equipment = equipments |> Seq.find(fun e -> e.EquipmentId = u.EquipmentId) 14 Some equipment.Icon 15 | None -> None 16 17 {TileId = tile.TerrainId; 18 ColumnNumber = tile.ColumnNumber; 19 RowNumber = tile.RowNumber; 20 UnitId = unitId}

With this function ready to return the image, I needed a way to put it in the correct place on the board.  In XF, this is accomplished via the Rectangle type

1 let createRectangle columnIndex rowIndex scale = 2 let height = 50.0 * scale 3 let width = 60.0 * scale 4 let yOffsetPlug = 25.0 * scale 5 let xOffsetPlug = -15.0 * scale 6 let columnIndex' = float columnIndex 7 let rowIndex' = float rowIndex 8 let yOffset = 9 match columnIndex % 2 = 0 with 10 | true -> yOffsetPlug 11 | false -> yOffsetPlug + yOffsetPlug 12 let xOffset = (columnIndex' * xOffsetPlug) + xOffsetPlug 13 let x = xOffset + columnIndex' * width 14 let y = yOffset + rowIndex' * height 15 new Rectangle(x,y,width,height)

With the individual hex functions ready, I was ready to put them onto the screen.  In XF, there is a type called layout that seems what I need to position images in absolute positions

1 let createLayout numberOfTiles scale = 2 let width = 60.0 * scale 3 let height = 50.0 * scale 4 let layout = new AbsoluteLayout() 5 layout.WidthRequest <- 12.0 * width 6 let rows = numberOfTiles % 20 |> float 7 layout.HeightRequest <- rows * height 8 layout

With the layout ready, I could create a function that takes in the layout, an individual hex’s contentdata, find it in the image files, and put it on the board.  I used the Children property of the Layout type to push multiple images on the same hex – I am not sure about Z-ordering yet, so I put the unit image after the tile

1 let addTileContent (layout:AbsoluteLayout) (contentData: ContentData) (scale: float) = 2 let rectangle = createRectangle contentData.ColumnNumber contentData.RowNumber scale 3 let terrainImageLocator = "tacmapdry" + contentData.TileId.ToString() 4 layout.Children.Add(createImage terrainImageLocator, rectangle) 5 match contentData.UnitId with 6 |Some i -> 7 let unitImageLocator = "tacicons" + i.ToString() 8 layout.Children.Add(createImage unitImageLocator, rectangle) 9 |None -> ()

With the individual hex functions ready, I could iterate through the first scenario and populate its initial board.  Notice that I put the Content control inside a ScrollView type that then put into a ContentPage that is then put into the Page.  I think the common convention is right – UX is very much OO in nature

1 let populateBoard = 2 let tiles = getTiles 0 3 let units = getUnits 0 4 let equipments = getEquipments 5 let numberOfTiles = tiles |> Seq.length 6 let scale = 1.0 7 let layout = createLayout numberOfTiles scale 8 tiles 9 |> Array.map(fun t -> createTileContentData t units equipments) 10 |> Array.iter(fun cd -> addTileContent layout cd scale) 11 let scrollView = new ScrollView() 12 scrollView.Orientation <- ScrollOrientation.Both 13 scrollView.Content <- layout 14 base.MainPage <- ContentPage(Content = scrollView) 15 16 do 17 populateBoard

So when I fired up the emulator, I got some pretty good first results

Screen Shot 2018-11-08 at 8.22.57 PM

Out of the box with Win Phone, I could pinch and spread a location and the screen images would automatically adjust and get bigger/smaller.  No such luck with XF – so I need to see how to do that.  In any event, zooming into the bottom left corner, all Panzer General fans will immediate recognize this layout. 

image

And it looks pretty good compared to the winphone

image

and the original game

image

Lots to do, but I am happy with the progress

Gist is here

2 Responses to Creating an Initial Game Board

  1. Pingback: F# Weekly #48, 2018 – F# Advent starts tomorrow! – Sergey Tihon's Blog

  2. Pingback: Dew Drop - December 3, 2018 (#2851) - Morning Dew

Leave a comment