{-# LANGUAGE ScopedTypeVariables #-}
import Test.Tasty
import Test.Tasty.HUnit
import Control.Concurrent
import Control.Monad (forever, when)
import Data.Bits
import Data.Map (Map)
import Data.Maybe (isNothing)
import qualified Data.Map as M
import Data.Int (Int64)
import System.Random (randomIO)
import System.Timeout (timeout)
import Alpaca.NetCode
import Alpaca.NetCode.Advanced
import Data.Maybe (fromMaybe)
import Data.List (foldl')
main :: IO ()
main = defaultMain $ testGroup "alpaca-netcode" $ let
tickRate = 1000
tickRate32 = fromIntegral 1000
initialInput :: Int64
initialInput = 123456789
inputLatency :: Float
inputLatency = 0.1
-- Step of the world does a simple hashes all the inputs.
stepWorld :: Map PlayerId Int64 -> Tick -> (Int64, Int64) -> (Int64, Int64)
stepWorld playerInputs (Tick t) (_numPlayersOld, hash) =
( fromIntegral $ M.size playerInputs
, foldl'
(\hash' x -> (shiftL hash' 1) `xor` x)
(shiftL hash 1 `xor` t)
(concat [[fromIntegral i, j] | (PlayerId i, j) <- M.toList playerInputs])
)
-- (number of players on this tick, hash over past states/inputs)
initialWorld :: (Int64, Int64)
initialWorld = (0, 12345654321)
simulateClient :: (Int64 -> IO ()) -> IO ThreadId
simulateClient setInput = forkIO $ forever $ do
threadDelay (1000000 `div` tickRate)
setInput =<< randomIO
test ::
( Maybe SimNetConditions
-> ServerConfig
-> Int64
-> IO ()
)
-> (Maybe SimNetConditions
-> ClientConfig
-> Int64
-> (Int64, Int64)
-> (Map PlayerId Int64 -> Tick -> (Int64, Int64) -> (Int64, Int64)) -> IO (Client (Int64, Int64) Int64)
)
-> (Maybe SimNetConditions
-> ClientConfig
-> Int64
-> (Int64, Int64)
-> (Map PlayerId Int64 -> Tick -> (Int64, Int64) -> (Int64, Int64)) -> IO (Client (Int64, Int64) Int64)
)
-> IO ()
test runServerWith' runClient0With' runClient1With' = do
x <- timeout (15 * 1000000) $ do
-- Run a server
tidServer <- forkIO $ runServerWith'
Nothing
(defaultServerConfig tickRate32)
initialInput
-- A client with Perfect network conditions
client0 <- runClient0With'
Nothing
(defaultClientConfig tickRate32)
initialInput
initialWorld
stepWorld
tid0 <- simulateClient (clientSetInput client0)
-- A client with very poor network conditions
client1 <- runClient1With'
(Just (SimNetConditions 0.2 0.1 0.5))
(defaultClientConfig tickRate32)
initialInput
initialWorld
stepWorld
tid1 <- simulateClient (clientSetInput client1)
-- Let the game play for a bit
threadDelay (4 * 1000000)
-- Collect auth worlds from both clients
let n = 2000
auths0 <- take n . fst <$> clientSample' client0
auths1 <- take n . fst <$> clientSample' client1
length auths0 >= n @? "Expected at least " ++ show n ++ " auth worlds but client 0 got " ++ show (length auths0)
length auths1 >= n @? "Expected at least " ++ show n ++ " auth worlds but client 1 got " ++ show (length auths1)
(auths0 == auths1) @? "Auth worlds do not match between clients"
let k = 100
length (filter ((>0) . fst) auths0) > k @? "Expected at least " ++ show k ++ " tick with more that 0 players"
killThread tidServer
clientStop client0
killThread tid0
clientStop client1
killThread tid1
return ()
when (isNothing x) (assertFailure "Timeout!")
in
[ testCase "Core" $ do
-- Use `Chan` to communicate
toServer <- newChan
toClient0 <- newChan
toClient1 <- newChan
test
(runServerWith'
(\msg (client :: Int64) -> case client of
0 -> writeChan toClient0 msg
1 -> writeChan toClient1 msg
_ -> error $ "Test error! unknown client: " ++ show client
)
(readChan toServer)
)
( runClientWith'
(\msg -> writeChan toServer (msg, 0))
(readChan toClient0)
)
(runClientWith'
(\msg -> writeChan toServer (msg, 1))
(readChan toClient1)
)
, testCase "UDP [NOCI]" $ do
let port = "8888"
test
(runServerWith port)
(runClientWith "localhost" port)
(runClientWith "localhost" port)
, testCase "clientStop" $ do
toServer <- newChan
toClient <- newChan
-- Run a server
tidServer <- forkIO $ runServerWith'
(\msg 0 -> writeChan toClient msg)
(readChan toServer)
Nothing
(defaultServerConfig tickRate32)
initialInput
-- A client with Perfect network conditions
client <- runClientWith'
(\msg -> writeChan toServer (msg, 0))
(readChan toClient)
Nothing
(defaultClientConfig tickRate32)
initialInput
initialWorld
stepWorld
tidClient <- simulateClient (clientSetInput client)
threadDelay (2 * 1000000)
clientStop client
w <- clientSample client
threadDelay (1 * 1000000)
(authWs', w') <- clientSample' client
assertEqual
"Sample after clientStop should return the last sampled world"
w w'
assertEqual
"Sample after clientStop should return no new auth worlds"
authWs' []
threadDelay (1 * 1000000)
clientStop client
(authWs'', w'') <- clientSample' client
assertEqual
"Sample after SECOND clientStop should return the last sampled world"
w w''
assertEqual
"Sample after clientStop should return no new auth worlds"
authWs'' []
killThread tidServer
killThread tidClient
]