Added strategy
This commit is contained in:
parent
553809fc56
commit
8ebe233bec
|
@ -21,5 +21,6 @@
|
|||
| [purpleflow](./purpleflow) | Unfinished dependently-typed programming language |
|
||||
| [slice](./slice) | Basic benchmarks and a `Slice` type |
|
||||
| [sprint](./sprint) | Failled effect-system based on typelevel lists |
|
||||
| [strategy](./strategy) | Unfinished attempt at implementing a mixed strategy nash equilibrium solver |
|
||||
| [streams](./streams) | Playing with `purescript-pipes` |
|
||||
| [typelevel](./typelevel) | Typelevel naturals, vectors, sum-types, orderings and lambda-calculus evaluation and a value-level bounded-type GADT |
|
||||
|
|
10
purescript/strategy/.gitignore
vendored
Normal file
10
purescript/strategy/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/bower_components/
|
||||
/node_modules/
|
||||
/.pulp-cache/
|
||||
/output/
|
||||
/generated-docs/
|
||||
/.psc-package/
|
||||
/.psc*
|
||||
/.purs*
|
||||
/.psa*
|
||||
/.spago
|
105
purescript/strategy/packages.dhall
Normal file
105
purescript/strategy/packages.dhall
Normal file
|
@ -0,0 +1,105 @@
|
|||
{-
|
||||
Welcome to your new Dhall package-set!
|
||||
|
||||
Below are instructions for how to edit this file for most use
|
||||
cases, so that you don't need to know Dhall to use it.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Most will want to do one or both of these options:
|
||||
1. Override/Patch a package's dependency
|
||||
2. Add a package not already in the default package set
|
||||
|
||||
This file will continue to work whether you use one or both options.
|
||||
Instructions for each option are explained below.
|
||||
|
||||
### Overriding/Patching a package
|
||||
|
||||
Purpose:
|
||||
- Change a package's dependency to a newer/older release than the
|
||||
default package set's release
|
||||
- Use your own modified version of some dependency that may
|
||||
include new API, changed API, removed API by
|
||||
using your custom git repo of the library rather than
|
||||
the package set's repo
|
||||
|
||||
Syntax:
|
||||
where `entityName` is one of the following:
|
||||
- dependencies
|
||||
- repo
|
||||
- version
|
||||
-------------------------------
|
||||
let upstream = --
|
||||
in upstream
|
||||
with packageName.entityName = "new value"
|
||||
-------------------------------
|
||||
|
||||
Example:
|
||||
-------------------------------
|
||||
let upstream = --
|
||||
in upstream
|
||||
with halogen.version = "master"
|
||||
with halogen.repo = "https://example.com/path/to/git/repo.git"
|
||||
|
||||
with halogen-vdom.version = "v4.0.0"
|
||||
with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies
|
||||
-------------------------------
|
||||
|
||||
### Additions
|
||||
|
||||
Purpose:
|
||||
- Add packages that aren't already included in the default package set
|
||||
|
||||
Syntax:
|
||||
where `<version>` is:
|
||||
- a tag (i.e. "v4.0.0")
|
||||
- a branch (i.e. "master")
|
||||
- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977")
|
||||
-------------------------------
|
||||
let upstream = --
|
||||
in upstream
|
||||
with new-package-name =
|
||||
{ dependencies =
|
||||
[ "dependency1"
|
||||
, "dependency2"
|
||||
]
|
||||
, repo =
|
||||
"https://example.com/path/to/git/repo.git"
|
||||
, version =
|
||||
"<version>"
|
||||
}
|
||||
-------------------------------
|
||||
|
||||
Example:
|
||||
-------------------------------
|
||||
let upstream = --
|
||||
in upstream
|
||||
with benchotron =
|
||||
{ dependencies =
|
||||
[ "arrays"
|
||||
, "exists"
|
||||
, "profunctor"
|
||||
, "strings"
|
||||
, "quickcheck"
|
||||
, "lcg"
|
||||
, "transformers"
|
||||
, "foldable-traversable"
|
||||
, "exceptions"
|
||||
, "node-fs"
|
||||
, "node-buffer"
|
||||
, "node-readline"
|
||||
, "datetime"
|
||||
, "now"
|
||||
]
|
||||
, repo =
|
||||
"https://github.com/hdgarrood/purescript-benchotron.git"
|
||||
, version =
|
||||
"v7.0.0"
|
||||
}
|
||||
-------------------------------
|
||||
-}
|
||||
let upstream =
|
||||
https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220921/packages.dhall
|
||||
sha256:169bd823a71ae033eaf4f77776e184f12c656163feae14e7f649a48932ca6ac0
|
||||
|
||||
in upstream
|
30
purescript/strategy/spago.dhall
Normal file
30
purescript/strategy/spago.dhall
Normal file
|
@ -0,0 +1,30 @@
|
|||
{-
|
||||
Welcome to a Spago project!
|
||||
You can edit this file as you like.
|
||||
|
||||
Need help? See the following resources:
|
||||
- Spago documentation: https://github.com/purescript/spago
|
||||
- Dhall language tour: https://docs.dhall-lang.org/tutorials/Language-Tour.html
|
||||
|
||||
When creating a new Spago project, you can use
|
||||
`spago init --no-comments` or `spago init -C`
|
||||
to generate this file without the comments in this block.
|
||||
-}
|
||||
{ name = "my-project"
|
||||
, dependencies =
|
||||
[ "arrays"
|
||||
, "console"
|
||||
, "dodo-printer"
|
||||
, "effect"
|
||||
, "either"
|
||||
, "fast-vect"
|
||||
, "foldable-traversable"
|
||||
, "maybe"
|
||||
, "prelude"
|
||||
, "random"
|
||||
, "safe-coerce"
|
||||
, "tuples"
|
||||
]
|
||||
, packages = ./packages.dhall
|
||||
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
|
||||
}
|
203
purescript/strategy/src/Data/Game.purs
Normal file
203
purescript/strategy/src/Data/Game.purs
Normal file
|
@ -0,0 +1,203 @@
|
|||
module EG.Data.Game where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Data.Number as Number
|
||||
import Data.Array as Array
|
||||
import Data.Foldable (maximum, maximumBy, sum)
|
||||
import Data.Function (on)
|
||||
import Data.Maybe (Maybe(..), fromMaybe, maybe)
|
||||
import Data.Number (infinity)
|
||||
import Data.Profunctor.Strong (second, (&&&))
|
||||
import Data.Tuple (fst, snd)
|
||||
import Data.Tuple.Nested (type (/\), (/\))
|
||||
import Dodo (Doc)
|
||||
import Dodo as DD
|
||||
import EG.Data.Payoff (Payoff(..))
|
||||
import EG.Data.Tuple (TupleIndex)
|
||||
import EG.Data.Tuple as ETuple
|
||||
import Effect (Effect)
|
||||
import Effect.Random (randomInt)
|
||||
import Safe.Coerce (coerce)
|
||||
|
||||
type Player = TupleIndex
|
||||
|
||||
newtype Game state choice = Game
|
||||
{ state :: state
|
||||
, choices :: Array (choice /\ (Game state choice))
|
||||
}
|
||||
|
||||
newtype GameReplay state choice = GameReplay
|
||||
{ state :: state
|
||||
, next :: Maybe (choice /\ GameReplay state choice)
|
||||
}
|
||||
|
||||
newtype SolvedState state choice = SolvedState
|
||||
{ state :: state
|
||||
, choice :: Maybe (choice /\ Game (SolvedState state choice) choice)
|
||||
, payoffs :: Maybe (Payoff /\ Payoff)
|
||||
}
|
||||
|
||||
type PayoffFunction s = Int -> s -> Maybe (Payoff /\ Payoff)
|
||||
|
||||
---------- Helpers
|
||||
solvedStatePayoffs :: forall s c. SolvedState s c -> Maybe (Payoff /\ Payoff)
|
||||
solvedStatePayoffs (SolvedState { payoffs }) = payoffs
|
||||
|
||||
gameNodeCount :: forall s c. Game s c -> Int
|
||||
gameNodeCount (Game { choices }) = 1 + sum (map (gameNodeCount <<< snd) choices)
|
||||
|
||||
gameLeafCount :: forall s c. Game s c -> Int
|
||||
gameLeafCount (Game { choices: [] }) = 1
|
||||
gameLeafCount (Game { choices }) = sum (map (gameLeafCount <<< snd) choices)
|
||||
|
||||
gameDepth :: forall s c. Game s c -> Int
|
||||
gameDepth (Game { choices }) = maybe 0 ((+) 1)
|
||||
$ maximum
|
||||
$ map (snd >>> gameDepth) choices
|
||||
|
||||
randomGame :: forall s c. Game s c -> Effect (GameReplay s c)
|
||||
randomGame (Game { choices, state }) = do
|
||||
index <- randomInt 0 (Array.length choices - 1)
|
||||
next <- case Array.index choices index of
|
||||
Nothing -> pure Nothing
|
||||
Just (choice /\ next) -> do
|
||||
next <- randomGame next
|
||||
pure (Just (choice /\ next))
|
||||
pure $ GameReplay { state, next }
|
||||
|
||||
displayReplay :: forall s c a. (s -> Doc a) -> (c -> Doc a) -> GameReplay s c -> Doc a
|
||||
displayReplay printState printChoice (GameReplay { state, next }) =
|
||||
DD.lines
|
||||
[ DD.indent (printState state)
|
||||
, case next of
|
||||
Nothing -> mempty
|
||||
Just (choice /\ next) ->
|
||||
DD.lines
|
||||
[ Array.intercalate DD.space
|
||||
[ DD.text "----"
|
||||
, printChoice choice
|
||||
, DD.text "---->"
|
||||
]
|
||||
, displayReplay printState printChoice next
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
gameState :: forall s c. Game s c -> s
|
||||
gameState (Game { state }) = state
|
||||
|
||||
computePayoffs
|
||||
:: forall s c
|
||||
. Player
|
||||
-> PayoffFunction s
|
||||
-> Game s c
|
||||
-> Game (SolvedState s c) c
|
||||
computePayoffs player calculatePayoffs game = go player 0 game
|
||||
where
|
||||
go player depth (Game { state, choices }) = case choices of
|
||||
[] -> Game
|
||||
{ choices: []
|
||||
, state: SolvedState
|
||||
{ state
|
||||
, payoffs: calculatePayoffs depth state
|
||||
, choice: Nothing
|
||||
}
|
||||
}
|
||||
choices -> Game
|
||||
{ choices: nested
|
||||
, state: SolvedState
|
||||
{ state
|
||||
, payoffs: map _.payoff optimalChoice
|
||||
, choice: map (_.choice &&& _.game) optimalChoice
|
||||
}
|
||||
}
|
||||
where
|
||||
nested = choices
|
||||
# map (second (go (ETuple.other player) (depth + 1)))
|
||||
|
||||
optimalChoice = nested
|
||||
# map
|
||||
( \(choice /\ game) ->
|
||||
{ choice
|
||||
, game
|
||||
, payoff: game
|
||||
# gameState
|
||||
# solvedStatePayoffs
|
||||
# fromMaybe (hell /\ hell)
|
||||
}
|
||||
)
|
||||
# maximumBy (on compare (_.payoff >>> ETuple.lookupPair player))
|
||||
|
||||
hell = Payoff (-infinity)
|
||||
|
||||
-- minmax :: forall s c. PayoffFunction s -> Player -> Game s c -> Maybe (c /\ Game s c)
|
||||
-- minmax calculatePayoffs player (Game { choices, state }) =
|
||||
-- choices
|
||||
-- # Array.mapMaybe
|
||||
-- ( \(choice /\ game) -> case calculatePayoffs state of
|
||||
-- Just payoffs -> Just { choice, game, payoffs }
|
||||
-- Nothing -> Nothing
|
||||
-- )
|
||||
-- # maximumBy (on compare (_.payoffs >>> ETuple.lookup player))
|
||||
-- # map \{ choice, game } -> choice /\ game
|
||||
|
||||
---------- Display stuff
|
||||
displaySolvedState :: forall a s c. (s -> Doc a) -> (c -> Doc a) -> SolvedState s c -> Doc a
|
||||
displaySolvedState displayState displayChoice (SolvedState { payoffs, choice, state }) = DD.lines
|
||||
[ DD.indent (displayState state)
|
||||
, Array.intercalate DD.space
|
||||
[ DD.text "Payoffs:"
|
||||
, displayUnknown displayPayoffs payoffs
|
||||
]
|
||||
, Array.intercalate DD.space
|
||||
[ DD.text "Optimal choice:"
|
||||
, displayUnknown displayChoice (map fst choice)
|
||||
]
|
||||
]
|
||||
|
||||
where
|
||||
displayUnknown :: forall e. (e -> Doc a) -> Maybe e -> Doc a
|
||||
displayUnknown f (Just a) = f a
|
||||
displayUnknown f Nothing = DD.text "???"
|
||||
|
||||
displayPayoffs (l /\ r) = Array.fold
|
||||
[ DD.text "("
|
||||
, displayNum (coerce l)
|
||||
, DD.text ","
|
||||
, DD.space
|
||||
, displayNum (coerce r)
|
||||
, DD.text ")"
|
||||
]
|
||||
where
|
||||
displayNum num = DD.text (show $ Number.floor (num * precision) / precision)
|
||||
precision = 1000.0
|
||||
|
||||
-- newtype Payoff = Payoff Int
|
||||
--
|
||||
-- newtype PayoffTreeNode label state choice = PayoffTreeNode
|
||||
-- { expectedPayoff :: Payoff /\ Payoff
|
||||
-- , children :: PayoffTree label state choicehoi--
|
||||
--
|
||||
-- newtype PayoffTree label state choice = PayoffTree
|
||||
-- { label :: label
|
||||
-- , state :: state
|
||||
-- , choices :: Array (PayoffTreeNode label state choice)
|
||||
-- }
|
||||
|
||||
-- minmax
|
||||
-- :: forall label choice
|
||||
-- . Foldable choice
|
||||
-- => Player
|
||||
-- -> PayoffTree label choice
|
||||
-- -> PayoffTreeNode label choice
|
||||
-- minmax player (PayoffTree { label, choices }) = choice
|
||||
-- where
|
||||
-- choice = choices # maximumBy
|
||||
-- ( on compare \(PayoffTreeNode { expectedPayoff, children }) ->
|
||||
-- ETuple.lookup player expectedPayoff
|
||||
-- )
|
||||
|
||||
---------- Typeclass instances
|
||||
-- derive instance Eq Payoff
|
||||
-- derive instance Ord Payoff
|
13
purescript/strategy/src/Data/Payoff.purs
Normal file
13
purescript/strategy/src/Data/Payoff.purs
Normal file
|
@ -0,0 +1,13 @@
|
|||
module EG.Data.Payoff where
|
||||
|
||||
import Prelude
|
||||
import Data.FastVect.FastVect (Vect)
|
||||
|
||||
newtype Payoff = Payoff Number
|
||||
|
||||
newtype PayoffMatrix p1 p2 = PayoffMatrix (Vect p1 (Vect p2 Payoff))
|
||||
|
||||
---------- Typeclass instances
|
||||
derive instance Eq Payoff
|
||||
derive instance Ord Payoff
|
||||
derive newtype instance Show Payoff
|
32
purescript/strategy/src/Data/Tuple.purs
Normal file
32
purescript/strategy/src/Data/Tuple.purs
Normal file
|
@ -0,0 +1,32 @@
|
|||
module EG.Data.Tuple (TupleIndex, left, right, lookupPair, lookup, other) where
|
||||
|
||||
import Data.Either (Either(..))
|
||||
import Data.Either.Nested (type (\/))
|
||||
import Data.HeytingAlgebra (not)
|
||||
import Data.Tuple.Nested (type (/\), (/\))
|
||||
import Safe.Coerce (coerce)
|
||||
|
||||
-- | Position in a tuple.
|
||||
-- | Logically equivalent to a boolean
|
||||
-- | Constructable using `left` and `right`
|
||||
newtype TupleIndex = TupleIndex Boolean
|
||||
|
||||
other :: TupleIndex -> TupleIndex
|
||||
other = coerce (not :: Boolean -> Boolean)
|
||||
|
||||
left :: TupleIndex
|
||||
left = TupleIndex false
|
||||
|
||||
right :: TupleIndex
|
||||
right = TupleIndex true
|
||||
|
||||
lookupPair :: forall a. TupleIndex -> a /\ a -> a
|
||||
lookupPair (TupleIndex inner) (a /\ b) =
|
||||
if inner then b
|
||||
else a
|
||||
|
||||
lookup :: forall a b. TupleIndex -> a /\ b -> a \/ b
|
||||
lookup (TupleIndex inner) (a /\ b) =
|
||||
if inner then Right b
|
||||
else Left a
|
||||
|
188
purescript/strategy/src/Games/Tictactoe.purs
Normal file
188
purescript/strategy/src/Games/Tictactoe.purs
Normal file
|
@ -0,0 +1,188 @@
|
|||
module EG.Games.Tictactoe where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Data.Array (findMap, foldl, intercalate)
|
||||
import Data.Array as Array
|
||||
import Data.Int as Int
|
||||
import Data.Maybe (Maybe(..), isJust)
|
||||
import Data.Tuple.Nested (type (/\), (/\))
|
||||
import Dodo (Doc)
|
||||
import Dodo as DD
|
||||
import EG.Data.Game (Game(..))
|
||||
import EG.Data.Payoff (Payoff(..))
|
||||
import EG.Data.Tuple as ETuple
|
||||
|
||||
---------- Types
|
||||
data Piece = X | O
|
||||
|
||||
type Board = Array (Maybe Piece)
|
||||
type Player = ETuple.TupleIndex
|
||||
|
||||
data TurnState = NextMove Player | WonBy Player | Draw
|
||||
|
||||
newtype State = State
|
||||
{ board :: Board
|
||||
, turnState :: TurnState
|
||||
}
|
||||
|
||||
type GameGenerationState = { board :: Board, player :: Player }
|
||||
|
||||
type Choice = Int -- mod 9
|
||||
|
||||
type TictactoeGame = Game State Choice
|
||||
|
||||
---------- Helpers
|
||||
initialState :: GameGenerationState
|
||||
initialState =
|
||||
{ board: Array.replicate 9 Nothing
|
||||
, player: ETuple.left
|
||||
}
|
||||
|
||||
pieceToPlayer :: Piece -> Player
|
||||
pieceToPlayer X = ETuple.left
|
||||
pieceToPlayer O = ETuple.right
|
||||
|
||||
playerToPiece :: Player -> Piece
|
||||
playerToPiece player = ETuple.lookupPair player (X /\ O)
|
||||
|
||||
winner :: Board -> Maybe Player
|
||||
winner = case _ of
|
||||
[ a11
|
||||
, a12
|
||||
, a13
|
||||
, a21
|
||||
, a22
|
||||
, a23
|
||||
, a31
|
||||
, a32
|
||||
, a33
|
||||
-- For 10 element version
|
||||
-- , a41
|
||||
] ->
|
||||
patterns # findMap (allEqual >=> map pieceToPlayer)
|
||||
where
|
||||
patterns =
|
||||
[ [ a11, a12, a13 ]
|
||||
, [ a21, a22, a23 ]
|
||||
, [ a31, a32, a33 ]
|
||||
, [ a11, a21, a31 ]
|
||||
, [ a12, a22, a32 ]
|
||||
, [ a13, a23, a33 ]
|
||||
, [ a11, a22, a33 ]
|
||||
, [ a13, a22, a31 ]
|
||||
-- for weird version
|
||||
-- , [ a11, a23, a32 ]
|
||||
-- , [ a33, a12, a21 ]
|
||||
-- even more weird version
|
||||
-- , [ a13, a32, a21 ]
|
||||
-- , [ a31, a23, a12 ]
|
||||
|
||||
-- for 10 element version
|
||||
-- , [ a21, a31, a41 ]
|
||||
-- , [ a23, a32, a41 ]
|
||||
|
||||
-- for 12 element version
|
||||
-- , [ a41, a42, a43 ]
|
||||
-- , [ a21, a31, a41 ]
|
||||
-- , [ a22, a32, a42 ]
|
||||
-- , [ a23, a33, a43 ]
|
||||
-- , [ a21, a32, a43 ]
|
||||
-- , [ a23, a32, a41 ]
|
||||
]
|
||||
_ -> Nothing
|
||||
where
|
||||
allEqual :: forall a. Eq a => Array a -> Maybe a
|
||||
allEqual = foldl go (Just Nothing) >>> join
|
||||
where
|
||||
go (Just Nothing) e = Just (Just e)
|
||||
go whole@(Just (Just p)) e
|
||||
| e == p = whole
|
||||
go _ _ = Nothing
|
||||
|
||||
---------- Game related functions
|
||||
game :: TictactoeGame
|
||||
game = go initialState
|
||||
where
|
||||
go { board, player }
|
||||
| Just player <- winner board = Game
|
||||
{ state: State { board, turnState: WonBy player }
|
||||
, choices: []
|
||||
}
|
||||
| Array.all isJust board = Game
|
||||
{ state: State { board, turnState: Draw }
|
||||
, choices: []
|
||||
}
|
||||
| otherwise = Game
|
||||
{ state: State { board, turnState: NextMove player }
|
||||
, choices: do
|
||||
emptySpot <- board
|
||||
# Array.mapWithIndex (/\)
|
||||
# Array.mapMaybe \(index /\ piece) -> case piece of
|
||||
Nothing -> Just index
|
||||
Just _ -> Nothing
|
||||
|
||||
let
|
||||
state =
|
||||
{ player: ETuple.other player
|
||||
, board: board #
|
||||
Array.modifyAtIndices
|
||||
[ emptySpot ]
|
||||
(const $ Just $ playerToPiece player)
|
||||
}
|
||||
|
||||
pure (emptySpot /\ (go state))
|
||||
}
|
||||
|
||||
payoff :: Int -> State -> Maybe (Payoff /\ Payoff)
|
||||
payoff depth (State { turnState }) = case turnState of
|
||||
WonBy player -> Just $ ETuple.lookupPair player ((pos /\ neg) /\ (neg /\ pos))
|
||||
Draw -> Just (mid /\ mid)
|
||||
_ -> Nothing
|
||||
where
|
||||
depth' = Int.toNumber depth
|
||||
pos = Payoff (1.0 / depth')
|
||||
mid = Payoff 0.0
|
||||
neg = Payoff (-1.0 / depth')
|
||||
|
||||
---------- Display stuff
|
||||
displayChoice :: forall a. Choice -> Doc a
|
||||
displayChoice num = Array.intercalate DD.space
|
||||
[ DD.text "Place a piece at"
|
||||
, DD.text "("
|
||||
<> DD.text (show (num `mod` 3))
|
||||
<> DD.text ","
|
||||
<> DD.space
|
||||
<> DD.text (show (num / 3))
|
||||
<> DD.text ")"
|
||||
]
|
||||
|
||||
displayState :: forall a. State -> Doc a
|
||||
displayState (State { board, turnState }) = DD.lines
|
||||
[ intercalate DD.space (map printPiece $ Array.slice 0 3 board)
|
||||
, intercalate DD.space (map printPiece $ Array.slice 3 6 board)
|
||||
, intercalate DD.space (map printPiece $ Array.slice 6 9 board)
|
||||
|
||||
-- for 10 element version
|
||||
-- , intercalate DD.space (map printPiece $ Array.slice 9 10 board)
|
||||
, DD.text "State: " <> printTurnState turnState
|
||||
]
|
||||
where
|
||||
printPiece (Just X) = DD.text "X"
|
||||
printPiece (Just O) = DD.text "O"
|
||||
printPiece Nothing = DD.text "-"
|
||||
|
||||
printTurnState Draw = DD.text "Draw!"
|
||||
printTurnState (NextMove player) = Array.intercalate DD.space
|
||||
[ DD.text "player "
|
||||
, DD.text (ETuple.lookupPair player ("1" /\ "2"))
|
||||
, DD.text "should make a move"
|
||||
]
|
||||
printTurnState (WonBy player) = Array.intercalate DD.space
|
||||
[ DD.text "won by - "
|
||||
, DD.text (ETuple.lookupPair player ("X player" /\ "O player"))
|
||||
]
|
||||
|
||||
---------- Typeclass instances
|
||||
derive instance Eq Piece
|
||||
|
10
purescript/strategy/src/Main.purs
Normal file
10
purescript/strategy/src/Main.purs
Normal file
|
@ -0,0 +1,10 @@
|
|||
module Main where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Effect (Effect)
|
||||
import Effect.Console (log)
|
||||
|
||||
main :: Effect Unit
|
||||
main = do
|
||||
log "🍝"
|
11
purescript/strategy/test/Main.purs
Normal file
11
purescript/strategy/test/Main.purs
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Test.Main where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Effect (Effect)
|
||||
import Effect.Class.Console (log)
|
||||
|
||||
main :: Effect Unit
|
||||
main = do
|
||||
log "🍝"
|
||||
log "You should add some tests."
|
Loading…
Reference in a new issue