-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Towards #558 I was motivated to build this after finding that editing scenario maps directly in the YAML file is rather constraining. ## What I've implemented so far * A small, collapsible panel to the left of the REPL containing World Editing status/operations. Enter world-editing mode with CTRL+e to show the panel. * This works only in `--cheat` mode * Terrain selection * A "picker"/"eye dropper" middle-click mechanism to select a terrain style to draw. * A pop-up selector to choose between the 5 different types of terrain. * Drawing terrain with the left mouse button * Saving a rectangular section of the world map (terrain only) to a file with CTRL+s * Code organization * The complete state of the World Editor, including "painted overlays" of terrain, is contained within the `uiWorldEditor` field of `UIState` record. * The bulk of the World Editor functionality shall be in new modules * Some refactoring of `Controller.hs` and `View.hs` to extract functions utilized by the World Editor (towards #707) ## Vision * The audience for this tooling is strictly envisioned to be Scenario authors. * Though, if we eventually allow swarm-lang to program the UI, there may be some common code to extract. * The World Editor is intended to be compatible with a workflow of editing maps in text form within YAML scenario files. # Demos ## Round-trip with random world stack run -- --scenario creative --seed 0 --cheat Then Ctrl+e, tab down to the Save button, hit Enter to save the map In another tab run: stack run -- --scenario mymap.yaml Toggle between tabs to compare, observe the derived map is an identical 41x21 subset.
- Loading branch information
Showing
27 changed files
with
1,211 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{-# LANGUAGE DerivingVia #-} | ||
|
||
-- | Stand-in type for an "Entity" for purposes | ||
-- that do not require carrying around the entire state | ||
-- of an Entity. | ||
-- | ||
-- Useful for simplified serialization, debugging, | ||
-- and equality checking, particularly for the World Editor. | ||
module Swarm.Game.Scenario.EntityFacade where | ||
|
||
import Control.Lens hiding (from, (.=), (<.>)) | ||
import Data.Text (Text) | ||
import Data.Yaml as Y | ||
import Swarm.Game.Display (Display) | ||
import Swarm.Game.Entity qualified as E | ||
|
||
type EntityName = Text | ||
|
||
-- | This datatype is a lightweight stand-in for the | ||
-- full-fledged "Entity" type without the baggage of all | ||
-- of its other fields. | ||
-- It contains the bare minimum display information | ||
-- for rendering. | ||
data EntityFacade = EntityFacade EntityName Display | ||
deriving (Eq) | ||
|
||
-- Note: This instance is used only for the purpose of WorldPalette | ||
instance ToJSON EntityFacade where | ||
toJSON (EntityFacade eName _display) = toJSON eName | ||
|
||
mkFacade :: E.Entity -> EntityFacade | ||
mkFacade e = | ||
EntityFacade | ||
(e ^. E.entityName) | ||
(e ^. E.entityDisplay) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
{-# LANGUAGE OverloadedStrings #-} | ||
|
||
-- | | ||
-- SPDX-License-Identifier: BSD-3-Clause | ||
module Swarm.Game.Scenario.WorldPalette where | ||
|
||
import Control.Arrow (first) | ||
import Control.Lens hiding (from, (.=), (<.>)) | ||
import Data.Aeson.KeyMap (KeyMap) | ||
import Data.Aeson.KeyMap qualified as KM | ||
import Data.Map qualified as M | ||
import Data.Set qualified as Set | ||
import Data.Text (Text) | ||
import Data.Text qualified as T | ||
import Data.Tuple (swap) | ||
import Swarm.Game.Entity | ||
import Swarm.Game.Scenario.Cell | ||
import Swarm.Game.Scenario.EntityFacade | ||
import Swarm.Game.Scenario.RobotLookup | ||
import Swarm.Game.Terrain (TerrainType) | ||
import Swarm.Util.Yaml | ||
|
||
-- | A world palette maps characters to 'Cell' values. | ||
newtype WorldPalette e = WorldPalette | ||
{unPalette :: KeyMap (PCell e)} | ||
deriving (Eq, Show) | ||
|
||
instance FromJSONE (EntityMap, RobotMap) (WorldPalette Entity) where | ||
parseJSONE = withObjectE "palette" $ fmap WorldPalette . mapM parseJSONE | ||
|
||
type TerrainWith a = (TerrainType, Maybe a) | ||
|
||
cellToTerrainPair :: CellPaintDisplay -> TerrainWith EntityFacade | ||
cellToTerrainPair (Cell terrain maybeEntity _) = (terrain, maybeEntity) | ||
|
||
toCellPaintDisplay :: Cell -> CellPaintDisplay | ||
toCellPaintDisplay (Cell terrain maybeEntity r) = | ||
Cell terrain (mkFacade <$> maybeEntity) r | ||
|
||
toKey :: TerrainWith EntityFacade -> TerrainWith EntityName | ||
toKey = fmap $ fmap (\(EntityFacade eName _display) -> eName) | ||
|
||
-- | We want to identify all of the unique (terrain, entity facade) pairs. | ||
-- However, "EntityFacade" includes a "Display" record, which contains more | ||
-- fields than desirable for use as a unique key. | ||
-- Therefore, we extract just the entity name for use in a | ||
-- (terrain, entity name) key, and couple it with the original | ||
-- (terrain, entity facade) pair in a Map. | ||
getUniqueTerrainFacadePairs :: | ||
[[CellPaintDisplay]] -> | ||
M.Map (TerrainWith EntityName) (TerrainWith EntityFacade) | ||
getUniqueTerrainFacadePairs cellGrid = | ||
M.fromList $ concatMap (map genTuple) cellGrid | ||
where | ||
genTuple c = | ||
(toKey terrainEfd, terrainEfd) | ||
where | ||
terrainEfd = cellToTerrainPair c | ||
|
||
constructPalette :: | ||
[(Char, TerrainWith EntityFacade)] -> | ||
KM.KeyMap CellPaintDisplay | ||
constructPalette mappedPairs = | ||
KM.fromMapText terrainEntityPalette | ||
where | ||
g (terrain, maybeEfd) = Cell terrain maybeEfd [] | ||
terrainEntityPalette = M.fromList $ map (bimap T.singleton g) mappedPairs | ||
|
||
constructWorldMap :: | ||
[(Char, TerrainWith EntityFacade)] -> | ||
[[CellPaintDisplay]] -> | ||
Text | ||
constructWorldMap mappedPairs = | ||
T.unlines . map (T.pack . map renderMapCell) | ||
where | ||
invertedMappedPairs = map (swap . fmap toKey) mappedPairs | ||
|
||
renderMapCell c = | ||
-- NOTE: This lookup should never fail | ||
M.findWithDefault (error "Palette lookup failed!") k $ | ||
M.fromList invertedMappedPairs | ||
where | ||
k = toKey $ cellToTerrainPair c | ||
|
||
-- | All alphanumeric characters. These are used as supplemental | ||
-- map placeholders in case a pre-existing display character is | ||
-- not available to re-use. | ||
genericCharacterPool :: Set.Set Char | ||
genericCharacterPool = Set.fromList $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] | ||
|
||
-- | Note that display characters are not unique | ||
-- across different entities! However, the palette KeyMap | ||
-- as a conveyance serves to dedupe them. | ||
prepForJson :: | ||
WorldPalette EntityFacade -> | ||
[[CellPaintDisplay]] -> | ||
(Text, KM.KeyMap CellPaintDisplay) | ||
prepForJson (WorldPalette suggestedPalette) cellGrid = | ||
(constructWorldMap mappedPairs cellGrid, constructPalette mappedPairs) | ||
where | ||
preassignments :: [(Char, TerrainWith EntityFacade)] | ||
preassignments = | ||
map (first T.head . fmap cellToTerrainPair) $ | ||
M.toList $ | ||
KM.toMapText suggestedPalette | ||
|
||
entityCells :: M.Map (TerrainWith EntityName) (TerrainWith EntityFacade) | ||
entityCells = getUniqueTerrainFacadePairs cellGrid | ||
|
||
unassignedCells :: M.Map (TerrainWith EntityName) (TerrainWith EntityFacade) | ||
unassignedCells = | ||
M.withoutKeys entityCells $ | ||
Set.fromList $ | ||
map (toKey . snd) preassignments | ||
|
||
unassignedCharacters :: Set.Set Char | ||
unassignedCharacters = | ||
-- TODO (#1149): How can we efficiently use the Unicode categories (in "Data.Char") | ||
-- to generate this pool? | ||
Set.difference genericCharacterPool $ | ||
Set.fromList $ | ||
map fst preassignments | ||
|
||
newlyAssignedPairs :: [(Char, TerrainWith EntityFacade)] | ||
newlyAssignedPairs = zip (Set.toList unassignedCharacters) $ M.elems unassignedCells | ||
|
||
mappedPairs = preassignments <> newlyAssignedPairs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.