packages feed

swarm-0.1.0.0: src/Swarm/App.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      :  Swarm.App
-- Copyright   :  Brent Yorgey
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Main entry point for the Swarm application.
module Swarm.App where

import Brick
import Brick.BChan
import Control.Concurrent (forkIO, threadDelay)
import Control.Lens ((%~), (&), (?~), (^.))
import Control.Monad.Except
import Data.IORef (newIORef, writeIORef)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Graphics.Vty qualified as V
import Swarm.Game.Robot (LogSource (ErrorTrace, Said))
import Swarm.TUI.Attr
import Swarm.TUI.Controller
import Swarm.TUI.Model
import Swarm.TUI.View
import Swarm.Version (getNewerReleaseVersion)
import Swarm.Web
import System.IO (stderr)

type EventHandler = BrickEvent Name AppEvent -> EventM Name AppState ()

-- | The definition of the app used by the @brick@ library.
app :: EventHandler -> App AppState AppEvent Name
app eventHandler =
  App
    { appDraw = drawUI
    , appChooseCursor = chooseCursor
    , appHandleEvent = eventHandler
    , appStartEvent = enablePasteMode
    , appAttrMap = const swarmAttrMap
    }

-- | The main @IO@ computation which initializes the state, sets up
--   some communication channels, and runs the UI.
appMain :: AppOpts -> IO ()
appMain opts = do
  res <- runExceptT $ initAppState opts
  case res of
    Left errMsg -> T.hPutStrLn stderr errMsg
    Right s -> do
      -- Send Frame events as at a reasonable rate for 30 fps. The
      -- game is responsible for figuring out how many steps to take
      -- each frame to achieve the desired speed, regardless of the
      -- frame rate.  Note that if the game cannot keep up with 30
      -- fps, it's not a problem: the channel will fill up and this
      -- thread will block.  So the force of the threadDelay is just
      -- to set a *maximum* possible frame rate.
      --
      -- 5 is the size of the bounded channel; when it gets that big,
      -- any writes to it will block.  Probably 1 would work fine,
      -- though it seems like it could be good to have a bit of buffer
      -- just so the app never has to wait for the thread to wake up
      -- and do another write.

      chan <- newBChan 5
      _ <- forkIO $
        forever $ do
          threadDelay 33_333 -- cap maximum framerate at 30 FPS
          writeBChan chan Frame

      _ <- forkIO $ do
        upRel <- getNewerReleaseVersion
        writeBChan chan (UpstreamVersion upRel)

      -- Start the web service with a reference to the game state
      gsRef <- newIORef (s ^. gameState)
      eport <- Swarm.Web.startWebThread (userWebPort opts) gsRef

      let logP p = logEvent Said ("Web API", -2) ("started on :" <> T.pack (show p))
      let logE e = logEvent ErrorTrace ("Web API", -2) (T.pack e)
      let s' =
            s & runtimeState
              %~ case eport of
                Right p -> (webPort ?~ p) . (eventLog %~ logP p)
                Left e -> eventLog %~ logE e

      -- Update the reference for every event
      let eventHandler e = do
            curSt <- get
            liftIO $ writeIORef gsRef (curSt ^. gameState)
            handleEvent e

      -- Run the app.
      let buildVty = V.mkVty V.defaultConfig
      initialVty <- buildVty
      V.setMode (V.outputIface initialVty) V.Mouse True
      void $ customMain initialVty buildVty (Just chan) (app eventHandler) s'

-- | A demo program to run the web service directly, without the terminal application.
-- This is useful to live update the code using `ghcid -W --test "Swarm.App.demoWeb"`
demoWeb :: IO ()
demoWeb = do
  let demoPort = 8080
  res <-
    runExceptT $
      initAppState $
        AppOpts
          { userSeed = Nothing
          , userScenario = demoScenario
          , toRun = Nothing
          , cheatMode = False
          , userWebPort = Nothing
          }
  case res of
    Left errMsg -> T.putStrLn errMsg
    Right s -> do
      gsRef <- newIORef (s ^. gameState)
      webMain Nothing demoPort gsRef
 where
  demoScenario = Just "./data/scenarios/Testing/475-wait-one.yaml"

-- | If available for the terminal emulator, enable bracketed paste mode.
enablePasteMode :: EventM n s ()
enablePasteMode = do
  vty <- getVtyHandle
  let output = V.outputIface vty
  when (V.supportsMode output V.BracketedPaste) $
    liftIO $
      V.setMode output V.BracketedPaste True