From d4fb8b0d45a33915e3a65dfed98cfb3defe3dfd7 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sun, 17 Dec 2023 15:12:42 -0800 Subject: [PATCH] pixijs --- src/Swarm/Game/Scenario/Topography/Center.hs | 7 +- src/Swarm/Game/World/Render.hs | 62 ++++++++++-------- src/Swarm/TUI/View.hs | 2 +- src/Swarm/Web.hs | 32 +++++++++ src/Swarm/Web/Worldview.hs | 59 +++++++++++++++++ .../Swarm/Util/OccurrenceEncoder.hs | 41 ++++++++++++ swarm.cabal | 4 +- web/handwritten-logo.png | Bin 0 -> 31484 bytes web/index.html | 2 + web/play.html | 32 +++++++++ web/script/display.js | 15 +++++ web/script/fetch.js | 62 ++++++++++++++++++ 12 files changed, 288 insertions(+), 30 deletions(-) create mode 100644 src/Swarm/Web/Worldview.hs create mode 100644 src/swarm-util/Swarm/Util/OccurrenceEncoder.hs create mode 100644 web/handwritten-logo.png create mode 100644 web/play.html create mode 100644 web/script/display.js create mode 100644 web/script/fetch.js diff --git a/src/Swarm/Game/Scenario/Topography/Center.hs b/src/Swarm/Game/Scenario/Topography/Center.hs index 2c4ff4f5bb..bee014eb11 100644 --- a/src/Swarm/Game/Scenario/Topography/Center.hs +++ b/src/Swarm/Game/Scenario/Topography/Center.hs @@ -15,11 +15,14 @@ import Swarm.Game.Scenario (Scenario) import Swarm.Game.State (SubworldDescription, genRobotTemplates) import Swarm.Game.Universe (Cosmic (..), SubworldName (DefaultRootSubworld)) -determineViewCenter :: +-- | Determine view center for a static map +-- without reference to a 'GameState' +-- (i.e. outside the context of an active game) +determineStaticViewCenter :: Scenario -> NonEmpty SubworldDescription -> Cosmic Location -determineViewCenter s worldTuples = +determineStaticViewCenter s worldTuples = fromMaybe defaultVC baseRobotLoc where theRobots = genRobotTemplates s worldTuples diff --git a/src/Swarm/Game/World/Render.hs b/src/Swarm/Game/World/Render.hs index 11849b4122..07c3019d6e 100644 --- a/src/Swarm/Game/World/Render.hs +++ b/src/Swarm/Game/World/Render.hs @@ -19,7 +19,7 @@ import Swarm.Game.Display (defaultChar) import Swarm.Game.Entity.Cosmetic import Swarm.Game.Location import Swarm.Game.ResourceLoading (initNameGenerator, readAppData) -import Swarm.Game.Scenario (Scenario, area, loadStandaloneScenario, scenarioCosmetics, scenarioWorlds, ul, worldName) +import Swarm.Game.Scenario (PWorldDescription, Scenario, area, loadStandaloneScenario, scenarioCosmetics, scenarioWorlds, ul, worldName) import Swarm.Game.Scenario.Status (seedLaunchParams) import Swarm.Game.Scenario.Topography.Area import Swarm.Game.Scenario.Topography.Cell @@ -85,43 +85,49 @@ fromHiFi = fmap $ \case -- those triples are not inputs to the VTY attribute creation. AnsiColor x -> namedToTriple x --- | When output size is not explicitly provided on command line, +-- | When output size is not explicitly provided, -- uses natural map bounds (if a map exists). +getBoundingBox :: + Location -> + PWorldDescription e -> + Maybe AreaDimensions -> + W.BoundsRectangle +getBoundingBox vc scenarioWorld maybeSize = + mkBoundingBox areaDims upperLeftLocation + where + upperLeftLocation = + if null maybeSize && not (isEmpty mapAreaDims) + then ul scenarioWorld + else vc .+^ ((`div` 2) <$> V2 (negate w) h) + + mkBoundingBox areaDimens upperLeftLoc = + both W.locToCoords locationBounds + where + lowerRightLocation = upperLeftToBottomRight areaDimens upperLeftLoc + locationBounds = (upperLeftLoc, lowerRightLocation) + + worldArea = area scenarioWorld + mapAreaDims = getAreaDimensions worldArea + areaDims@(AreaDimensions w h) = + fromMaybe (AreaDimensions 20 10) $ + maybeSize <|> surfaceEmpty isEmpty mapAreaDims + getDisplayGrid :: + Location -> Scenario -> GameState -> Maybe AreaDimensions -> - Grid (PCell EntityFacade) -getDisplayGrid myScenario gs maybeSize = + Grid CellPaintDisplay +getDisplayGrid vc myScenario gs maybeSize = getMapRectangle mkFacade (getContentAt worlds . mkCosmic) - (mkBoundingBox areaDims upperLeftLocation) + (getBoundingBox vc firstScenarioWorld maybeSize) where mkCosmic = Cosmic $ worldName firstScenarioWorld - worlds = view (landscape . multiWorld) gs - worldTuples = buildWorldTuples myScenario - vc = determineViewCenter myScenario worldTuples - firstScenarioWorld = NE.head $ view scenarioWorlds myScenario - worldArea = area firstScenarioWorld - mapAreaDims = getAreaDimensions worldArea - areaDims@(AreaDimensions w h) = - fromMaybe (AreaDimensions 20 10) $ - maybeSize <|> surfaceEmpty isEmpty mapAreaDims - - upperLeftLocation = - if null maybeSize && not (isEmpty mapAreaDims) - then ul firstScenarioWorld - else view planar vc .+^ ((`div` 2) <$> V2 (negate w) h) - - mkBoundingBox areaDimens upperLeftLoc = - both W.locToCoords locationBounds - where - lowerRightLocation = upperLeftToBottomRight areaDimens upperLeftLoc - locationBounds = (upperLeftLoc, lowerRightLocation) getRenderableGrid :: RenderOpts -> @@ -138,7 +144,11 @@ getRenderableGrid (RenderOpts maybeSeed _ _ maybeSize) fp = simpleErrorHandle $ myScenario (seedLaunchParams maybeSeed) gsc - return (getDisplayGrid myScenario gs maybeSize, myScenario ^. scenarioCosmetics) + let vc = + view planar $ + determineStaticViewCenter myScenario $ + buildWorldTuples myScenario + return (getDisplayGrid vc myScenario gs maybeSize, myScenario ^. scenarioCosmetics) doRenderCmd :: RenderOpts -> FilePath -> IO () doRenderCmd opts@(RenderOpts _ asPng _ _) mapPath = diff --git a/src/Swarm/TUI/View.hs b/src/Swarm/TUI/View.hs index 32fb588ffc..6421c4f208 100644 --- a/src/Swarm/TUI/View.hs +++ b/src/Swarm/TUI/View.hs @@ -243,7 +243,7 @@ drawNewGameMenuUI (l :| ls) launchOptions = case displayedFor of , padTop (Pad 1) table ] where - vc = determineViewCenter s worldTuples + vc = determineStaticViewCenter s worldTuples worldTuples = buildWorldTuples s theWorlds = genMultiWorld worldTuples $ fromMaybe 0 $ s ^. scenarioSeed diff --git a/src/Swarm/Web.hs b/src/Swarm/Web.hs index 2b9dfe82f1..12fe522c84 100644 --- a/src/Swarm/Web.hs +++ b/src/Swarm/Web.hs @@ -67,6 +67,7 @@ import Swarm.Game.Robot import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Objective.Graph import Swarm.Game.Scenario.Objective.WinCheck +import Swarm.Game.Scenario.Topography.Area (AreaDimensions (..)) import Swarm.Game.Scenario.Topography.Structure.Recognition import Swarm.Game.Scenario.Topography.Structure.Recognition.Log import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry @@ -83,6 +84,7 @@ import Swarm.TUI.Model import Swarm.TUI.Model.Goal import Swarm.TUI.Model.UI import Swarm.Util.RingBuffer +import Swarm.Web.Worldview import System.Timeout (timeout) import Text.Read (readEither) import WaiAppStatic.Types (unsafeToPiece) @@ -108,6 +110,7 @@ type SwarmAPI = :<|> "code" :> "run" :> ReqBody '[PlainText] T.Text :> Post '[PlainText] T.Text :<|> "paths" :> "log" :> Get '[JSON] (RingBuffer CacheLogEntry) :<|> "repl" :> "history" :> "full" :> Get '[JSON] [REPLHistItem] + :<|> "map" :> Capture "size" AreaDimensions :> Get '[JSON] GridResponse swarmApi :: Proxy SwarmAPI swarmApi = Proxy @@ -162,6 +165,7 @@ mkApp state events = :<|> codeRunHandler events :<|> pathsLogHandler state :<|> replHandler state + :<|> mapViewHandler state robotsHandler :: ReadableIORef AppState -> Handler [Robot] robotsHandler appStateRef = do @@ -244,6 +248,18 @@ replHandler appStateRef = do items = toList replHistorySeq pure items +mapViewHandler :: ReadableIORef AppState -> AreaDimensions -> Handler GridResponse +mapViewHandler appStateRef areaSize = do + appState <- liftIO (readIORef appStateRef) + let maybeScenario = fst <$> appState ^. uiState . scenarioRef + pure $ case maybeScenario of + Just s -> + GridResponse True + . Just + . getCellGrid s (appState ^. gameState) + $ areaSize + Nothing -> GridResponse False Nothing + -- ------------------------------------------------------------------ -- Main app (used by service and for development) -- ------------------------------------------------------------------ @@ -338,3 +354,19 @@ instance ToCapture (Capture "id" RobotID) where SD.DocCapture "id" -- name "(integer) robot ID" -- description + +instance FromHttpApiData AreaDimensions where + parseUrlPiece x = left T.pack $ do + pieces <- mapM (readEither . T.unpack) $ T.splitOn "x" x + case pieces of + [w, h] -> return $ AreaDimensions w h + _ -> Left "Need two dimensions" + +instance SD.ToSample AreaDimensions where + toSamples _ = SD.samples [AreaDimensions 20 30] + +instance ToCapture (Capture "size" AreaDimensions) where + toCapture _ = + SD.DocCapture + "size" -- name + "(integer, integer) dimensions of area" -- description diff --git a/src/Swarm/Web/Worldview.hs b/src/Swarm/Web/Worldview.hs new file mode 100644 index 0000000000..ceb5ef9ad8 --- /dev/null +++ b/src/Swarm/Web/Worldview.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.Web.Worldview where + +import Control.Lens ((^.)) +import Control.Monad.Trans.State +import Data.Aeson (ToJSON) +import Data.Colour.Palette.BrewerSet (Kolor) +import Data.Colour.SRGB (RGB (..), sRGB24, sRGB24show) +import Data.IntMap qualified as IM +import Data.Text qualified as T +import GHC.Generics (Generic) +import Servant.Docs qualified as SD +import Swarm.Game.Entity.Cosmetic (RGBColor, flattenBg) +import Swarm.Game.Scenario (Scenario, scenarioCosmetics) +import Swarm.Game.Scenario.Style +import Swarm.Game.Scenario.Topography.Area (AreaDimensions (..), Grid) +import Swarm.Game.State (GameState, robotInfo) +import Swarm.Game.State.Robot (viewCenter) +import Swarm.Game.Universe (planar) +import Swarm.Game.World.Render +import Swarm.TUI.View.CellDisplay (getTerrainEntityColor) +import Swarm.Util.OccurrenceEncoder + +data GridResponse = GridResponse + { isPlaying :: Bool + , grid :: Maybe CellGrid + } + deriving (Generic, ToJSON) + +getCellGrid :: + Scenario -> + GameState -> + AreaDimensions -> + CellGrid +getCellGrid myScenario gs requestedSize = + CellGrid indexGrid $ getIndices encoding + where + vc = gs ^. robotInfo . viewCenter + dg = getDisplayGrid (vc ^. planar) myScenario gs (Just requestedSize) + aMap = myScenario ^. scenarioCosmetics + + asColour :: RGBColor -> Kolor + asColour (RGB r g b) = sRGB24 r g b + asHex = HexColor . T.pack . sRGB24show . asColour + + f = asHex . maybe (RGB 0 0 0) (flattenBg . fromHiFi) . getTerrainEntityColor aMap + (indexGrid, encoding) = runState (mapM (encodeOccurrence . f) dg) emptyEncoder + +data CellGrid = CellGrid + { coords :: Grid IM.Key + , colors :: [HexColor] + } + deriving (Generic, ToJSON) + +instance SD.ToSample GridResponse where + toSamples _ = SD.noSamples diff --git a/src/swarm-util/Swarm/Util/OccurrenceEncoder.hs b/src/swarm-util/Swarm/Util/OccurrenceEncoder.hs new file mode 100644 index 0000000000..4ada5c3dc7 --- /dev/null +++ b/src/swarm-util/Swarm/Util/OccurrenceEncoder.hs @@ -0,0 +1,41 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.Util.OccurrenceEncoder ( + Encoder, + encodeOccurrence, + getIndices, + emptyEncoder, +) where + +import Control.Monad.Trans.State +import Data.List (sortOn) +import Data.Map (Map) +import Data.Map qualified as M + +type OccurrenceEncoder a = State (Encoder a) + +newtype Encoder a = Encoder (Map a Int) + +emptyEncoder :: Ord a => Encoder a +emptyEncoder = Encoder mempty + +-- | Map indices are guaranteed to be contiguous +-- from @[0..N]@, so we may convert to a list +-- with no loss of information. +getIndices :: Encoder a -> [a] +getIndices (Encoder m) = map fst $ sortOn snd $ M.toList m + +-- | Translate each the first occurrence in the structure +-- to a new integer as it is encountered. +-- Subsequent encounters re-use the allocated integer. +encodeOccurrence :: Ord a => a -> OccurrenceEncoder a Int +encodeOccurrence c = do + Encoder currentMap <- get + maybe (cacheNewIndex currentMap) return $ + M.lookup c currentMap + where + cacheNewIndex currentMap = do + put $ Encoder $ M.insert c newIdx currentMap + return newIdx + where + newIdx = M.size currentMap diff --git a/swarm.cabal b/swarm.cabal index b738aeb69c..636af0b94b 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -103,6 +103,7 @@ library swarm-util Swarm.Util Swarm.Util.Erasable Swarm.Util.Lens + Swarm.Util.OccurrenceEncoder Swarm.Util.Parse Swarm.Util.RingBuffer Swarm.Util.UnitInterval @@ -288,18 +289,19 @@ library Swarm.Util.Effect Swarm.Version Swarm.Web + Swarm.Web.Worldview reexported-modules: Control.Carrier.Accum.FixedStrict , Data.BoolExpr.Simplify , Swarm.Util , Swarm.Util.Erasable , Swarm.Util.Lens + , Swarm.Util.OccurrenceEncoder , Swarm.Util.Parse , Swarm.Util.RingBuffer , Swarm.Util.UnitInterval , Swarm.Util.WindowedCounter , Swarm.Util.Yaml - other-modules: Paths_swarm autogen-modules: Paths_swarm diff --git a/web/handwritten-logo.png b/web/handwritten-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1b77c9a1a75ec066518c757e05e52d7d696f0111 GIT binary patch literal 31484 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 literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index 3293ce3d84..34bbb0a472 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,9 @@ Swarm server +

Hello Swarm player!

Looking for the Web API docs?

+

Or an experimental web frontend for the game?

\ No newline at end of file diff --git a/web/play.html b/web/play.html new file mode 100644 index 0000000000..3864ee2f2f --- /dev/null +++ b/web/play.html @@ -0,0 +1,32 @@ + + + + Web frontend + + + + + + + + + + + \ No newline at end of file diff --git a/web/script/display.js b/web/script/display.js new file mode 100644 index 0000000000..c90253277a --- /dev/null +++ b/web/script/display.js @@ -0,0 +1,15 @@ +function setupGraphics(button, displayWidth, displayHeight) { + + // Create the application helper and add its render target to the page + let app = new PIXI.Application({ + width: displayWidth, + height: displayHeight, + backgroundColor: 0xFFFFFF + }); + + document.body.insertBefore(app.view, button); + + const graphics = new PIXI.Graphics(); + app.stage.addChild(graphics); + return [app.view, graphics]; +} \ No newline at end of file diff --git a/web/script/fetch.js b/web/script/fetch.js new file mode 100644 index 0000000000..33efea1a1d --- /dev/null +++ b/web/script/fetch.js @@ -0,0 +1,62 @@ +var globalRefetchCount = 0; +var lastPrintTime = Date.now(); + +let cellSize = 8; + +function drawGraphics(graphics, colorMap, grid) { + + graphics.clear(); + + for (let rowIdx=0; rowIdx < grid.length; rowIdx++) { + let row = grid[rowIdx]; + for (let colIdx=0; colIdx <= row.length; colIdx++) { + + let colorIdx = row[colIdx]; + let color = colorMap[colorIdx]; + + graphics.beginFill(color); + let xPos = colIdx * cellSize; + let yPos = rowIdx * cellSize; + graphics.drawRect(xPos, yPos, xPos + cellSize, yPos + cellSize); + graphics.endFill(); + } + } +} + +function doFetch(appView, button, gfx, renderWidth, renderHeight) { + + globalRefetchCount += 1; + + const newPrintTime = Date.now(); + const millis = newPrintTime - lastPrintTime; + + if (millis > 3000) { + console.log("Fetch count: " + globalRefetchCount); + lastPrintTime = newPrintTime; + } + + let hCellCount = Math.floor(renderWidth / cellSize); + let vCellCount = Math.floor(renderHeight / cellSize); + let areaSpec = hCellCount + "x" + vCellCount; + fetch("map/" + areaSpec) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error, status = ${response.status}`); + } + return response.json(); + }) + .then((data) => { + if (data.isPlaying) { + drawGraphics(gfx, data.grid.colors, data.grid.coords); + } + + setTimeout(() => doFetch(appView, button, gfx, renderWidth, renderHeight), 30); + }) + .catch((error) => { + const p = document.createElement("p"); + p.appendChild(document.createTextNode(`Error: ${error.message}`)); + document.body.appendChild(p); + + button.style.display = 'block'; + }); +}