diff --git a/cabal.project b/cabal.project index 434bed5..edc8e3c 100644 --- a/cabal.project +++ b/cabal.project @@ -1,7 +1,7 @@ packages: - . + ./haskell/ --- source-repository-package --- type: git --- location: https://github.com/eskimor/purescript-bridge.git --- tag: 793cd8206ae777dd8f77245c1c8dbeb597c7828f +source-repository-package + type: git + location: https://github.com/eskimor/purescript-bridge.git + tag: 793cd8206ae777dd8f77245c1c8dbeb597c7828f diff --git a/CHANGELOG.md b/haskell/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to haskell/CHANGELOG.md diff --git a/LICENSE b/haskell/LICENSE similarity index 100% rename from LICENSE rename to haskell/LICENSE diff --git a/app/GeneratePurescript.hs b/haskell/app/GeneratePurescript.hs similarity index 100% rename from app/GeneratePurescript.hs rename to haskell/app/GeneratePurescript.hs diff --git a/app/Main.hs b/haskell/app/Main.hs similarity index 100% rename from app/Main.hs rename to haskell/app/Main.hs diff --git a/halogen-chess.cabal b/haskell/halogen-chess.cabal similarity index 100% rename from halogen-chess.cabal rename to haskell/halogen-chess.cabal diff --git a/haskell/src/Bridge.hs b/haskell/src/Bridge.hs new file mode 100644 index 0000000..3212cd9 --- /dev/null +++ b/haskell/src/Bridge.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeOperators #-} + +module Bridge where + +import Data.Proxy (Proxy (..)) +import GHC.Generics (Generic) +import Language.PureScript.Bridge + (BridgePart, Language (Haskell), SumType, defaultBridge, mkSumType) +import Language.PureScript.Bridge.PSTypes () + +import Game.Chess (Color (..), PieceType (..), Ply (..), Position (..), Sq (..)) + +import Game.Chess.Board (Board) +import Game.Chess.Move (Move) + +myBridge :: BridgePart +myBridge = defaultBridge + +myTypes :: [SumType 'Haskell] +myTypes = + [ mkSumType (Proxy :: Proxy Color) + , mkSumType (Proxy :: Proxy Sq) + , mkSumType (Proxy :: Proxy PieceType) + , mkSumType (Proxy :: Proxy Board) + , mkSumType (Proxy :: Proxy Move) + -- , mkSumType (Proxy :: Proxy Ply) + -- , mkSumType (Proxy :: Proxy Position) + ] diff --git a/haskell/src/Game/Chess/Board.hs b/haskell/src/Game/Chess/Board.hs new file mode 100644 index 0000000..3e371d0 --- /dev/null +++ b/haskell/src/Game/Chess/Board.hs @@ -0,0 +1,60 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE ViewPatterns #-} + +module Game.Chess.Board where + +import Prelude + +import qualified Control.Exception as Exc +import Control.Monad (forM) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Data.Aeson +import Data.Bits (Bits (unsafeShiftL, (.|.))) +import GHC.Generics + +import Game.Chess + (IsSquare (toIndex), Ply (..), Position, Sq (..), doPly, fromFEN, + legalPlies, toFEN, unsafeDoPly) +import Game.Chess.Move (Move (Move)) +import Game.Chess.Orphans () + +newtype Board = Board [Sq] + +allPieces :: Board +allPieces = Board $ enumFrom minBound + +deriving instance Generic Board + +deriving instance ToJSON Board +deriving instance FromJSON Board + +-- copied from https://github.com/peterbecich/chessIO/blob/0d61d8352096e4f893c13a8ff7b275b9a76d2de9/src/Game/Chess/Internal.hs#L237-L239 +move :: (IsSquare from, IsSquare to) => from -> to -> Ply +move (toIndex -> from) (toIndex -> to) = + Ply $ fromIntegral to .|. fromIntegral from `unsafeShiftL` 6 + +-- copied +-- https://github.com/peterbecich/chessIO/blob/0d61d8352096e4f893c13a8ff7b275b9a76d2de9/src/Game/Chess/Internal.hs#L373-L376 +-- can't use `doPly`; can't capture `error`, intended to be irrecoverable +doPly' :: Position -> Ply -> Maybe Position +doPly' p m + | m `elem` legalPlies p = Just $ unsafeDoPly p m + | otherwise = Nothing + +checkMove + :: Sq -> Sq -> Position -> Maybe Position +checkMove from to start = doPly' start mv + where + mv = move from to + +checkMove' :: Move -> Maybe String +checkMove' (Move fenPosition from to) = do + let + mPosition :: Maybe Position = fromFEN fenPosition + case mPosition of + Nothing -> Nothing + Just position -> do + toFEN <$> checkMove from to position diff --git a/haskell/src/Game/Chess/Board.purs b/haskell/src/Game/Chess/Board.purs new file mode 100644 index 0000000..2f5c2a4 --- /dev/null +++ b/haskell/src/Game/Chess/Board.purs @@ -0,0 +1,40 @@ +-- File auto generated by purescript-bridge! -- +module Game.Chess.Board where + +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson) +import Data.Argonaut.Aeson.Options as Argonaut +import Data.Argonaut.Decode.Class (class DecodeJson, class DecodeJsonField, decodeJson) +import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson) +import Data.Generic.Rep (class Generic) +import Data.Lens (Iso', Lens', Prism', lens, prism') +import Data.Lens.Iso.Newtype (_Newtype) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype) +import Foreign.Class (class Decode, class Encode) +import Foreign.Generic (defaultOptions, genericDecode, genericEncode) +import Game.Chess.Internal.Square (Sq) +import Prim (Array) +import Type.Proxy (Proxy(Proxy)) + +import Prelude + +newtype Board = + Board (Array Sq) + +instance encodeBoard :: Encode Board where + encode = genericEncode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance decodeBoard :: Decode Board where + decode = genericDecode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance encodeJsonBoard :: EncodeJson Board where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +instance decodeJsonBoard :: DecodeJson Board where + decodeJson = genericDecodeAeson Argonaut.defaultOptions +derive instance genericBoard :: Generic Board _ +derive instance newtypeBoard :: Newtype Board _ + +-------------------------------------------------------------------------------- +_Board :: Iso' Board (Array Sq) +_Board = _Newtype +-------------------------------------------------------------------------------- diff --git a/haskell/src/Game/Chess/Internal.purs b/haskell/src/Game/Chess/Internal.purs new file mode 100644 index 0000000..f12a7a0 --- /dev/null +++ b/haskell/src/Game/Chess/Internal.purs @@ -0,0 +1,104 @@ +-- File auto generated by purescript-bridge! -- +module Game.Chess.Internal where + +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson) +import Data.Argonaut.Aeson.Options as Argonaut +import Data.Argonaut.Decode.Class (class DecodeJson, class DecodeJsonField, decodeJson) +import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson) +import Data.Generic.Rep (class Generic) +import Data.Lens (Iso', Lens', Prism', lens, prism') +import Data.Lens.Iso.Newtype (_Newtype) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype) +import Foreign.Class (class Decode, class Encode) +import Foreign.Generic (defaultOptions, genericDecode, genericEncode) +import Type.Proxy (Proxy(Proxy)) + +import Prelude + +data Color = + Black + | White + +instance encodeColor :: Encode Color where + encode = genericEncode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance decodeColor :: Decode Color where + decode = genericDecode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance encodeJsonColor :: EncodeJson Color where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +instance decodeJsonColor :: DecodeJson Color where + decodeJson = genericDecodeAeson Argonaut.defaultOptions +derive instance genericColor :: Generic Color _ + +-------------------------------------------------------------------------------- +_Black :: Prism' Color Unit +_Black = prism' (\_ -> Black) f + where + f Black = Just unit + f _ = Nothing + +_White :: Prism' Color Unit +_White = prism' (\_ -> White) f + where + f White = Just unit + f _ = Nothing + +-------------------------------------------------------------------------------- +data PieceType = + Pawn + | Knight + | Bishop + | Rook + | Queen + | King + +instance encodePieceType :: Encode PieceType where + encode = genericEncode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance decodePieceType :: Decode PieceType where + decode = genericDecode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance encodeJsonPieceType :: EncodeJson PieceType where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +instance decodeJsonPieceType :: DecodeJson PieceType where + decodeJson = genericDecodeAeson Argonaut.defaultOptions +derive instance genericPieceType :: Generic PieceType _ + +-------------------------------------------------------------------------------- +_Pawn :: Prism' PieceType Unit +_Pawn = prism' (\_ -> Pawn) f + where + f Pawn = Just unit + f _ = Nothing + +_Knight :: Prism' PieceType Unit +_Knight = prism' (\_ -> Knight) f + where + f Knight = Just unit + f _ = Nothing + +_Bishop :: Prism' PieceType Unit +_Bishop = prism' (\_ -> Bishop) f + where + f Bishop = Just unit + f _ = Nothing + +_Rook :: Prism' PieceType Unit +_Rook = prism' (\_ -> Rook) f + where + f Rook = Just unit + f _ = Nothing + +_Queen :: Prism' PieceType Unit +_Queen = prism' (\_ -> Queen) f + where + f Queen = Just unit + f _ = Nothing + +_King :: Prism' PieceType Unit +_King = prism' (\_ -> King) f + where + f King = Just unit + f _ = Nothing + +-------------------------------------------------------------------------------- diff --git a/haskell/src/Game/Chess/Internal/Square.purs b/haskell/src/Game/Chess/Internal/Square.purs new file mode 100644 index 0000000..fd04c50 --- /dev/null +++ b/haskell/src/Game/Chess/Internal/Square.purs @@ -0,0 +1,482 @@ +-- File auto generated by purescript-bridge! -- +module Game.Chess.Internal.Square where + +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson) +import Data.Argonaut.Aeson.Options as Argonaut +import Data.Argonaut.Decode.Class (class DecodeJson, class DecodeJsonField, decodeJson) +import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson) +import Data.Generic.Rep (class Generic) +import Data.Lens (Iso', Lens', Prism', lens, prism') +import Data.Lens.Iso.Newtype (_Newtype) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype) +import Foreign.Class (class Decode, class Encode) +import Foreign.Generic (defaultOptions, genericDecode, genericEncode) +import Type.Proxy (Proxy(Proxy)) + +import Prelude + +data Sq = + A1 + | B1 + | C1 + | D1 + | E1 + | F1 + | G1 + | H1 + | A2 + | B2 + | C2 + | D2 + | E2 + | F2 + | G2 + | H2 + | A3 + | B3 + | C3 + | D3 + | E3 + | F3 + | G3 + | H3 + | A4 + | B4 + | C4 + | D4 + | E4 + | F4 + | G4 + | H4 + | A5 + | B5 + | C5 + | D5 + | E5 + | F5 + | G5 + | H5 + | A6 + | B6 + | C6 + | D6 + | E6 + | F6 + | G6 + | H6 + | A7 + | B7 + | C7 + | D7 + | E7 + | F7 + | G7 + | H7 + | A8 + | B8 + | C8 + | D8 + | E8 + | F8 + | G8 + | H8 + +instance encodeSq :: Encode Sq where + encode = genericEncode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance decodeSq :: Decode Sq where + decode = genericDecode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance encodeJsonSq :: EncodeJson Sq where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +instance decodeJsonSq :: DecodeJson Sq where + decodeJson = genericDecodeAeson Argonaut.defaultOptions +derive instance genericSq :: Generic Sq _ + +-------------------------------------------------------------------------------- +_A1 :: Prism' Sq Unit +_A1 = prism' (\_ -> A1) f + where + f A1 = Just unit + f _ = Nothing + +_B1 :: Prism' Sq Unit +_B1 = prism' (\_ -> B1) f + where + f B1 = Just unit + f _ = Nothing + +_C1 :: Prism' Sq Unit +_C1 = prism' (\_ -> C1) f + where + f C1 = Just unit + f _ = Nothing + +_D1 :: Prism' Sq Unit +_D1 = prism' (\_ -> D1) f + where + f D1 = Just unit + f _ = Nothing + +_E1 :: Prism' Sq Unit +_E1 = prism' (\_ -> E1) f + where + f E1 = Just unit + f _ = Nothing + +_F1 :: Prism' Sq Unit +_F1 = prism' (\_ -> F1) f + where + f F1 = Just unit + f _ = Nothing + +_G1 :: Prism' Sq Unit +_G1 = prism' (\_ -> G1) f + where + f G1 = Just unit + f _ = Nothing + +_H1 :: Prism' Sq Unit +_H1 = prism' (\_ -> H1) f + where + f H1 = Just unit + f _ = Nothing + +_A2 :: Prism' Sq Unit +_A2 = prism' (\_ -> A2) f + where + f A2 = Just unit + f _ = Nothing + +_B2 :: Prism' Sq Unit +_B2 = prism' (\_ -> B2) f + where + f B2 = Just unit + f _ = Nothing + +_C2 :: Prism' Sq Unit +_C2 = prism' (\_ -> C2) f + where + f C2 = Just unit + f _ = Nothing + +_D2 :: Prism' Sq Unit +_D2 = prism' (\_ -> D2) f + where + f D2 = Just unit + f _ = Nothing + +_E2 :: Prism' Sq Unit +_E2 = prism' (\_ -> E2) f + where + f E2 = Just unit + f _ = Nothing + +_F2 :: Prism' Sq Unit +_F2 = prism' (\_ -> F2) f + where + f F2 = Just unit + f _ = Nothing + +_G2 :: Prism' Sq Unit +_G2 = prism' (\_ -> G2) f + where + f G2 = Just unit + f _ = Nothing + +_H2 :: Prism' Sq Unit +_H2 = prism' (\_ -> H2) f + where + f H2 = Just unit + f _ = Nothing + +_A3 :: Prism' Sq Unit +_A3 = prism' (\_ -> A3) f + where + f A3 = Just unit + f _ = Nothing + +_B3 :: Prism' Sq Unit +_B3 = prism' (\_ -> B3) f + where + f B3 = Just unit + f _ = Nothing + +_C3 :: Prism' Sq Unit +_C3 = prism' (\_ -> C3) f + where + f C3 = Just unit + f _ = Nothing + +_D3 :: Prism' Sq Unit +_D3 = prism' (\_ -> D3) f + where + f D3 = Just unit + f _ = Nothing + +_E3 :: Prism' Sq Unit +_E3 = prism' (\_ -> E3) f + where + f E3 = Just unit + f _ = Nothing + +_F3 :: Prism' Sq Unit +_F3 = prism' (\_ -> F3) f + where + f F3 = Just unit + f _ = Nothing + +_G3 :: Prism' Sq Unit +_G3 = prism' (\_ -> G3) f + where + f G3 = Just unit + f _ = Nothing + +_H3 :: Prism' Sq Unit +_H3 = prism' (\_ -> H3) f + where + f H3 = Just unit + f _ = Nothing + +_A4 :: Prism' Sq Unit +_A4 = prism' (\_ -> A4) f + where + f A4 = Just unit + f _ = Nothing + +_B4 :: Prism' Sq Unit +_B4 = prism' (\_ -> B4) f + where + f B4 = Just unit + f _ = Nothing + +_C4 :: Prism' Sq Unit +_C4 = prism' (\_ -> C4) f + where + f C4 = Just unit + f _ = Nothing + +_D4 :: Prism' Sq Unit +_D4 = prism' (\_ -> D4) f + where + f D4 = Just unit + f _ = Nothing + +_E4 :: Prism' Sq Unit +_E4 = prism' (\_ -> E4) f + where + f E4 = Just unit + f _ = Nothing + +_F4 :: Prism' Sq Unit +_F4 = prism' (\_ -> F4) f + where + f F4 = Just unit + f _ = Nothing + +_G4 :: Prism' Sq Unit +_G4 = prism' (\_ -> G4) f + where + f G4 = Just unit + f _ = Nothing + +_H4 :: Prism' Sq Unit +_H4 = prism' (\_ -> H4) f + where + f H4 = Just unit + f _ = Nothing + +_A5 :: Prism' Sq Unit +_A5 = prism' (\_ -> A5) f + where + f A5 = Just unit + f _ = Nothing + +_B5 :: Prism' Sq Unit +_B5 = prism' (\_ -> B5) f + where + f B5 = Just unit + f _ = Nothing + +_C5 :: Prism' Sq Unit +_C5 = prism' (\_ -> C5) f + where + f C5 = Just unit + f _ = Nothing + +_D5 :: Prism' Sq Unit +_D5 = prism' (\_ -> D5) f + where + f D5 = Just unit + f _ = Nothing + +_E5 :: Prism' Sq Unit +_E5 = prism' (\_ -> E5) f + where + f E5 = Just unit + f _ = Nothing + +_F5 :: Prism' Sq Unit +_F5 = prism' (\_ -> F5) f + where + f F5 = Just unit + f _ = Nothing + +_G5 :: Prism' Sq Unit +_G5 = prism' (\_ -> G5) f + where + f G5 = Just unit + f _ = Nothing + +_H5 :: Prism' Sq Unit +_H5 = prism' (\_ -> H5) f + where + f H5 = Just unit + f _ = Nothing + +_A6 :: Prism' Sq Unit +_A6 = prism' (\_ -> A6) f + where + f A6 = Just unit + f _ = Nothing + +_B6 :: Prism' Sq Unit +_B6 = prism' (\_ -> B6) f + where + f B6 = Just unit + f _ = Nothing + +_C6 :: Prism' Sq Unit +_C6 = prism' (\_ -> C6) f + where + f C6 = Just unit + f _ = Nothing + +_D6 :: Prism' Sq Unit +_D6 = prism' (\_ -> D6) f + where + f D6 = Just unit + f _ = Nothing + +_E6 :: Prism' Sq Unit +_E6 = prism' (\_ -> E6) f + where + f E6 = Just unit + f _ = Nothing + +_F6 :: Prism' Sq Unit +_F6 = prism' (\_ -> F6) f + where + f F6 = Just unit + f _ = Nothing + +_G6 :: Prism' Sq Unit +_G6 = prism' (\_ -> G6) f + where + f G6 = Just unit + f _ = Nothing + +_H6 :: Prism' Sq Unit +_H6 = prism' (\_ -> H6) f + where + f H6 = Just unit + f _ = Nothing + +_A7 :: Prism' Sq Unit +_A7 = prism' (\_ -> A7) f + where + f A7 = Just unit + f _ = Nothing + +_B7 :: Prism' Sq Unit +_B7 = prism' (\_ -> B7) f + where + f B7 = Just unit + f _ = Nothing + +_C7 :: Prism' Sq Unit +_C7 = prism' (\_ -> C7) f + where + f C7 = Just unit + f _ = Nothing + +_D7 :: Prism' Sq Unit +_D7 = prism' (\_ -> D7) f + where + f D7 = Just unit + f _ = Nothing + +_E7 :: Prism' Sq Unit +_E7 = prism' (\_ -> E7) f + where + f E7 = Just unit + f _ = Nothing + +_F7 :: Prism' Sq Unit +_F7 = prism' (\_ -> F7) f + where + f F7 = Just unit + f _ = Nothing + +_G7 :: Prism' Sq Unit +_G7 = prism' (\_ -> G7) f + where + f G7 = Just unit + f _ = Nothing + +_H7 :: Prism' Sq Unit +_H7 = prism' (\_ -> H7) f + where + f H7 = Just unit + f _ = Nothing + +_A8 :: Prism' Sq Unit +_A8 = prism' (\_ -> A8) f + where + f A8 = Just unit + f _ = Nothing + +_B8 :: Prism' Sq Unit +_B8 = prism' (\_ -> B8) f + where + f B8 = Just unit + f _ = Nothing + +_C8 :: Prism' Sq Unit +_C8 = prism' (\_ -> C8) f + where + f C8 = Just unit + f _ = Nothing + +_D8 :: Prism' Sq Unit +_D8 = prism' (\_ -> D8) f + where + f D8 = Just unit + f _ = Nothing + +_E8 :: Prism' Sq Unit +_E8 = prism' (\_ -> E8) f + where + f E8 = Just unit + f _ = Nothing + +_F8 :: Prism' Sq Unit +_F8 = prism' (\_ -> F8) f + where + f F8 = Just unit + f _ = Nothing + +_G8 :: Prism' Sq Unit +_G8 = prism' (\_ -> G8) f + where + f G8 = Just unit + f _ = Nothing + +_H8 :: Prism' Sq Unit +_H8 = prism' (\_ -> H8) f + where + f H8 = Just unit + f _ = Nothing + +-------------------------------------------------------------------------------- diff --git a/haskell/src/Game/Chess/Move.hs b/haskell/src/Game/Chess/Move.hs new file mode 100644 index 0000000..35e81b7 --- /dev/null +++ b/haskell/src/Game/Chess/Move.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE StandaloneDeriving #-} + +module Game.Chess.Move where + +import Prelude + +import Game.Chess (Position, Sq) +import Game.Chess.Orphans () + +import Data.Aeson +import GHC.Generics + +data Move = Move { fenPosition :: String, from :: Sq, to :: Sq } + +deriving instance Generic Move + +deriving instance ToJSON Move +deriving instance FromJSON Move diff --git a/haskell/src/Game/Chess/Move.purs b/haskell/src/Game/Chess/Move.purs new file mode 100644 index 0000000..f3f8200 --- /dev/null +++ b/haskell/src/Game/Chess/Move.purs @@ -0,0 +1,45 @@ +-- File auto generated by purescript-bridge! -- +module Game.Chess.Move where + +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson) +import Data.Argonaut.Aeson.Options as Argonaut +import Data.Argonaut.Decode.Class (class DecodeJson, class DecodeJsonField, decodeJson) +import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson) +import Data.Generic.Rep (class Generic) +import Data.Lens (Iso', Lens', Prism', lens, prism') +import Data.Lens.Iso.Newtype (_Newtype) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype) +import Foreign.Class (class Decode, class Encode) +import Foreign.Generic (defaultOptions, genericDecode, genericEncode) +import Game.Chess.Internal.Square (Sq) +import Prim (String) +import Type.Proxy (Proxy(Proxy)) + +import Prelude + +newtype Move = + Move { + fenPosition :: String + , from :: Sq + , to :: Sq + } + +instance encodeMove :: Encode Move where + encode = genericEncode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance decodeMove :: Decode Move where + decode = genericDecode $ defaultOptions { unwrapSingleConstructors = false , unwrapSingleArguments = false } +instance encodeJsonMove :: EncodeJson Move where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +instance decodeJsonMove :: DecodeJson Move where + decodeJson = genericDecodeAeson Argonaut.defaultOptions +derive instance genericMove :: Generic Move _ +derive instance newtypeMove :: Newtype Move _ + +-------------------------------------------------------------------------------- +_Move :: Iso' Move { fenPosition :: String, from :: Sq, to :: Sq} +_Move = _Newtype + +-------------------------------------------------------------------------------- diff --git a/haskell/src/Game/Chess/Orphans.hs b/haskell/src/Game/Chess/Orphans.hs new file mode 100644 index 0000000..798acde --- /dev/null +++ b/haskell/src/Game/Chess/Orphans.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} + +module Game.Chess.Orphans where + +import Data.Aeson +import GHC.Generics + +import Game.Chess + (Color (..), PieceType (..), Ply (..), Position, Sq (..), fromFEN, toFEN) + + +deriving instance Generic Sq +deriving instance Generic Color +deriving instance Generic PieceType +deriving instance Generic Ply + +deriving instance ToJSON Sq +deriving instance FromJSON Sq + +deriving instance ToJSON Color +deriving instance FromJSON Color + +deriving instance ToJSON PieceType +deriving instance FromJSON PieceType + +deriving instance ToJSON Ply +deriving instance FromJSON Ply + +instance ToJSON Position where + toJSON = toJSON . toFEN + +instance FromJSON Position where + parseJSON pos = do + x :: String <- parseJSON pos + case fromFEN x of + Nothing -> fail "failed to parse FEN" + Just x' -> pure x' diff --git a/haskell/src/Game/Components/Chessboard.purs b/haskell/src/Game/Components/Chessboard.purs new file mode 100644 index 0000000..b8b6a56 --- /dev/null +++ b/haskell/src/Game/Components/Chessboard.purs @@ -0,0 +1,183 @@ +module Game.Components.Chessboard where + +import Prelude + +import Data.Array (length) +import Data.Lens (view) +import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..)) +import Halogen as H +import Data.Argonaut.Core (Json) +import Data.Argonaut.Encode.Class (encodeJson) +import Halogen.HTML as HH +import Type.Proxy (Proxy(..)) +import Effect.Class.Console (logShow) +import Effect.Aff.Class (class MonadAff, liftAff) +import Affjax.RequestBody as RequestBody +import Affjax.Web (Error, Response, get, post) +import Affjax.StatusCode (StatusCode(StatusCode)) +import Affjax.ResponseFormat (json, string) +import Data.Either (Either(Left, Right)) +import Halogen.Store.Monad (class MonadStore) +import Data.Traversable (for_) +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Data.Argonaut.Aeson.Options (defaultOptions) +import Game.Sq (Sq'(Sq')) +import Game.Chess.Internal.Square (Sq) +import Game.Chess.Board (Board(Board), _Board) +import Game.Chess.Move (Move(Move)) +import Halogen.Store.Monad as Store +import Game.Store as GS +import Halogen.Store.Connect (Connected, connect) +import Halogen.Store.Select (selectAll) +import Game.Components.Square as Square +import CSS as CSS +import Halogen.HTML.CSS as HCSS +import CSS.Display as CSS.Display + + +data Action + = Initialize + | ReceiveSquare Square.Output + +type State = + { toggleCount :: Int + , buttonState :: Maybe Boolean + , board :: Board + , sourceSelection :: Maybe Sq + , destinationSelection :: Maybe Sq + , fenPosition :: String + } + +type ChildSlots = ( square :: Square.SquareSlot Sq' ) + +_chessboard :: Proxy "chessboard" +_chessboard = Proxy + +component :: + forall q i o m + . MonadAff m + => MonadStore GS.Action GS.Store m + => H.Component q i o m +component = connect selectAll $ H.mkComponent + { initialState: deriveState + , render + , eval: H.mkEval + $ H.defaultEval + { handleAction = handleAction + , initialize = Just Initialize + } + } + +deriveState :: forall i. Connected GS.Store i -> State +deriveState { context } = + { toggleCount: 0 + , buttonState: Nothing + , board: context.board + , sourceSelection: Nothing + , destinationSelection: Nothing + , fenPosition: context.fenPosition + } + +initialState :: forall i. i -> State +initialState _ = + { toggleCount: 0 + , buttonState: Nothing + , board: Board mempty + , sourceSelection: Nothing + , destinationSelection: Nothing + , fenPosition: mempty + } + +render + :: forall m. MonadAff m + => MonadStore GS.Action GS.Store m + => State + -> H.ComponentHTML Action ChildSlots m +render state = HH.div style (h <> [HH.text state.fenPosition]) + where + b :: Array Sq + b = view _Board state.board + h :: Array (HH.ComponentHTML Action ChildSlots m) + h = flip map b $ \sq -> + HH.slot Square._square (Sq' sq) (Square.component sq) sq ReceiveSquare + + style = pure $ HCSS.style do + -- grid properties not supported; have to set manually + -- https://github.com/purescript-contrib/purescript-css/issues/112 + CSS.key (CSS.fromString "grid-template-columns") "50px 50px 50px 50px 50px 50px 50px 50px" + CSS.key (CSS.fromString "grid-template-rows") "50px 50px 50px 50px 50px 50px 50px 50px " + CSS.Display.display CSS.Display.grid + +handleAction :: + forall o m + . MonadAff m + => MonadStore GS.Action GS.Store m + => Action + -> H.HalogenM State Action ChildSlots o m Unit +handleAction = case _ of + ReceiveSquare (Square.Clicked sq) -> do + {sourceSelection, destinationSelection, fenPosition} <- H.get + case Tuple sourceSelection destinationSelection of + Tuple (Just from) Nothing -> do + H.tell Square._square (Sq' sq) (pure Square.Select) + H.modify_ _ { destinationSelection = Just sq } + -- make move + + eMoveResponse :: Either Error (Response String) <- + liftAff + $ post string "/move" + $ Just $ RequestBody.json + $ encodeJson $ Move { fenPosition, from, to: sq } + case eMoveResponse of + Left _ -> logShow "no move response" + Right (moveResponse :: Response String) -> + if (moveResponse.status == StatusCode 200) + then do + H.modify_ _ {fenPosition = moveResponse.body} + Store.updateStore $ GS.SetFenPosition moveResponse.body + mPiece <- H.request Square._square (Sq' from) Square.GivePiece + for_ mPiece $ \piece -> do + H.tell Square._square (Sq' sq) (pure $ Square.ReceivePiece piece) + else logShow $ "illegal move" + + Tuple (Just src) (Just dst) -> do + let + srcSlot = Sq' src + dstSlot = Sq' dst + H.tell Square._square srcSlot (pure Square.Unselect) + H.tell Square._square dstSlot (pure Square.Unselect) + H.modify_ _ { sourceSelection = Just sq, destinationSelection = Nothing } + H.tell Square._square (Sq' sq) (pure Square.Select) + + Tuple Nothing (Just _) -> do + H.modify_ _ { sourceSelection = Nothing, destinationSelection = Nothing } + Tuple Nothing Nothing -> do + H.tell Square._square (Sq' sq) (pure Square.Select) + H.modify_ _ { sourceSelection = Just sq } + + Initialize -> do + logShow "Initialize" + + s <- H.get + logShow $ "Board squares: " <> show (length (view _Board s.board)) + logShow $ "FEN position: " <> s.fenPosition + when (length (view _Board s.board) == 0) do + logShow "Board has 0 items" + eBoardResponse :: Either Error (Response Json) <- + liftAff $ get json "/board" + case eBoardResponse of + Left _ -> logShow "no board response" + Right boardResponse -> + for_ (genericDecodeAeson defaultOptions boardResponse.body) + $ \(board :: Board) -> do + logShow "got board" + Store.updateStore $ GS.SetBoard board + H.modify_ _ {board = board} + eStartPositionResponse :: Either Error (Response String) <- + liftAff $ get string "/start" + case eStartPositionResponse of + Left _ -> logShow "no start position response" + Right startResponse -> do + void $ H.modify _ {fenPosition = startResponse.body} + logShow $ "got start pos: " <> startResponse.body diff --git a/haskell/src/Game/Components/HTML/Footer.purs b/haskell/src/Game/Components/HTML/Footer.purs new file mode 100644 index 0000000..c5936f8 --- /dev/null +++ b/haskell/src/Game/Components/HTML/Footer.purs @@ -0,0 +1,12 @@ +module Game.Components.HTML.Footer where + +import Halogen.HTML as HH + +footer :: forall i p. HH.HTML i p +footer = + HH.footer_ + [ HH.div_ + [ HH.span_ + [ HH.text "Footer" ] + ] + ] diff --git a/haskell/src/Game/Components/HTML/Header.purs b/haskell/src/Game/Components/HTML/Header.purs new file mode 100644 index 0000000..ec0104f --- /dev/null +++ b/haskell/src/Game/Components/HTML/Header.purs @@ -0,0 +1,12 @@ +module Game.Components.HTML.Header where + +import Halogen.HTML as HH + +header :: forall i p. HH.HTML i p +header = + HH.header_ + [ HH.div_ + [ HH.h2_ + [ HH.text "Chess" ] + ] + ] diff --git a/haskell/src/Game/Components/HTML/Piece.purs b/haskell/src/Game/Components/HTML/Piece.purs new file mode 100644 index 0000000..117abad --- /dev/null +++ b/haskell/src/Game/Components/HTML/Piece.purs @@ -0,0 +1,31 @@ +module Game.Components.HTML.Piece where + +import Prelude + +import Data.Maybe (Maybe(Just,Nothing)) +import Data.Tuple (Tuple(Tuple)) +import Game.Chess.Internal (PieceType(..), Color(Black,White)) + +import Halogen.HTML as HH +import Halogen.HTML.Properties as HP + +pieceHTML :: + forall t10 t11. Maybe (Tuple Color PieceType) + -> HH.HTML t10 t11 +pieceHTML Nothing = HH.fromPlainHTML $ HH.div_ mempty +pieceHTML (Just (Tuple color piece)) = HH.fromPlainHTML + $ HH.img [ HP.src $ root <> p piece <> c color <> suffix ] + where + root = "images/Chess_" + + p Pawn = "p" + p Knight = "n" + p Bishop = "b" + p Rook = "r" + p Queen = "q" + p King = "k" + + c Black = "d" + c White = "l" + + suffix = "t45.svg" diff --git a/haskell/src/Game/Components/Navigation.purs b/haskell/src/Game/Components/Navigation.purs new file mode 100644 index 0000000..e101389 --- /dev/null +++ b/haskell/src/Game/Components/Navigation.purs @@ -0,0 +1,112 @@ +module Game.Components.Navigation where + +import Prelude + +import Data.Maybe (Maybe(..)) +import Prim (Boolean, Int, Row, Type, Array) + +import Halogen.HTML.CSS as HCSS +import CSS as CSS +import CSS.Border as CSS.Border +import CSS.Size as CSS.Size +import Color as Colors +import CSS.Color as CSS.Color +import CSS.Display as CSS.Display +import CSS.Background as CSS.Background +import CSS.Geometry as CSS.Geometry +import Halogen as H +import Halogen.HTML as HH +import Halogen.HTML.Events as HE +import Type.Proxy (Proxy(..)) +import Effect.Aff.Class (class MonadAff) +import Game.Routes (Route(..)) + +data Action + = GoToChess + | GoToPageB + | GoToPageC + +data Output = GoTo Route + +data Query :: forall k. k -> Type +data Query a = Foo + +type NavigationSlot = H.Slot Query Output + +type State = { } + +initialState :: forall i. i -> State +initialState _ = { } + +type ChildSlots :: forall k. Row k +type ChildSlots = () + +_navigation :: Proxy "navigation" +_navigation = Proxy + +component :: + forall i m. MonadAff m + => H.Component Query i Output m +component = + H.mkComponent + { initialState: initialState + , render: render + , eval: H.mkEval + $ H.defaultEval + { handleAction = handleAction + , handleQuery = \_ -> pure Nothing + , initialize = Nothing + } + } + +render :: + forall m. State + -> H.ComponentHTML Action ChildSlots m +render _ = HH.nav [ buttons ] + [ HH.div + [ border, HE.onClick \_ -> GoToChess ] [ HH.text "Play Chess" ] + , HH.div + [ border, HE.onClick \_ -> GoToPageB ] [ HH.text "Page B" ] + , HH.div + [ border, HE.onClick \_ -> GoToPageC ] [ HH.text "Page C" ] + ] + where + blue = Colors.rgb 0 0 255 + -- https://www.w3schools.com/css/css_boxmodel.asp + -- https://pursuit.purescript.org/packages/purescript-css/5.0.1/docs/CSS + buttons = HCSS.style do + -- CSS.Display.display CSS.Display.flex + CSS.Geometry.paddingLeft $ CSS.Size.pt 10.0 + CSS.Geometry.paddingRight $ CSS.Size.pt 10.0 + CSS.Geometry.paddingTop $ CSS.Size.pt 10.0 + CSS.Geometry.paddingBottom $ CSS.Size.pt 10.0 + CSS.Geometry.marginLeft $ CSS.Size.pt 10.0 + CSS.Geometry.marginRight $ CSS.Size.pt 10.0 + CSS.Geometry.marginTop $ CSS.Size.pt 10.0 + CSS.Geometry.marginBottom $ CSS.Size.pt 10.0 + -- CSS.Geometry.marginLeft $ CSS.Size.pt 60.0 + CSS.Geometry.width $ CSS.Size.pt 350.0 + CSS.Geometry.height $ CSS.Size.pt 60.0 + CSS.Background.backgroundColor $ CSS.Color.lighten 0.4 blue + + + border = HCSS.style do + CSS.float CSS.floatLeft + CSS.Geometry.marginLeft $ CSS.Size.pt 10.0 + CSS.Geometry.width $ CSS.Size.pt 90.0 + CSS.Geometry.height $ CSS.Size.pt 30.0 + CSS.Background.backgroundColor $ CSS.Color.lighten 0.3 blue + CSS.Border.border + CSS.Border.solid + (CSS.Size.px 2.0) + CSS.Color.black + + +handleAction :: + forall m. MonadAff m + => Action + -> H.HalogenM State Action ChildSlots Output m Unit +handleAction = case _ of + GoToChess -> H.raise $ GoTo PlayChess + GoToPageB -> H.raise $ GoTo PageB + GoToPageC -> H.raise $ GoTo PageC diff --git a/haskell/src/Game/Components/Router.purs b/haskell/src/Game/Components/Router.purs new file mode 100644 index 0000000..97b2924 --- /dev/null +++ b/haskell/src/Game/Components/Router.purs @@ -0,0 +1,106 @@ +module Game.Components.Router where + +import Prelude + +import Data.Maybe (Maybe(..)) +import Halogen as H +import Effect.Console (log) +import Effect.Aff.Class (class MonadAff) +import Halogen.HTML as HH +import Type.Proxy (Proxy(..)) + +import Foreign (unsafeToForeign) + +import Routing.PushState (PushStateInterface) + +import Game.Store as GS +import Halogen.Store.Monad (class MonadStore) + +import Game.Routes (Route(..)) +import Game.Components.Utils (OpaqueSlot) +import Game.Components.Chessboard as Chessboard +import Game.Components.Navigation as Navigation +import Game.Components.HTML.Header as Header +import Game.Components.HTML.Footer as Footer + +type State = Maybe Route + +data Output = Foo + +data Query a + = Navigate Route a + +data Action + = Initialize + | HandleNavOutput Navigation.Output + +type Slots = + ( navigation :: Navigation.NavigationSlot Unit + , chessboard :: OpaqueSlot Unit + , pageB :: OpaqueSlot Unit + , pageC :: OpaqueSlot Unit + ) + +_router :: Proxy "router" +_router = Proxy + +component + :: forall input output m + . MonadAff m + => MonadStore GS.Action GS.Store m + => PushStateInterface + -> H.Component Query input output m +component nav = + H.mkComponent + { initialState + , render + , eval: H.mkEval + $ H.defaultEval + { handleAction = handleAction + , handleQuery = handleQuery + , initialize = Just Initialize + } + } + where + initialState :: input -> State + initialState _ = Just PlayChess + + render :: State -> H.ComponentHTML Action Slots m + render mRoute = HH.div_ + [ HH.div_ + [ Header.header + , HH.slot Navigation._navigation unit Navigation.component {} HandleNavOutput + , body + , Footer.footer + ] + ] + where + body = case mRoute of + Nothing -> HH.div_ mempty + Just PlayChess -> HH.slot Chessboard._chessboard unit Chessboard.component {} absurd + Just PageB -> HH.div_ [ HH.text "Page B" ] + Just PageC -> HH.div_ [ HH.text "Page C" ] + + handleQuery + :: forall a. Query a + -> H.HalogenM State Action Slots output m (Maybe a) + handleQuery = case _ of + Navigate route _ -> do + H.liftEffect $ log $ "Received query: GoTo " <> show route + H.put $ Just route + pure Nothing + + handleAction + :: Action + -> H.HalogenM State Action Slots output m Unit + handleAction = case _ of + HandleNavOutput (Navigation.GoTo route) -> do + H.liftEffect do + log $ "GoTo " <> show route + case route of + PlayChess -> nav.pushState (unsafeToForeign {}) "/chess" + PageB -> nav.pushState (unsafeToForeign {}) "/pageB" + PageC -> nav.pushState (unsafeToForeign {}) "/pageC" + + H.put $ Just route + _ -> pure unit diff --git a/haskell/src/Game/Components/Square.purs b/haskell/src/Game/Components/Square.purs new file mode 100644 index 0000000..edd84f0 --- /dev/null +++ b/haskell/src/Game/Components/Square.purs @@ -0,0 +1,196 @@ +module Game.Components.Square where + +import Prelude + +import Halogen.Store.Connect (Connected, connect) +import Game.Sq (Sq'(Sq'), State) +import Game.Chess.Internal (Color(..), PieceType) +import Game.Chess.Internal.Square (Sq) +import Data.Maybe (Maybe(..)) +import Data.Argonaut.Encode.Class (encodeJson) +import Prim (Boolean, Int, Row, Type, Array) +import Data.Tuple (Tuple(Tuple)) +import Effect.Console (log) +import Data.Argonaut.Core (Json) +import Affjax.RequestBody as RequestBody +import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson) +import Halogen.Store.Select (selectAll) +import Data.Either (Either) +import Data.HashMap (lookup) +import Data.Argonaut.Aeson.Options (defaultOptions) +import Data.Traversable (for_) +import Halogen.HTML.CSS as CSS +import Color as Colors +import CSS.Color as CSS.Color +import Affjax.Web (Error, Response, post) +import Affjax.ResponseFormat (json) +import CSS.Background as Background +import CSS.Border as CSS.Border +import Halogen.Store.Monad (class MonadStore) +import Halogen.Store.Monad as Store +import Game.Store as GS +import CSS.Size as CSS.Size +import Halogen as H +import Halogen.HTML.Properties as HP +import Halogen.HTML as HH +import Halogen.HTML.Events as HE +import Type.Proxy (Proxy(..)) +import Effect.Aff.Class (class MonadAff, liftAff) +import Game.Components.HTML.Piece as Piece + +data Action + = Initialize + | Click + +data Query :: Type -> Type +data Query a + = Unselect + | Select + | ReceivePiece (Tuple Color PieceType) + | GivePiece (Tuple Color PieceType -> a) + +data Output = Clicked Sq + +type SquareSlot = H.Slot Query Output + + +initialState :: forall i. Sq -> i -> State +initialState sq' _ = + { sq: sq' + , coordinates: Tuple 1 2 + , color: White + , piece: Nothing + , selected: false + } + +type ChildSlots :: forall k. Row k +type ChildSlots = () + +_square :: Proxy "square" +_square = Proxy + +deriveState :: forall i. Sq -> Connected GS.Store i -> State +deriveState sq { context } = case lookup (Sq' sq) context.squares of + Nothing -> initialState sq 1.0 + Just s -> s + +component + :: forall i m + . MonadAff m + => MonadStore GS.Action GS.Store m + => Sq + -> H.Component Query i Output m +component sq' = connect selectAll $ H.mkComponent + { initialState: deriveState sq' + , render: square + , eval: H.mkEval + $ H.defaultEval + { handleAction = handleAction sq' + , handleQuery = handleQuery sq' + , initialize = Just Initialize + } + } + +coordinatesToCSS :: Tuple Int Int -> Array HH.ClassName +coordinatesToCSS (Tuple x y) = + [ HH.ClassName "board" + , HH.ClassName ("row-" <> show (8 - x)) + , HH.ClassName ("column-" <> show (y + 1)) + ] + +square + :: forall m + . MonadStore GS.Action GS.Store m + => State + -> H.ComponentHTML Action ChildSlots m +square { coordinates, color, piece, selected } = + HH.div + [ border + , HP.classes (coordinatesToCSS coordinates) + , HE.onClick \_ -> Click + ] + [ Piece.pieceHTML piece + ] + where + blue = Colors.rgb 0 0 255 + + color' = case color of + Black -> + Background.backgroundColor $ CSS.Color.lighten 0.6 Colors.black + White -> + Background.backgroundColor $ Colors.white + + border = case selected of + false -> CSS.style color' + true -> do + CSS.style do + color' + CSS.Border.border + CSS.Border.dashed + (CSS.Size.px 3.0) + blue + +handleQuery + :: forall m a + . MonadStore GS.Action GS.Store m + => Sq + -> Query a + -> H.HalogenM State Action () Output m (Maybe a) +handleQuery sq = case _ of + ReceivePiece tuple -> do + H.modify_ _ { piece = Just tuple } + state <- H.get + Store.updateStore $ GS.SetSquare sq state + pure Nothing + GivePiece reply -> do + { piece } <- H.get + H.modify_ _ { piece = Nothing } + case piece of + Nothing -> pure Nothing + Just p -> do + Store.updateStore $ GS.ClearSquare sq + pure $ Just (reply p) + Unselect -> do + H.modify_ _ { selected = false } + pure Nothing + Select -> do + H.modify_ _ { selected = true } + pure Nothing + +handleAction + :: forall m. MonadAff m + => MonadStore GS.Action GS.Store m + => Sq + -> Action + -> H.HalogenM State Action ChildSlots Output m Unit +handleAction sq = case _ of + Click -> do + -- { piece } <- H.get + -- for_ piece $ const do + { selected } <- H.get + H.liftEffect $ log $ "click " <> show (not selected) + H.raise $ Clicked sq + Initialize -> do + coordinateResponse :: Either Error (Response Json) <- + liftAff $ post json "/rf" $ Just $ RequestBody.json $ encodeJson sq + for_ coordinateResponse $ \res -> do + for_ (genericDecodeAeson defaultOptions res.body) + $ \(coordinates :: Tuple Int Int) -> do + H.modify_ _ { coordinates = coordinates } + + colorResponse :: Either Error (Response Json) <- + liftAff $ post json "/color" $ Just $ RequestBody.json $ encodeJson sq + for_ colorResponse $ \res -> do + for_ (genericDecodeAeson defaultOptions res.body) + $ \(color :: Color) -> do + H.modify_ _ { color = color } + + startingPosition :: Either Error (Response Json) <- + liftAff $ post json "/pieceAtStartingPosition" $ Just $ RequestBody.json $ encodeJson sq + for_ startingPosition $ \res -> do + for_ (genericDecodeAeson defaultOptions res.body) + $ \(tuple :: Tuple Color PieceType) -> do + H.modify_ _ { piece = Just tuple } + + state <- H.get + Store.updateStore $ GS.SetSquare sq state diff --git a/haskell/src/Game/Components/Utils.purs b/haskell/src/Game/Components/Utils.purs new file mode 100644 index 0000000..512942b --- /dev/null +++ b/haskell/src/Game/Components/Utils.purs @@ -0,0 +1,7 @@ +module Game.Components.Utils where + +import Prelude + +import Halogen as H + +type OpaqueSlot slot = forall query. H.Slot query Void slot diff --git a/haskell/src/Game/Routes.purs b/haskell/src/Game/Routes.purs new file mode 100644 index 0000000..c1474e3 --- /dev/null +++ b/haskell/src/Game/Routes.purs @@ -0,0 +1,29 @@ +module Game.Routes where + +import Prelude + +import Data.Show.Generic (genericShow) +import Data.Generic.Rep (class Generic) + +import Routing.Duplex (RouteDuplex', root) +import Routing.Duplex.Generic (noArgs, sum) +import Routing.Duplex.Generic.Syntax ((/)) + +data Route + = PlayChess + | PageB + | PageC + +derive instance genericRoute :: Generic Route _ +derive instance eqRoute :: Eq Route +derive instance ordRoute :: Ord Route + +instance showRoute :: Show Route where + show = genericShow + +routeCodec :: RouteDuplex' Route +routeCodec = root $ sum + { "PlayChess" : "chess" / noArgs + , "PageB" : "pageB" / noArgs + , "PageC" : "pageC" / noArgs + } diff --git a/haskell/src/Game/Sq.purs b/haskell/src/Game/Sq.purs new file mode 100644 index 0000000..8f552cf --- /dev/null +++ b/haskell/src/Game/Sq.purs @@ -0,0 +1,36 @@ +module Game.Sq where + +import Prelude + +import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson) +import Data.Generic.Rep (class Generic) +import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson) +import Game.Chess.Internal.Square (Sq) +import Data.Argonaut.Aeson.Options as Argonaut +import Data.Argonaut.Core (Json, stringify) +import Data.Hashable (class Hashable, hash) +import Data.Tuple (Tuple(Tuple)) +import Game.Chess.Internal (Color(..), PieceType) +import Data.Maybe (Maybe(..)) + + +type State = + { sq :: Sq + , coordinates :: Tuple Int Int + , color :: Color + , piece :: Maybe (Tuple Color PieceType) + , selected :: Boolean + } + +data Sq' = Sq' Sq +instance encodeJsonSq' :: EncodeJson Sq' where + encodeJson = genericEncodeAeson Argonaut.defaultOptions +derive instance gensq :: Generic Sq' _ +instance showsq :: Show Sq' where + show x = stringify $ encodeJson x +instance eqsq :: Eq Sq' where + eq x y = eq (show x) (show y) +instance ordsq :: Ord Sq' where + compare x y = compare (show x) (show y) +instance hashsq :: Hashable Sq' where + hash sq = hash $ show sq diff --git a/haskell/src/Game/Store.purs b/haskell/src/Game/Store.purs new file mode 100644 index 0000000..63dff08 --- /dev/null +++ b/haskell/src/Game/Store.purs @@ -0,0 +1,36 @@ +module Game.Store where + +import Prelude + +import Game.Chess.Board (Board(Board)) +import Game.Sq(Sq'(Sq'), State) +import Game.Chess.Internal.Square (Sq) + +import Data.HashMap (HashMap, empty, insert, delete) + +type Store = + { board :: Board + , fenPosition :: String + , squares :: HashMap Sq' State + } + +initialStore :: Store +initialStore = + { board: Board mempty + , fenPosition: mempty + , squares: empty + } + +data Action + = SetBoard Board + | SetFenPosition String + | SetSquare Sq State + | ClearSquare Sq + +reduce :: Store -> Action -> Store +reduce store = case _ of + SetBoard b -> store { board = b } + SetFenPosition s -> store { fenPosition = s } + SetSquare sq squareState -> + store { squares = insert (Sq' sq) squareState store.squares } + ClearSquare sq -> store { squares = delete (Sq' sq) store.squares } diff --git a/haskell/src/MyLib.hs b/haskell/src/MyLib.hs new file mode 100644 index 0000000..ae7c957 --- /dev/null +++ b/haskell/src/MyLib.hs @@ -0,0 +1,110 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeOperators #-} + +module MyLib (someFunc) where + +-- https://docs.servant.dev/en/stable/cookbook/structuring-apis/StructuringApis.html +import Prelude + +import Control.Lens (view) +import Control.Monad (forM, mapM) +import Control.Monad.IO.Class (MonadIO, liftIO) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy.UTF8 as UTF8 +import Data.Data (Proxy (..)) +import Data.Maybe (fromMaybe) +import qualified Data.Text as Text +import Data.Text.Encoding (encodeUtf8) +import Network.HTTP.Media ((//), (/:)) +import Network.Wai.Handler.Warp (run) +import qualified Network.Wai.Middleware.Gzip as Gzip +import Servant + (Accept (contentType), Get, Handler, JSON, MimeRender (..), PlainText, Post, + Raw, ReqBody, Server, ServerError (errBody), err403, serve, + serveDirectoryWebApp, throwError, type (:<|>) (..), type (:>)) +import Servant.API (Accept (..)) +import System.Environment (lookupEnv) + +import Game.Chess + (Color (..), PieceType (..), Position, Sq (..), fromFEN, isLight, pieceAt, + startpos, toFEN, toRF) +import Game.Chess.Board (Board, allPieces, checkMove') +import Game.Chess.Move (Move) + +instance Accept HTML where + contentType _ = let + p :: String -> BS.ByteString + p = encodeUtf8 . Text.pack + in p "text" // p "html" /: (p "charset", p "utf-8") + +type MoveServer + = "move" :> ReqBody '[JSON] Move :> Post '[PlainText] String + +type ChessServer + = "board" :> (Get '[JSON] Board) + :<|> "rf" :> ReqBody '[JSON] Sq :> Post '[JSON] (Int, Int) + :<|> "color" :> ReqBody '[JSON] Sq :> Post '[JSON] Color + :<|> "pieceAtStartingPosition" + :> ReqBody '[JSON] Sq + :> Post '[JSON] (Maybe (Color, PieceType)) + :<|> "start" :> Get '[PlainText] String + :<|> MoveServer + :<|> Raw + +instance MimeRender HTML RawHtml where + mimeRender _ = unRaw + +data HTML = HTML +newtype RawHtml = RawHtml { unRaw :: BSL.ByteString } + +moveServer :: Move -> Handler String +moveServer move = do + liftIO $ putStrLn "Move" + let mv = checkMove' move + case mv of + Nothing -> do + liftIO $ putStrLn "Illegal chess move" + throwError + $ err403 { errBody = UTF8.fromString "illegal chess move" } + Just position -> return position + +chessServer :: FilePath -> Server ChessServer +chessServer clientDir = return allPieces + :<|> return . toRF + :<|> (\sq -> return (if isLight sq then White else Black)) + :<|> (return . pieceAt startpos) + :<|> return (toFEN startpos) + :<|> moveServer + :<|> serveDirectoryWebApp clientDir + +type RootServer = Get '[HTML] RawHtml + :<|> "chess" :> Get '[HTML] RawHtml + :<|> "pageB" :> Get '[HTML] RawHtml + :<|> "pageC" :> Get '[HTML] RawHtml + +rootServer :: BSL.ByteString -> Server RootServer +rootServer root = return (RawHtml root) + :<|> return (RawHtml root) + :<|> return (RawHtml root) + :<|> return (RawHtml root) + +api :: Proxy (RootServer :<|> ChessServer) +api = Proxy + +someFunc :: IO () +someFunc = do + mClientDir <- lookupEnv "CLIENT_DIR" + let + rootDir :: FilePath + rootDir = fromMaybe "static" mClientDir + rootFile :: FilePath + rootFile = rootDir <> "/index.html" + + putStrLn $ "Root file: " <> rootFile + root <- liftIO $ BSL.readFile rootFile + putStrLn $ "Start pos: " <> toFEN startpos + run 8080 + . Gzip.gzip Gzip.def + $ serve api (rootServer root :<|> chessServer rootDir)