diff --git a/dist/less/board-component.less b/dist/less/board-component.less index fa01245..871e416 100644 --- a/dist/less/board-component.less +++ b/dist/less/board-component.less @@ -41,7 +41,6 @@ } & #screen-background { - fill: #8AF85B; } & #screen-border { diff --git a/dist/less/default.less b/dist/less/default.less index cd51cfd..5236cb8 100644 --- a/dist/less/default.less +++ b/dist/less/default.less @@ -131,4 +131,14 @@ span.green { color: @green } & #title { font-size: 20px; } +} + +.clearfix { + overflow: auto; +} + +.clearfix::after { + content: ""; + clear: both; + display: table; } \ No newline at end of file diff --git a/dist/less/fonts.less b/dist/less/fonts.less index 656e4ca..4cd2892 100644 --- a/dist/less/fonts.less +++ b/dist/less/fonts.less @@ -1,15 +1,4 @@ @font-face { font-family: monogram; src: url(./fonts/monogram.ttf); -} - -@font-face { - font-family: retro-computer; - src: url(./fonts/retro_computer_personal_use.ttf); -} - - -@font-face { - font-family: boxxy; - src: url(./fonts/boxxy.bdf); } \ No newline at end of file diff --git a/dist/less/sidebar-component.less b/dist/less/sidebar-component.less index 84c4efd..4039d16 100644 --- a/dist/less/sidebar-component.less +++ b/dist/less/sidebar-component.less @@ -1,4 +1,10 @@ #sidebar-component { + & > * { + display: inline-flex; + justify-content: space-between; + width: 100%; + } + & .port { color:red; } & .direction { color:darkgoldenrod; } @@ -10,7 +16,49 @@ & > div { border: lightgray 2px solid; - //padding: lightgray 2px solid; } } + + & .completion-status { + height: 100px; + } +} + + +.board-port-diagram { + @red: #f85b5b; + @green: #8af85b; + @grey: #8c7777; + + width: 100px; + stroke: black; + fill: black; + flex: none; + + + + + & g[data-port-mismatch='port-expected'] { + stroke: @grey; + fill: @grey; + marker { fill: @grey; } + } + + & g[data-port-mismatch='no-port-expected'], g[data-port-mismatch='incorrect-port-type'] { + stroke: @red; + fill: @red; + marker { fill: @red; } + } + + & g[data-port-mismatch='incorrect-capacity'] { + & text { + stroke: @red; + } + } + + & g[data-port-mismatch='no-mismatch'] { + stroke: @green; + fill: @green; + marker { fill: @green; } + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 45cc5e3..976ed39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,14 +6,14 @@ "": { "hasInstallScript": true, "dependencies": { - "animate.css": "^4.1.1", - "purs-backend-es": "^1.4.2" + "driver.js": "^1.3.1" }, "devDependencies": { "esbuild": "^0.18.15", "less": "^4.2.0", "nodemon": "^3.0.1", "purescript": "^0.15.14", + "purs-backend-es": "^1.4.2", "spago": "^0.21.0" } }, @@ -565,11 +565,6 @@ "node": ">=8" } }, - "node_modules/animate.css": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", - "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -937,6 +932,11 @@ "ms": "^2.1.1" } }, + "node_modules/driver.js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.1.tgz", + "integrity": "sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==" + }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -2790,6 +2790,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/purs-backend-es/-/purs-backend-es-1.4.2.tgz", "integrity": "sha512-S9Dxq0rb21eyjtiYKv3S2Hw6T2JE3/uyRSG82BV1/iiDhoQIaie5aKb5EvHhOjfWlbF3dK6b0TWXaePdjqTHuw==", + "dev": true, "bin": { "purs-backend-es": "index.js" } diff --git a/package.json b/package.json index 6c0bf6b..4e8e48b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "compile-css-watch": "nodemon --watch dist/less/* --exec 'npm run compile-css'", "build": "spago bundle-app --main Main --to dist/abed.js", "build-watch": "npm run compile-css-watch & npm run build -- --watch", - "build-prod": "spago -x prod.dhall build && purs-backend-es bundle-app --no-build --minify --to dist/abed.js && npm run compile-css", + "build-prod": "spago -x prod.dhall build && purs-backend-es bundle-app --no-build --minify --to dist/abed.js && npm run compile-css", "test": "spago -x test.dhall test --purs-args '-g sourcemaps'", "test-watch": "npm run test -- --watch" }, @@ -16,9 +16,10 @@ "less": "^4.2.0", "nodemon": "^3.0.1", "purescript": "^0.15.14", - "spago": "^0.21.0", - "purs-backend-es": "^1.4.2" + "purs-backend-es": "^1.4.2", + "spago": "^0.21.0" }, "dependencies": { + "driver.js": "^1.3.1" } } diff --git a/packages.dhall b/packages.dhall index dcf5ea7..cd7087a 100644 --- a/packages.dhall +++ b/packages.dhall @@ -103,4 +103,4 @@ let upstream = sha256:f91d36c7e4793fe4d7e042c57fef362ff3f9e9ba88454cd38686701e30bf545a in upstream with halogen-svg-elems.repo = "https://github.com/MitchStevens/purescript-halogen-svg-elems.git" - with halogen-svg-elems.version = "a3ad0435165c483f04e8bbbeb604f98d938947e3" \ No newline at end of file + with halogen-svg-elems.version = "df7b35f" \ No newline at end of file diff --git a/src/Component/Board.purs b/src/Component/Board.purs index 6bb3117..9072994 100644 --- a/src/Component/Board.purs +++ b/src/Component/Board.purs @@ -54,7 +54,7 @@ import Debug (trace) import Effect.Aff.Class (class MonadAff) import Effect.Class (class MonadEffect, liftEffect) import Effect.Class.Console (log, logShow) -import Game.Board (Board(..), _pieces, _size, addBoardPath, addPiece, buildEvaluableBoard, capacityRipple, decreaseSize, evalBoardM, evalWithPortInfo, getBoardPort, getPieceInfo, increaseSize, pieceDropped, removePiece, rotatePieceBy, runEvaluableM, toLocalInputs) +import Game.Board (Board(..), _pieces, _size, addBoardPath, addPiece, buildEvaluableBoard, capacityRipple, decreaseSize, evalBoardM, evalWithPortInfo, getBoardPortEdge, getPieceInfo, increaseSize, pieceDropped, removePiece, rotatePieceBy, runEvaluableM, toLocalInputs) import Game.Capacity (maxValue) import Game.Direction (CardinalDirection, allDirections) import Game.Direction as Direction @@ -218,7 +218,7 @@ component = mkComponent { eval , initialState , render } --updateStore (BoardEvent (RemovedPiece loc (name piece))) pure Nothing GetMouseOverLocation f -> do - maybeDst <- gets (_.mouseOverLocation) + maybeDst <- gets (_.isMouseOverLocation) pure (f <$> maybeDst) SetGoalPorts boardPorts -> do lift $ debug (tag "boardPorts" (show boardPorts)) "Set goal ports on board" @@ -258,7 +258,7 @@ component = mkComponent { eval , initialState , render } PieceOutput (Piece.Dropped src) -> do lift $ debug M.empty ("Piece dropped at " <> show src) -- when a piece is dropped, it can be dropped over a new location or outside the game board - maybeDst <- gets (_.mouseOverLocation) + maybeDst <- gets (_.isMouseOverLocation) eitherPiece <- liftBoardM (pieceDropped src maybeDst) case eitherPiece of Left boardError -> do @@ -343,14 +343,14 @@ component = mkComponent { eval , initialState , render } -- can these events be simplified? do we need all of them? LocationOnDragEnter loc dragEvent -> do liftEffect $ preventDefault (toEvent dragEvent) - modify_ (_ { mouseOverLocation = Just loc } ) + modify_ (_ { isMouseOverLocation = Just loc } ) LocationOnDragOver loc dragEvent -> do liftEffect $ preventDefault (toEvent dragEvent) LocationOnDrop loc dragEvent -> do - modify_ (_ { mouseOverLocation = Just loc } ) + modify_ (_ { isMouseOverLocation = Just loc } ) liftEffect $ preventDefault (toEvent dragEvent) LocationOnDragLeave _ -> do - modify_ (_ { mouseOverLocation = Nothing } ) + modify_ (_ { isMouseOverLocation = Nothing } ) GlobalOnKeyDown ke -> do case key ke of "z" -> when (ctrlKey ke) (handleAction Undo) @@ -363,7 +363,7 @@ component = mkComponent { eval , initialState , render } BoardPortOnMouseEnter dir -> do modify_ (_ { isMouseOverBoardPort = Just dir }) - relativeEdge <- evalState (getBoardPort dir) <$> use _board + relativeEdge <- evalState (getBoardPortEdge dir) <$> use _board signals <- gets (_.lastEvalWithPortInfo) let focus = { info: _, relativeEdge } <$> M.lookup relativeEdge signals tell slot.multimeter unit (\_ -> Multimeter.NewFocus focus) diff --git a/src/Component/Board/Types.purs b/src/Component/Board/Types.purs index 4f4246e..c412719 100644 --- a/src/Component/Board/Types.purs +++ b/src/Component/Board/Types.purs @@ -33,7 +33,7 @@ import Data.TraversableWithIndex (forWithIndex) import Data.Tuple (Tuple(..)) import Data.Zipper (Zipper) import Data.Zipper as Z -import Game.Board (Board(..), BoardError, BoardM, RelativeEdge, getBoardPort, runBoardM, standardBoard) +import Game.Board (Board(..), BoardError, BoardM, RelativeEdge, getBoardPortEdge, runBoardM, standardBoard) import Game.Direction (CardinalDirection) import Game.Location (Location(..)) import Game.Piece (Piece(..)) @@ -46,11 +46,10 @@ import Web.HTML.Event.DragEvent (DragEvent) import Web.UIEvent.KeyboardEvent (KeyboardEvent) import Web.UIEvent.MouseEvent (MouseEvent) -type Input = Maybe Board +type Input = { board ::Board } type State = { boardHistory :: Zipper Board -- todo: limit the number of boards in this data structure - , mouseOverLocation :: Maybe Location , inputs :: Map CardinalDirection Signal , outputs :: Map CardinalDirection Signal , lastEvalWithPortInfo :: Map RelativeEdge PortInfo @@ -60,6 +59,7 @@ type State = , locations :: Array Location } , isMouseOverBoardPort :: Maybe CardinalDirection + , isMouseOverLocation :: Maybe Location } data Query a @@ -105,14 +105,14 @@ type Slots = ) initialState :: Input -> State -initialState maybeBoard = - { boardHistory: Z.singleton (fromMaybe standardBoard maybeBoard) - , mouseOverLocation: Nothing +initialState { board } = + { boardHistory: Z.singleton board , boardPorts: M.empty , inputs: M.empty , outputs: M.empty , lastEvalWithPortInfo: M.empty , isCreatingWire: Nothing + , isMouseOverLocation: Nothing , isMouseOverBoardPort: Nothing } @@ -138,7 +138,7 @@ boardPortInfo = do boardPorts <- gets (_.boardPorts) board <- use _board forWithIndex boardPorts \dir port -> do - let relEdge = evalState (getBoardPort dir) board + let relEdge = evalState (getBoardPortEdge dir) board gets (_.lastEvalWithPortInfo >>> M.lookup relEdge >>> fromMaybe { connected: false, port, signal: Signal 0}) liftBoardM :: forall m a. MonadState State m => BoardM a -> m (Either BoardError (Tuple a Board)) diff --git a/src/Component/Lesson/Tutorial.js b/src/Component/Lesson/Tutorial.js new file mode 100644 index 0000000..017e97b --- /dev/null +++ b/src/Component/Lesson/Tutorial.js @@ -0,0 +1,18 @@ +import { driver } from "driver.js"; + +export const addPieceLesson = () => { + const idPiece = document.querySelector("div[data-avilable-piece='id-piece']") + const pieceLocation = document.querySelector("div[data-location='(0,1)']") + + const driver = driver({ + steps: + [ { element: idPiece, popover: { title: "Mouse down on the piece" }} + , { element: pieceLocation, popover: { title: "Drag the piece to the board" }} + ] + }) + + idPiece.addEventListener("mousedown", () => driver.moveNext(), { once: true}) + pieceLocation.addEventListener("onDrop", () => driver.moveNext(), { once: true}) + + return driver.drive() +} diff --git a/src/Component/Lesson/Tutorial.purs b/src/Component/Lesson/Tutorial.purs new file mode 100644 index 0000000..d38a645 --- /dev/null +++ b/src/Component/Lesson/Tutorial.purs @@ -0,0 +1,25 @@ +module Component.Lesson.Tutorial where + +import Prelude + +import Effect (Effect) + +{- + lessons: + addPiece + removePiece + movePiece + create path + using multimeter + changing capacity + toggleing input ports + increasing/decreasing input ports + runnning tests + + undo/redo + + +-} + + +foreign import addPieceLesson :: Effect Unit \ No newline at end of file diff --git a/src/Component/Level.purs b/src/Component/Level.purs index f5a3a74..395c348 100644 --- a/src/Component/Level.purs +++ b/src/Component/Level.purs @@ -12,6 +12,7 @@ import Control.Monad.Cont (ContT(..), callCC, lift, runContT) import Control.Monad.Except (ExceptT(..), runExceptT) import Control.Monad.Logger.Class (class MonadLogger, debug, info) import Control.Monad.Reader (class MonadAsk, class MonadReader) +import Control.Monad.State (evalState) import Data.Array (intercalate, intersperse) import Data.Array as A import Data.Bifunctor (lmap) @@ -33,16 +34,17 @@ import Effect.Aff (delay) import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (class MonadEffect, liftEffect) import Effect.Class.Console (log) -import Game.Board (_size, firstEmptyLocation) +import Game.Board (Board(..), _size, firstEmptyLocation, getBoardPorts, standardBoard) import Game.Direction (CardinalDirection) import Game.GameEvent (GameEvent, GameEventStore) import Game.Level (LevelId, Level) import Game.Level.Completion (CompletionStatus(..), FailedTestCase, isReadyForTesting, runSingleTest) import Game.Message (Message(..), green, htmlMessage, message, red) import Game.Piece (Piece(..), pieceLookup) +import Game.Port (Port(..)) import Game.Signal (Signal(..)) import GlobalState (GlobalState) -import Halogen (ClassName(..), Component, HalogenM, HalogenQ, Slot, gets) +import Halogen (ClassName(..), Component, HalogenM, HalogenQ, Slot, gets, modify_) import Halogen as H import Halogen.HTML (PlainHTML) import Halogen.HTML as HH @@ -55,13 +57,18 @@ type Input = , level :: Level } -type State = Input +type State = + { levelId :: LevelId + , level :: Level + , completionStatus :: CompletionStatus + , boardSize :: Int + , boardPorts :: Map CardinalDirection Port + } --data Query a data Action = Initialise - | LevelComplete | BoardOutput Board.Output | SidebarOutput Sidebar.Output @@ -83,14 +90,17 @@ component :: forall q o m => Component q Input o m component = H.mkComponent { eval , initialState , render } where - initialState = identity + Board initialBoard = standardBoard + + + initialState {levelId, level } = {levelId, level, completionStatus: NotStarted, boardSize: initialBoard.size, boardPorts: evalState getBoardPorts (Board initialBoard) } --render :: State -> HalogenM State Action Slots o m Unit - render { level, levelId } = HH.div + render { level, levelId, completionStatus, boardSize, boardPorts } = HH.div [ HP.id "puzzle-component"] - [ HH.slot _board unit Board.component Nothing BoardOutput + [ HH.slot _board unit Board.component { board: Board initialBoard} BoardOutput , HH.slot_ _chat unit Chat.component unit - , HH.slot _sidebar unit Sidebar.component { problem: level.problem, boardSize: 3 } SidebarOutput + , HH.slot _sidebar unit Sidebar.component { problem: level.problem, completionStatus, boardSize, boardPorts } SidebarOutput ] eval :: HalogenQ q Action Input ~> HalogenM State Action Slots o m @@ -116,27 +126,22 @@ component = H.mkComponent { eval , initialState , render } -- make the board component display goal ports Piece piece <- H.gets (_.level.problem.goal) H.tell _board unit (\_ -> Board.SetGoalPorts piece.ports) - LevelComplete -> do - H.tell _sidebar unit (\_ -> Sidebar.SetCompletionStatus Completed) - - levelId <- gets (_.levelId) - liftEffect $ Progress.saveLevelProgress levelId Progress.Completed BoardOutput (Board.NewBoardState board) -> do - -- the sidebar is also update with the current size of the board - H.tell _sidebar unit (\_ -> Sidebar.SetBoardSize (board ^. _size)) - -- the side bar updates the puzzle completion status, if the board is ready to be -- shown, trigger to test is on the sidebar problem <- H.gets (_.level.problem) - let status = isReadyForTesting problem board - - H.tell _sidebar unit (\_ -> Sidebar.SetCompletionStatus status) + modify_ $ _ + { completionStatus = isReadyForTesting problem board + , boardSize = board ^. _size + , boardPorts = evalState getBoardPorts board } SidebarOutput (Sidebar.PieceDropped pieceId) -> do maybeLocation <- H.request _board unit (Board.GetMouseOverLocation) - for_ maybeLocation (addPieceToComponent pieceId) + for_ maybeLocation \loc -> + H.tell _board unit (\_ -> Board.AddPiece loc (pieceLookup pieceId)) SidebarOutput (Sidebar.PieceAdded pieceId) -> do H.request _board unit Board.GetBoard >>= traverse_ \board -> - for_ (firstEmptyLocation board) (addPieceToComponent pieceId) + for_ (firstEmptyLocation board) \loc -> + H.tell _board unit (\_ -> Board.AddPiece loc (pieceLookup pieceId)) SidebarOutput Sidebar.BoardSizeIncremented -> void $ H.request _board unit Board.IncrementBoardSize SidebarOutput Sidebar.BoardSizeDecremented -> @@ -149,12 +154,17 @@ component = H.mkComponent { eval , initialState , render } res <- runAllTests problem.goal (L.fromFoldable problem.testCases) eval - when (isRight res) do - sendMessage (htmlMessage "/test" (HH.b_ [HH.text "Tests completed successfully!"])) - handleAction LevelComplete - - addPieceToComponent pieceId loc = - H.tell _board unit (\_ -> Board.AddPiece loc (pieceLookup pieceId)) + case res of + Left failedTestCase -> do + modify_ $ _ { completionStatus = FailedTestCase failedTestCase} + pure unit + Right _ -> do + sendMessage (htmlMessage "/test" (HH.b_ [HH.text "Tests completed successfully!"])) + levelId <- gets (_.levelId) + liftEffect $ Progress.saveLevelProgress levelId Progress.Completed + modify_ $ _ { completionStatus = Completed } + + -- this should be CPS but it's fine for small number of inputs diff --git a/src/Component/Multimeter.purs b/src/Component/Multimeter.purs index 42ebfc0..d2f3b1e 100644 --- a/src/Component/Multimeter.purs +++ b/src/Component/Multimeter.purs @@ -3,6 +3,7 @@ module Component.Multimeter where import Prelude import Capability.GlobalEventEmmiters (globalKeyDownEventEmitter, globalMouseMoveEventEmitter) +import Component.Rendering.Colours (green) import Data.Align (aligned) import Data.Array (elem, range, replicate) import Data.Array as A @@ -113,6 +114,7 @@ component = H.mkComponent { eval, initialState, render } [ SA.transform [ Translate screen.x screen.y ] ] [ SE.rect [ SA.id "screen-background" + , SA.fill green , SA.height screen.height , SA.width screen.width ] diff --git a/src/Component/Rendering/BoardPortDiagram.purs b/src/Component/Rendering/BoardPortDiagram.purs new file mode 100644 index 0000000..0a8aa96 --- /dev/null +++ b/src/Component/Rendering/BoardPortDiagram.purs @@ -0,0 +1,115 @@ +module Component.Rendering.BoardPortDiagram + ( renderBoardPortDiagram + ) + where + +import Prelude + +import Component.DataAttribute (DataAttribute) +import Component.DataAttribute as DA +import Component.Rendering.Colours (red) +import Data.FoldableWithIndex (foldMapWithIndex) +import Data.Map (Map) +import Data.Map as M +import Data.Maybe (Maybe(..), maybe) +import Data.Tuple (Tuple(..)) +import Game.Board (EvaluableBoard(..), evaluableBoardPiece) +import Game.Capacity (toInt) +import Game.Direction (CardinalDirection, allDirections, clockwiseRotation) +import Game.Direction as Direction +import Game.Level.Completion (PortMismatch(..), isPortMismatch) +import Game.Piece (Piece(..), getPort) +import Game.Port (Port(..), portCapacity, portType) +import Game.Port as Port +import Game.Rotation (toDegrees) +import Halogen (AttrName(..), ClassName(..), ComponentHTML) +import Halogen.HTML as HH +import Halogen.Svg.Attributes (Baseline(..), CommandPositionReference(..), Orient(..), TextAnchor(..), Transform(..), l, m, z) +import Halogen.Svg.Attributes as SA +import Halogen.Svg.Elements (defs) +import Halogen.Svg.Elements as SE + + + +renderBoardPortDiagram :: forall a s m. Piece -> Map CardinalDirection Port -> ComponentHTML a s m +renderBoardPortDiagram goal boardPorts = + SE.svg + [ SA.viewBox 0.0 0.0 100.0 100.0 + , SA.class_ (ClassName "board-port-diagram") ] + ([center] <> map port allDirections) + where + centerWidth = 30.0 + arrowLength = ((100.0 - centerWidth) / 2.0) - 5.0 + + center = SE.rect + [ SA.width centerWidth + , SA.height centerWidth + , SA.x ((100.0 - centerWidth) / 2.0) + , SA.y ((100.0 - centerWidth) / 2.0) + ] + + port :: CardinalDirection -> ComponentHTML a s m + port dir = + SE.g + [ DA.attr portMismatchDataAttribute portMismatch ] + [ defs + , SE.g [] $ + case portMismatch of + Just (PortExpected { direction, expected }) -> [ arrow (portType expected), label (portCapacity expected) ] + Just (NoPortExpected { direction, received }) -> [ arrow (portType received) ] + Just (IncorrectPortType { direction, capacity, received, expected }) -> [ arrow received, label capacity ] + Just (IncorrectCapacity { direction, portType, received, expected }) -> [ arrow portType, label expected ] + Nothing -> maybe [] (\(Port { capacity, portType }) -> [ arrow portType, label capacity ]) (M.lookup dir boardPorts) + ] + where + portMismatch = isPortMismatch dir (getPort goal dir) (M.lookup dir boardPorts) + + arrow portType = SE.g + [ SA.transform [ Rotate (toDegrees (clockwiseRotation Direction.Up dir)) 50.0 50.0 ] ] + [ SE.line + [ SA.x1 50.0 + , SA.y1 5.0 + , SA.x2 50.0 + , SA.y2 arrowLength + , SA.strokeWidth 2.0 + , if portType == Port.Input + then SA.markerEnd $ "url(#arrow" <> show dir <> ")" + else SA.markerStart $ "url(#arrow" <> show dir <>")" + ] + ] + + portMismatchDataAttribute = DA.Attr (AttrName "data-port-mismatch") $ case _ of + Just (PortExpected _) -> "port-expected" + Just (NoPortExpected _) -> "no-port-expected" + Just (IncorrectPortType _) -> "incorrect-port-type" + Just (IncorrectCapacity _) -> "incorrect-capacity" + Nothing -> "no-mismatch" + + + label capacity = + let Tuple x y = case dir of + Direction.Up -> Tuple 55.0 (arrowLength - 5.0) + Direction.Right -> Tuple (100.0 - arrowLength + 5.0) (50.0 - 5.0) + Direction.Down -> Tuple 55.0 (100.0 - arrowLength + 3.0) + Direction.Left -> Tuple (arrowLength - 5.0) (50.0 - 5.0) + textAnchor = if dir == Direction.Left then AnchorEnd else AnchorStart + baseline = if dir == Direction.Down then Hanging else Auto + in + SE.text + [ SA.x x, SA.y y, SA.textAnchor textAnchor, SA.dominantBaseline baseline ] + [ HH.text $ show (toInt capacity) ] + + defs = SE.defs [] + [ SE.marker + [ SA.id $ "arrow" <> show dir + , SA.refX 5.0 + , SA.refY 5.0 + , SA.markerWidth 5.0 + , SA.markerHeight 5.0 + , SA.orient AutoStartReverse + , SA.viewBox 0.0 0.0 10.0 10.0 + ] + [ SE.path [ SA.d [ m Abs 0.0 0.0, l Abs 10.0 5.0, l Abs 0.0 10.0, z] ] + ] + ] + diff --git a/src/Component/Rendering/Colours.purs b/src/Component/Rendering/Colours.purs new file mode 100644 index 0000000..577223b --- /dev/null +++ b/src/Component/Rendering/Colours.purs @@ -0,0 +1,18 @@ +module Component.Rendering.Colours where + +import Prelude + +import Halogen.Svg.Attributes (Color(..)) + +portColours = + { teal: RGB 117 242 191 + , blue: RGB 120 204 250 + , purple: RGB 208 135 221 + , pink: RGB 228 100 156 + } + +green :: Color +green = RGB 138 248 91 + +red :: Color +red = RGB 248 91 91 diff --git a/src/Component/Rendering/Gradient.purs b/src/Component/Rendering/Gradient.purs index 0d218db..910b57b 100644 --- a/src/Component/Rendering/Gradient.purs +++ b/src/Component/Rendering/Gradient.purs @@ -2,6 +2,7 @@ module Component.Rendering.Gradient where import Prelude +import Component.Rendering.Colours (portColours) import Data.Foldable (intercalate) import Game.Capacity (Capacity(..), toInt) import Game.Piece as Port @@ -46,15 +47,10 @@ setAlpha alpha = case _ of portColor :: Port -> Signal -> Color portColor port signal = (if signal == Signal 0 then shadeColor (-30) else identity) $ case portCapacity port of - OneBit -> green - TwoBit -> blue - FourBit -> purple - EightBit -> pink - where - green = RGB 117 242 191 -- #7 - blue = RGB 120 204 250 -- # - purple = RGB 208 135 221 -- # - pink = RGB 228 100 156 -- # + OneBit -> portColours.teal + TwoBit -> portColours.blue + FourBit -> portColours.purple + EightBit -> portColours.pink shadeColor :: Int -> Color -> Color shadeColor percentage = case _ of diff --git a/src/Component/Rendering/Piece.purs b/src/Component/Rendering/Piece.purs index cabbd5d..9667ca1 100644 --- a/src/Component/Rendering/Piece.purs +++ b/src/Component/Rendering/Piece.purs @@ -29,8 +29,7 @@ import Halogen.Svg.Elements as SE renderPiece :: forall s m. State -> ComponentHTML Action s m renderPiece state = SE.svg - [ SA.viewBox 0.0 0.0 100.0 100.0 - ] + [ SA.viewBox 0.0 0.0 100.0 100.0 ] [ SE.g attributes ([ render ] <> animations) diff --git a/src/Component/Sidebar.purs b/src/Component/Sidebar.purs index 48beb28..41bc5c0 100644 --- a/src/Component/Sidebar.purs +++ b/src/Component/Sidebar.purs @@ -14,9 +14,10 @@ import Capability.Navigate (Route(..), navigateTo) import Component.DataAttribute (attr) import Component.DataAttribute as DataAttr import Component.Piece as Piece +import Component.Rendering.BoardPortDiagram (renderBoardPortDiagram) import Component.Rendering.Piece (renderPiece) import Control.Monad.Except (runExceptT) -import Control.Monad.State.Class (modify_, gets) +import Control.Monad.State.Class (gets, modify_, put) import Data.Array as A import Data.Bifunctor (bimap) import Data.Either (Either(..), blush, hush) @@ -24,6 +25,7 @@ import Data.Filterable (eitherBool) import Data.Foldable (find, for_) import Data.List (List(..), (:)) import Data.List as L +import Data.Map (Map) import Data.Map as M import Data.Maybe (Maybe(..), fromMaybe, maybe) import Data.String (Pattern(..), split) @@ -32,7 +34,8 @@ import Effect.Aff.Class (class MonadAff) import Effect.Class (class MonadEffect, liftEffect) import Effect.Class.Console (log) import Game.Capacity (toInt) -import Game.Level.Completion (CompletionStatus(..), FailedTestCase) +import Game.Direction (CardinalDirection) +import Game.Level.Completion (CompletionStatus(..), FailedTestCase, PortMismatch(..)) import Game.Level.Problem (Problem) import Game.Location (location) import Game.Piece (PieceId(..), name, pieceVault) @@ -51,21 +54,23 @@ import Web.UIEvent.MouseEvent (MouseEvent) type Input = { problem :: Problem + , completionStatus :: CompletionStatus , boardSize :: Int + , boardPorts :: Map CardinalDirection Port } type State = { problem :: Problem , completionStatus :: CompletionStatus , boardSize :: Int + , boardPorts :: Map CardinalDirection Port } data Query a - = SetCompletionStatus CompletionStatus - | SetBoardSize Int data Action - = PieceOnDrop PieceId DragEvent + = Initialise Input + | PieceOnDrop PieceId DragEvent | PieceOnClick PieceId MouseEvent | BackToLevelSelect | IncrementBoardSize @@ -93,34 +98,33 @@ data Output component :: forall m. MonadAff m => Component Query Input Output m component = mkComponent { eval , initialState , render } where - initialState { problem, boardSize } = - { problem - , completionStatus: NotStarted - , boardSize } + initialState { problem, boardSize, completionStatus, boardPorts } = + { problem , completionStatus , boardSize, boardPorts } - render :: forall s m. State -> ComponentHTML Action s m + render :: forall s. State -> ComponentHTML Action s m render state = HH.div [ HP.id "sidebar-component" ] [ HH.h2_ [ HH.text state.problem.title ] , HH.span_ [ renderDescription state.problem.description ] - , HH.br_ - , renderCompletionStatus state.completionStatus + , HH.hr_ + , HH.div + [ HP.classes [ ClassName "completion-status"] ] + [ renderCompletionStatus + , renderBoardPortDiagram state.problem.goal state.boardPorts + ] , HH.h3_ [ HH.text "Available pieces:"] , HH.span [ HP.class_ (ClassName "pieces") ] $ renderAvailablePiece <$> A.nub state.problem.availablePieces - , HH.h3_ [ HH.text "Board size" ] - , HH.span_ - [ HH.button [ HE.onClick (\_ -> DecrementBoardSize) ] [ HH.text "-" ] - , HH.text (show state.boardSize) - , HH.button [ HE.onClick (\_ -> IncrementBoardSize) ] [ HH.text "+" ] - ] , HH.br_ - , HH.p [ HE.onClick (\_ -> BackToLevelSelect)] [HH.text "I give up (choose another level)"] + , renderBoardSize + , renderGiveUp ] where + + --renderAvailablePiece :: forall p. PieceId -> HTML (ComponentSlot Slots) Action renderAvailablePiece piece = let input = { piece, location: location 0 0, portStates: M.empty } @@ -136,11 +140,74 @@ component = mkComponent { eval , initialState , render } , HH.text (show pieceId) ] + renderCompletionStatus = HH.div_ + case state.completionStatus of + NotStarted -> [] + FailedRestriction restriction -> + [ HH.text $ "This level has a special restriction: " + , HH.b_ [ HH.text restriction.name ] + , HH.br_ + , HH.text restriction.description + ] + NotEvaluable boardError -> + [ HH.text ("not evaluable due to: " <> show boardError) ] + PortMismatch mismatch -> + [ HH.div_ + [ HH.b_ [ HH.text "Port mismatch:" ] + , case mismatch of + PortExpected { direction, expected } -> HH.text $ "You need " <> describePort expected <> " in the " <> show direction <> " direction" + NoPortExpected { direction, received } -> HH.text $ "Remove the port in the " <> show direction <> "direction" + IncorrectPortType { direction, capacity, received, expected } -> HH.text $ "Port in the " <> show direction <> " direction should be an " <> show expected + IncorrectCapacity { direction, portType, received, expected } -> HH.text $ "Port in the " <> show direction <> " direction should have capacity " <> show (toInt expected) + ] + ] + ReadyForTesting -> + [ HH.text "Ready for testing: " + , HH.button + [ HE.onClick (\_ -> RunTestsClicked) ] + [ HH.text "Run Tests"] + ] + FailedTestCase testCase -> + [ HH.text "failed test case, render later" ] + Completed -> + [ HH.text "Level Complete!" + , HH.button + [ HE.onClick (\_ -> RunTestsClicked) ] + [ HH.text "Run Tests again"] + , HH.button + [ HE.onClick (\_ -> BackToLevelSelect) ] + [ HH.text "Back to Level Select "] + ] + where + describePort :: Port -> String + describePort (Port {portType, capacity}) = + "an " <> if portType == Port.Input then "input" else "output" <> " of capacity " <> show (toInt capacity) + + + renderBoardSize = + HH.div + [ HP.classes [ ClassName "board-size" ]] + [ HH.b_ [ HH.text "Board size" ] + , HH.div + [ HP.classes [ ClassName "buttons"] ] + [ HH.button [ HE.onClick (\_ -> DecrementBoardSize) ] [ HH.text "-" ] + , HH.text (" " <> show state.boardSize <> " ") + , HH.button [ HE.onClick (\_ -> IncrementBoardSize) ] [ HH.text "+" ] + ] + ] + + renderGiveUp = + HH.div + [ HP.classes [ClassName "give-up"] ] + [ HH.b_ [ HH.text "I give up" ] + , HH.button [ HE.onClick (\_ -> BackToLevelSelect) ] [HH.text "Choose another level"] + ] eval :: forall slots. HalogenQ Query Action Input ~> HalogenM State Action slots Output m eval = mkEval { finalize: Nothing , handleAction: case _ of + Initialise input -> put (initialState input) PieceOnDrop piece _ -> do raise (PieceDropped piece) PieceOnClick piece _ -> @@ -151,20 +218,9 @@ component = mkComponent { eval , initialState , render } DecrementBoardSize -> raise BoardSizeDecremented RunTestsClicked -> raise TestsTriggered DoNothing -> pure unit - , handleQuery: case _ of - SetCompletionStatus completionStatus -> do - modify_ $ _ { completionStatus = completionStatus } - pure Nothing - --IsProblemSolved board f -> do - -- problemDescription <- H.gets (_.problem) - -- isSolved <- liftAff $ runExceptT $ solvedBy problemDescription board - -- H.modify_ (_ { error = blush isSolved }) - -- pure (f <$> blush isSolved) - SetBoardSize boardSize -> do - modify_ $ _ { boardSize = boardSize } - pure Nothing + , handleQuery: \_ -> pure Nothing , initialize: Nothing - , receive: const Nothing -- :: input -> Maybe action + , receive: Just <<< Initialise -- :: input -> Maybe action } --renderError :: PieceSpecMismatch -> PlainHTML @@ -202,51 +258,3 @@ renderDescription = HH.div_ <<< A.fromFoldable <<< map asHTML <<< reduceStrings asHTML (Left pieceName) = HH.span [ HP.class_ (ClassName "piece-name") ] [ HH.text pieceName ] asHTML (Right text) = HH.text text - -renderCompletionStatus :: forall s m. CompletionStatus -> ComponentHTML Action s m -renderCompletionStatus = case _ of - NotStarted -> - HH.div_ [] - FailedRestriction restriction -> - HH.span_ - [ HH.text $ "This level has a special restriction: " - , HH.b_ [ HH.text restriction.name ] - , HH.br_ - , HH.text restriction.description - ] - NotEvaluable boardError -> - HH.text ("not evaluable due to: " <> show boardError) - PortMismatch mismatch -> - HH.div_ - [ HH.b_ [ HH.text "Port mismatch:" ] - , HH.text $ "Expected " <> renderMaybePort mismatch.expected <> " on the " <> show mismatch.dir <> ", but found " <> renderMaybePort mismatch.received - ] - ReadyForTesting -> - HH.span_ - [ HH.text "Ready for testing: " - , HH.button - [ HE.onClick (\_ -> RunTestsClicked) ] - [ HH.text "Run Tests"] - ] - FailedTestCase testCase -> - renderFailedTestCase testCase - Completed -> - HH.div_ - [ HH.text "Level Complete!" - , HH.button - [ HE.onClick (\_ -> RunTestsClicked) ] - [ HH.text "Run Tests again"] - , HH.button - [ HE.onClick (\_ -> BackToLevelSelect) ] - [ HH.text "Back to Level Select "] - ] - where - renderMaybePort :: Maybe Port -> String - renderMaybePort = case _ of - Nothing -> "no port" - Just (Port {portType, capacity}) -> - "an " <> if portType == Port.Input then "input" else "output" <> " of capacity " <> show (toInt capacity) - - renderFailedTestCase :: FailedTestCase -> ComponentHTML Action s m - renderFailedTestCase _ = -- todo: - HH.text "failed test case, render later" diff --git a/src/Game/Board/EvaluableBoard.purs b/src/Game/Board/EvaluableBoard.purs index 5c97bce..130fbc9 100644 --- a/src/Game/Board/EvaluableBoard.purs +++ b/src/Game/Board/EvaluableBoard.purs @@ -30,7 +30,7 @@ import Control.Monad.Error.Class (class MonadError, throwError) import Control.Monad.Except (ExceptT, runExcept) import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT) import Control.Monad.Reader (class MonadReader, ReaderT, asks, runReaderT) -import Control.Monad.State (class MonadState, State, StateT, evalState, evalStateT, get, gets, modify, modify_, runState) +import Control.Monad.State (class MonadState, State, StateT, evalState, evalStateT, execState, get, gets, modify, modify_, runState) import Data.Either (Either, note) import Data.FoldableWithIndex (forWithIndex_) import Data.FunctorWithIndex (mapWithIndex) @@ -50,7 +50,7 @@ import Data.TraversableWithIndex (forWithIndex) import Data.Tuple (Tuple(..), snd) import Debug (debugger, trace) import Game.Board.PseudoPiece (getPsuedoPiecePort, isPseudoInput, isPseudoPiece, psuedoPiece) -import Game.Board.Query (adjacentRelativeEdge, buildConnectionMap, getBoardEdgePseudoLocation, getPortOnEdge, toRelativeEdge) +import Game.Board.Query (adjacentRelativeEdge, buildConnectionMap, getBoardEdgePseudoLocation, getBoardPorts, getPortOnEdge, toRelativeEdge) import Game.Board.RelativeEdge (RelativeEdge, absolute, relative, relativeEdgeLocation) import Game.Board.Types (Board(..), BoardError(..), _pieces) import Game.Capacity (Capacity, clampSignal) @@ -104,15 +104,9 @@ setOuterPort dir signal = void $ runMaybeT do let portInfo = { connected: false, port, signal: clampSignal (portCapacity port) signal } modify_ (M.insert relEdge portInfo) + toEvaluableBoard :: Board -> Either BoardError EvaluableBoard -toEvaluableBoard board = runExcept $ flip (evalStateT) board do - psuedoPiecePorts <- M.catMaybes <<< M.fromFoldable <$> - for allDirections \dir -> do - loc <- getBoardEdgePseudoLocation dir - relEdge <- toRelativeEdge (absolute loc (oppositeDirection dir)) >>= adjacentRelativeEdge - maybePort <- getPortOnEdge relEdge - pure (Tuple dir maybePort) - get >>= buildEvaluableBoard psuedoPiecePorts +toEvaluableBoard board = runExcept (buildEvaluableBoard (evalState getBoardPorts board) board) {- There are two ways to build an `EvaluableBoard`, either by specifying outer ports or not. If the outer ports are not specified (via the `maybePorts` parameter), outer ports will be created from the pieces adjacent to the outer ports. diff --git a/src/Game/Board/Query.purs b/src/Game/Board/Query.purs index 6432efd..e402db6 100644 --- a/src/Game/Board/Query.purs +++ b/src/Game/Board/Query.purs @@ -27,7 +27,7 @@ import Game.Board.PieceInfo (_rotation) import Game.Board.RelativeEdge (AbsoluteEdge, RelativeEdge(..), absolute, relative, relativeEdgeDirection, relativeEdgeLocation) import Game.Board.Types (Board(..), _pieces, _size) import Game.Capacity (Capacity) -import Game.Direction (CardinalDirection, allDirections, rotateDirection) +import Game.Direction (CardinalDirection, allDirections, oppositeDirection, rotateDirection) import Game.Direction as Direction import Game.Edge (Edge(..), matchEdge) import Game.Location (Location(..), location) @@ -143,11 +143,23 @@ getBoardEdgePseudoLocation dir = do Direction.Down -> location (n`div`2) (n) Direction.Left -> location (-1) (n`div`2) -getBoardPort :: forall m. MonadState Board m => CardinalDirection -> m RelativeEdge -getBoardPort dir = do + + + +getBoardPortEdge :: forall m. MonadState Board m => CardinalDirection -> m RelativeEdge +getBoardPortEdge dir = do loc <- getBoardEdgePseudoLocation dir pure $ relative loc Direction.Right +getBoardPorts :: forall m. MonadState Board m => m (Map CardinalDirection Port) +getBoardPorts = + M.catMaybes <<< M.fromFoldable <$> + for allDirections \dir -> do + loc <- getBoardEdgePseudoLocation dir + relEdge <- toRelativeEdge (absolute loc (oppositeDirection dir)) >>= adjacentRelativeEdge + maybePort <- getPortOnEdge relEdge + pure (Tuple dir maybePort) + {- Create a bidirectional mapping from inputs to ouptuts ports -} diff --git a/src/Game/Board/Types.purs b/src/Game/Board/Types.purs index 153ec6a..1062fe2 100644 --- a/src/Game/Board/Types.purs +++ b/src/Game/Board/Types.purs @@ -7,10 +7,11 @@ import Control.Monad.State (StateT, runStateT) import Data.Array ((..)) import Data.Array as A import Data.Either (Either) -import Data.Foldable (foldMap, intercalate, surround) +import Data.Foldable (foldMap, intercalate, maximumBy, surround) +import Data.Function (on) import Data.Group (ginverse) import Data.Identity (Identity) -import Data.Lens (Lens', to, view) +import Data.Lens (Lens', to, view, (^.)) import Data.Lens.Iso.Newtype (_Newtype) import Data.Lens.Record (prop) import Data.List (List(..)) @@ -30,7 +31,7 @@ import Game.Board.PieceInfo (PieceInfo) import Game.Board.RelativeEdge (RelativeEdge, relative, relativeEdgeDirection) import Game.Direction (CardinalDirection, oppositeDirection, rotateDirection) import Game.Direction as Direction -import Game.Location (Location(..), location) +import Game.Location (Location(..), location, taxicabDistance) import Game.Piece (Piece(..)) import Game.Port (Port(..), isInput) import Game.Rotation (Rotation(..)) @@ -83,6 +84,7 @@ toGlobalInputs loc = unsafeMapKey (relative loc) allOccupiedLocations :: Board -> Set Location allOccupiedLocations = view $ _pieces <<< to M.keys + -- todo: ensure that this short circuits when the empty loction is found firstEmptyLocation :: Board -> Maybe Location firstEmptyLocation board = do @@ -94,15 +96,16 @@ firstEmptyLocation board = do let occupied = allOccupiedLocations board A.find (\loc -> S.member loc occupied) allLocations +closestEmptyLocation :: Board -> Location -> Maybe Location +closestEmptyLocation board loc = maximumBy (compare `on` (taxicabDistance loc)) emptyLocations + where + occupied = allOccupiedLocations board + n = board ^. _size + emptyLocations = do + j <- 0 .. (n - 1) + i <- 0 .. (n - 1) + if S.member (location i j) occupied then [] else [ location i j ] -{- - would be nice to print a graphical version of the board - +━━━━∧ - ┃ 1 ┃ - > 1 1 > - ┃ 1 ┃ - +━━━━∨ ━━━+ --} printBoard :: Board -> String printBoard (Board b) = "SHOW BOARD\n" <> (foldMap (_ <> "\n") $ interleave colEdges rows ) where diff --git a/src/Game/Level.purs b/src/Game/Level.purs index f310f1d..f5a8ccf 100644 --- a/src/Game/Level.purs +++ b/src/Game/Level.purs @@ -6,7 +6,9 @@ import Data.Array (zip, zipWith) import Data.HeytingAlgebra (ff, tt) import Data.Map (Map) import Data.Map as M +import Data.Maybe (Maybe(..)) import Data.Traversable (traverse) +import Effect (Effect) import Foreign.Object (Object) import Game.Direction (CardinalDirection) import Game.GameEvent (GameEventStore) @@ -19,7 +21,9 @@ import Web.HTML.Common (AttrName(..)) type LevelOptions = { enableBoardSizeChange :: Boolean - , compulsory :: Boolean } + , compulsory :: Boolean + , tutorial :: Maybe (Effect Unit) + } type Level = { problem :: Problem @@ -34,6 +38,7 @@ defaultLevelOptions :: LevelOptions defaultLevelOptions = { enableBoardSizeChange: true , compulsory: false + , tutorial: Nothing } defaultLevel :: Level diff --git a/src/Game/Level/Completion.purs b/src/Game/Level/Completion.purs index 2709b73..8f3a917 100644 --- a/src/Game/Level/Completion.purs +++ b/src/Game/Level/Completion.purs @@ -8,12 +8,13 @@ import Data.Bifunctor (lmap) import Data.Either (Either(..), fromLeft) import Data.Foldable (for_) import Data.Map (Map) -import Data.Maybe (Maybe) +import Data.Maybe (Maybe(..), fromMaybe, maybe) import Game.Board (Board(..), BoardError, EvaluableBoard(..), evaluableBoardPiece, toEvaluableBoard) +import Game.Capacity (Capacity) import Game.Direction (CardinalDirection, allDirections) import Game.Level.Problem (Problem) import Game.Piece (Piece(..), eval, getPort) -import Game.Port (Port(..)) +import Game.Port (Port(..), PortType, portCapacity) import Game.Signal (Signal(..)) data CompletionStatus @@ -34,11 +35,12 @@ derive instance Eq CompletionStatus -- "differnt port at:" <> show r.dir <> ": " <> showMismatch r -- FailedRestriction r -> "Failed restriction " <> r.name <> " with message " <> r.description -type PortMismatch = - { dir :: CardinalDirection - , received :: Maybe Port - , expected :: Maybe Port - } +data PortMismatch + = PortExpected { direction :: CardinalDirection, expected :: Port } + | NoPortExpected { direction :: CardinalDirection, received :: Port } + | IncorrectPortType { direction :: CardinalDirection, capacity :: Capacity, received :: PortType, expected :: PortType } + | IncorrectCapacity { direction :: CardinalDirection, portType :: PortType, received :: Capacity, expected :: Capacity } +derive instance Eq PortMismatch type FailedTestCase = { inputs :: Map CardinalDirection Signal @@ -65,8 +67,20 @@ checkPortMismatch :: Problem -> Piece -> Either PortMismatch Unit checkPortMismatch problem evaluable = for_ allDirections \dir -> do let expected = getPort problem.goal dir let received = getPort evaluable dir - when (expected /= received) do - throwError { dir, expected, received } + maybe (Right unit) Left (isPortMismatch dir expected received) + +isPortMismatch :: CardinalDirection -> Maybe Port -> Maybe Port -> Maybe PortMismatch +isPortMismatch direction maybeExpected maybeRecieved = case maybeExpected, maybeRecieved of + Nothing, Nothing -> Nothing + Just expected, Nothing -> Just $ PortExpected { direction, expected } + Nothing, Just received -> Just $ NoPortExpected { direction, received } + Just (Port expected), Just (Port received) -> + if expected.portType /= received.portType + then Just $ IncorrectPortType { direction, capacity: received.capacity, expected: expected.portType, received: received.portType } + else if expected.capacity /= received.capacity + then Just $ IncorrectCapacity { direction, portType: received.portType, expected: expected.capacity, received: received.capacity } + else + Nothing checkOtherRestrictions :: Problem -> Board -> Either FailedRestriction Unit checkOtherRestrictions problem board = for_ problem.otherRestrictions \r -> diff --git a/src/Game/Location.purs b/src/Game/Location.purs index 6791ab7..aeca9f0 100644 --- a/src/Game/Location.purs +++ b/src/Game/Location.purs @@ -7,6 +7,7 @@ import Data.Enum (class BoundedEnum, class Enum, Cardinality(..), cardinality, e import Data.Group (class Group, ginverse) import Data.Maybe (Maybe(..), fromMaybe) import Data.Newtype (class Newtype) +import Data.Ord (abs) import Data.Tuple (Tuple(..)) import Game.Direction (CardinalDirection, allDirections) import Game.Direction as Direction @@ -40,4 +41,5 @@ followDirection (Location {x, y}) = case _ of directionTo :: Location -> Location -> Maybe CardinalDirection directionTo l1 l2 = find (\d -> followDirection l1 d == l2) allDirections - +taxicabDistance :: Location -> Location -> Int +taxicabDistance (Location a) (Location b) = abs (a.x - b.x) + abs (a.y - b.y) diff --git a/src/IO/Levels/IntermediateSuite.purs b/src/IO/Levels/IntermediateSuite.purs index e510da2..bed8984 100644 --- a/src/IO/Levels/IntermediateSuite.purs +++ b/src/IO/Levels/IntermediateSuite.purs @@ -9,6 +9,7 @@ import Foreign.Object (fromHomogeneous) import Game.Direction as Direction import Game.GameEvent (count, firstTime, latest, pieceAdded, pieceMovedTo, secondTime) import Game.Level (LevelSuite, binaryTestInputs, defaultLevel) +import Game.Level.Problem (defaultProblem) import Game.Message (addDelay, message) import Game.Piece (andPiece, crossPiece, idPiece, leftPiece, name, notPiece, orPiece, rightPiece, superPiece, xorPiece) import Game.Piece.BasicPiece (allBasicPieces, crossPiece, xorPiece) @@ -50,10 +51,19 @@ intermediateSuite = fromHomogeneous , conversation = addDelay <$> [ message "test" "criss cross" ] } - , "Exclusive Or": defaultLevel + , "From Or, birthed And": defaultLevel + { problem = defaultProblem + { goal = andPiece + , title ="From Or, birthed And" + , description = "Create an and-piece using only or-piece and not-piece" + , testCases = binaryTestInputs [ Direction.Left, Direction.Up ] + , availablePieces = [ orPiece, notPiece ] + } + } + , "Exclusive Or: Pick One": defaultLevel { problem = { goal: xorPiece - , title: "Exclusive Or" + , title: "Exclusive Or: Pick One" , description: "Output true when EXACTLY one input is true. If both inputs are true, output false" , testCases: binaryTestInputs [ Direction.Left, Direction.Up ] , requiresAutomaticTesting: false diff --git a/src/IO/Levels/TutorialSuite.purs b/src/IO/Levels/TutorialSuite.purs index 0e3f071..3bc2f55 100644 --- a/src/IO/Levels/TutorialSuite.purs +++ b/src/IO/Levels/TutorialSuite.purs @@ -52,7 +52,9 @@ tutorialSuite = fromHomogeneous -- selector DataAttr.location l3 ] , conversation = mapWithIndex (\i m -> if i == 0 then m else addDelay m) $ - [] + [ fromGuide "Welcome to ABED!" + + ] --[ fromGuide "welcome to ABED! click 'id' to get started adding pieces!" # -- _selector .~ Just (selector DataAttr.availablePiece (name idPiece)) --]