From b2760e011801449aa766cf9624c3da7cbb2c05b1 Mon Sep 17 00:00:00 2001 From: Travis Date: Tue, 17 Jul 2018 20:14:58 +0400 Subject: [PATCH 1/2] removed mutable values. removed loops. Introduced railway oriented error handling. --- .paket/Paket.Restore.targets | 9 +- src/s2client-dotnet.sln | 82 +++-- src/s2client-fsharp/Async.fs | 60 --- src/s2client-fsharp/ErrorDefs.fs | 16 + src/s2client-fsharp/Instance.fs | 119 +++--- src/s2client-fsharp/ProtobufConnection.fs | 408 ++++++++------------- src/s2client-fsharp/Railway.fs | 101 +++++ src/s2client-fsharp/Sc2Game.fs | 242 +++++++----- src/s2client-fsharp/s2client-fsharp.fsproj | 3 +- 9 files changed, 530 insertions(+), 510 deletions(-) delete mode 100644 src/s2client-fsharp/Async.fs create mode 100644 src/s2client-fsharp/ErrorDefs.fs create mode 100644 src/s2client-fsharp/Railway.fs diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets index 55292f3..e12083c 100644 --- a/.paket/Paket.Restore.targets +++ b/.paket/Paket.Restore.targets @@ -71,7 +71,10 @@ false true - + + + true + @@ -132,11 +135,11 @@ - + - + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) diff --git a/src/s2client-dotnet.sln b/src/s2client-dotnet.sln index 612472c..19d3216 100644 --- a/src/s2client-dotnet.sln +++ b/src/s2client-dotnet.sln @@ -1,15 +1,14 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "s2client-fsharp", "s2client-fsharp\s2client-fsharp.fsproj", "{1C9FB686-318F-49BA-BB2F-297863EB8745}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "s2client-fsharp", "s2client-fsharp\s2client-fsharp.fsproj", "{1C9FB686-318F-49BA-BB2F-297863EB8745}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "s2client-proto", "s2client-proto\s2client-proto.csproj", "{A353213F-13F2-40AE-BEAF-832AD0255223}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "s2client-proto", "s2client-proto\s2client-proto.csproj", "{A353213F-13F2-40AE-BEAF-832AD0255223}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "s2client-test", "s2client-test\s2client-test.fsproj", "{60932B3D-F9A5-40FD-AD17-F3BF2540D67D}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "s2client-test", "s2client-test\s2client-test.fsproj", "{60932B3D-F9A5-40FD-AD17-F3BF2540D67D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "s2client-testCSharp", "s2client-testCSharp\s2client-testCSharp.csproj", "{22359A31-BC1E-4F75-860B-3A1773969F19}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "s2client-testCSharp", "s2client-testCSharp\s2client-testCSharp.csproj", "{22359A31-BC1E-4F75-860B-3A1773969F19}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,57 +19,60 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x64.ActiveCfg = Debug|x64 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x64.Build.0 = Debug|x64 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x86.ActiveCfg = Debug|x86 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x86.Build.0 = Debug|x86 + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x64.Build.0 = Debug|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Debug|x86.Build.0 = Debug|Any CPU {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|Any CPU.Build.0 = Release|Any CPU - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x64.ActiveCfg = Release|x64 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x64.Build.0 = Release|x64 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x86.ActiveCfg = Release|x86 - {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x86.Build.0 = Release|x86 + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x64.ActiveCfg = Release|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x64.Build.0 = Release|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x86.ActiveCfg = Release|Any CPU + {1C9FB686-318F-49BA-BB2F-297863EB8745}.Release|x86.Build.0 = Release|Any CPU {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x64.ActiveCfg = Debug|x64 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x64.Build.0 = Debug|x64 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x86.ActiveCfg = Debug|x86 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x86.Build.0 = Debug|x86 + {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x64.ActiveCfg = Debug|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x64.Build.0 = Debug|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x86.ActiveCfg = Debug|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Debug|x86.Build.0 = Debug|Any CPU {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|Any CPU.ActiveCfg = Release|Any CPU {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|Any CPU.Build.0 = Release|Any CPU - {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x64.ActiveCfg = Release|x64 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x64.Build.0 = Release|x64 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x86.ActiveCfg = Release|x86 - {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x86.Build.0 = Release|x86 + {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x64.ActiveCfg = Release|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x64.Build.0 = Release|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x86.ActiveCfg = Release|Any CPU + {A353213F-13F2-40AE-BEAF-832AD0255223}.Release|x86.Build.0 = Release|Any CPU {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x64.ActiveCfg = Debug|x64 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x64.Build.0 = Debug|x64 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x86.ActiveCfg = Debug|x86 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x86.Build.0 = Debug|x86 + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x64.ActiveCfg = Debug|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x64.Build.0 = Debug|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x86.ActiveCfg = Debug|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Debug|x86.Build.0 = Debug|Any CPU {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|Any CPU.ActiveCfg = Release|Any CPU {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|Any CPU.Build.0 = Release|Any CPU - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x64.ActiveCfg = Release|x64 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x64.Build.0 = Release|x64 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x86.ActiveCfg = Release|x86 - {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x86.Build.0 = Release|x86 + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x64.ActiveCfg = Release|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x64.Build.0 = Release|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x86.ActiveCfg = Release|Any CPU + {60932B3D-F9A5-40FD-AD17-F3BF2540D67D}.Release|x86.Build.0 = Release|Any CPU {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x64.ActiveCfg = Debug|x64 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x64.Build.0 = Debug|x64 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x86.ActiveCfg = Debug|x86 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x86.Build.0 = Debug|x86 + {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x64.ActiveCfg = Debug|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x64.Build.0 = Debug|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x86.ActiveCfg = Debug|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Debug|x86.Build.0 = Debug|Any CPU {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|Any CPU.ActiveCfg = Release|Any CPU {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|Any CPU.Build.0 = Release|Any CPU - {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x64.ActiveCfg = Release|x64 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x64.Build.0 = Release|x64 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x86.ActiveCfg = Release|x86 - {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x86.Build.0 = Release|x86 + {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x64.ActiveCfg = Release|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x64.Build.0 = Release|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x86.ActiveCfg = Release|Any CPU + {22359A31-BC1E-4F75-860B-3A1773969F19}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D4C1D667-89AE-4C6A-BAAA-7DCF34A72516} EndGlobalSection EndGlobal diff --git a/src/s2client-fsharp/Async.fs b/src/s2client-fsharp/Async.fs deleted file mode 100644 index c3e924b..0000000 --- a/src/s2client-fsharp/Async.fs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Starcraft2 - - -/// Extensions for async workflows. -[] -module AsyncExtensions = - open System - open System.Threading.Tasks - open System.Threading - open System.Runtime.ExceptionServices - - // This uses a trick to get the underlying OperationCanceledException - let inline getCancelledException (completedTask:Task) (waitWithAwaiter) = - let fallback = new TaskCanceledException(completedTask) :> OperationCanceledException - // sadly there is no other public api to retrieve it, but to call .GetAwaiter().GetResult(). - try waitWithAwaiter() - // should not happen, but just in case... - fallback - with - | :? OperationCanceledException as o -> o - | other -> - // shouldn't happen, but just in case... - new TaskCanceledException(fallback.Message, other) :> OperationCanceledException - type Microsoft.FSharp.Control.Async with - static member AwaitTaskWithoutAggregate (task:Task<'T>) : Async<'T> = - Async.FromContinuations(fun (cont, econt, ccont) -> - let continuation (completedTask : Task<_>) = - if completedTask.IsCanceled then - let cancelledException = - getCancelledException completedTask (fun () -> completedTask.GetAwaiter().GetResult() |> ignore) - econt (cancelledException) - elif completedTask.IsFaulted then - if completedTask.Exception.InnerExceptions.Count = 1 then - econt completedTask.Exception.InnerExceptions.[0] - else - econt completedTask.Exception - else - cont completedTask.Result - task.ContinueWith(Action>(continuation)) |> ignore) - static member AwaitTaskWithoutAggregate (task:Task) : Async = - Async.FromContinuations(fun (cont, econt, ccont) -> - let continuation (completedTask : Task) = - if completedTask.IsCanceled then - let cancelledException = - getCancelledException completedTask (fun () -> completedTask.GetAwaiter().GetResult() |> ignore) - econt (cancelledException) - elif completedTask.IsFaulted then - if completedTask.Exception.InnerExceptions.Count = 1 then - econt completedTask.Exception.InnerExceptions.[0] - else - econt completedTask.Exception - else - cont () - task.ContinueWith(Action(continuation)) |> ignore) - -module Runner = - let run a = - a |> Async.RunSynchronously - let runTask a = - a |> Async.StartAsTask \ No newline at end of file diff --git a/src/s2client-fsharp/ErrorDefs.fs b/src/s2client-fsharp/ErrorDefs.fs new file mode 100644 index 0000000..0cd439b --- /dev/null +++ b/src/s2client-fsharp/ErrorDefs.fs @@ -0,0 +1,16 @@ +namespace Starcraft2 + +type ApplicationError = + |FailedToEstablishConnection of string + |SendMessageBufferTooSmall + |ExpectedBinaryResponse + |FailedToSendMessage of string + |FailedToReceiveMessage of string + |NullResultWithNoError + |NullResultWithError of string seq + |ExecutableNotFound of string + |ConfigError of string + |GameNotStarted + |GameNotJoined + |NotInGame + |BotError \ No newline at end of file diff --git a/src/s2client-fsharp/Instance.fs b/src/s2client-fsharp/Instance.fs index da85b6f..d832fc8 100644 --- a/src/s2client-fsharp/Instance.fs +++ b/src/s2client-fsharp/Instance.fs @@ -1,16 +1,19 @@ namespace Starcraft2 open SC2APIProtocol +open Rail // manage a starcraft instance module Instance = + let private checkStatus expectedStatus errorType (x, status) = + if status = expectedStatus then + Ok x + else + Error errorType type Sc2Instance = - { Connection : ProtbufConnection.Sc2Connection; Process : System.Diagnostics.Process } - member x.Disconnect (exitInstance:bool) = - x.Connection.Disconnect(exitInstance) - if (not(x.Process.WaitForExit(1000))) then - x.Process.Kill() + {Connection:ProtobufConnection.Sc2Connection; Process:System.Diagnostics.Process} + let internal getFreePort () = let l = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0) l.Start() @@ -45,41 +48,45 @@ module Instance = | None -> getFreePort() let address = "127.0.0.1" let timeout = defaultArg settings.Timeout (System.TimeSpan.FromMinutes 1.0) - let executable = + let execResult = match settings.Executable with - | Some exec -> exec + | Some exec -> exec |> Ok | None -> match userSettings.Value.Executable with - | Some exec -> exec - | None -> failwithf "No executable specified." - if not (System.IO.File.Exists executable) then - failwithf "Executable '%s' doesn't exist, please try to specify the executable by hand via StartSettings or UserSettings." executable - let sc2Dir = executable |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName - let supportDir = System.IO.Path.Combine(sc2Dir, "Support64") - let proc = System.Diagnostics.ProcessStartInfo(executable) - // -dataVersion - // -windowwidth - // -windowheight - // -windowx - // -windowy - proc.Arguments <- sprintf "-listen %s -port %d -displayMode 0" address port - proc.WorkingDirectory <- supportDir - printfn "Starting SC2 ... (%s %s)" executable proc.Arguments - let processInstance = System.Diagnostics.Process.Start(proc) - - let watch = System.Diagnostics.Stopwatch.StartNew() - let mutable connection = None - let mutable lastError = null - while connection.IsNone && watch.Elapsed < timeout do - try - let! con = ProtbufConnection.connect address port timeout tok.Token - connection <- Some con - with :? System.Net.WebSockets.WebSocketException as err -> - lastError <- err - match connection with - | None -> return raise <| System.TimeoutException("Could not connect within the specified time", lastError) - | Some connection -> - return { Connection = connection; Process = processInstance } } + | Some exec -> exec |> Ok + | None -> "No executable specified." |> ConfigError |> Error + + let checkExecExists s = + if not (System.IO.File.Exists s) then + s |> ExecutableNotFound |> Error + else + s |> Ok + + + + let getInstance executable = async { + let sc2Dir = executable |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName + let supportDir = System.IO.Path.Combine(sc2Dir, "Support64") + let proc = System.Diagnostics.ProcessStartInfo(executable) + // -dataVersion + // -windowwidth + // -windowheight + // -windowx + // -windowy + proc.Arguments <- sprintf "-listen %s -port %d -displayMode 0" address port + proc.WorkingDirectory <- supportDir + printfn "Starting SC2 ... (%s %s)" executable proc.Arguments + let processInstance = System.Diagnostics.Process.Start(proc) + + let! connection = ProtobufConnection.connect address port timeout tok.Token + return {Connection = connection; Process = processInstance} |> Ok + } + + return! + execResult + |> Result.bind checkExecExists + |> Result.bindAsyncBinder getInstance + } type Participant = | Participant of Race @@ -93,7 +100,8 @@ module Instance = let createGame (instance:Sc2Instance) mapName (participants:Participant list) realTime = async { let req = new RequestCreateGame() - for player in participants do + participants + |> List.iter (fun player -> let playerSetup = new PlayerSetup() playerSetup.Type <- player.PlayerType match player with @@ -104,6 +112,7 @@ module Instance = playerSetup.Difficulty <- difficulty | Observer -> () req.PlayerSetup.Add(playerSetup) + ) req.Realtime <- realTime // map @@ -125,9 +134,7 @@ module Instance = req.LocalMap <- localmap // create the game - let! status = ProtbufConnection.createGame req instance.Connection - assert (status = Status.InitGame) - return () + return! ProtobufConnection.createGame req instance.Connection |> Result.bindAsyncInput (checkStatus Status.InitGame GameNotStarted) } type ClientPort = @@ -152,11 +159,14 @@ module Instance = let server_ports = new PortSet() server_ports.GamePort <- ports.ServerPorts.GamePort server_ports.BasePort <- ports.ServerPorts.BasePort - for clientPorts in ports.ClientPorts do + ports.ClientPorts + |> + List.iter (fun clientPorts -> let cl = new PortSet() cl.BasePort <- clientPorts.BasePort cl.GamePort <- clientPorts.GamePort req.ClientPorts.Add(cl) + ) ) // interface @@ -170,31 +180,24 @@ module Instance = req.Options <- interfaceOpts // Do the join command - let! playerId, status = ProtbufConnection.joinGame req instance.Connection - assert (status = Status.InGame) - - return playerId + return! ProtobufConnection.joinGame req instance.Connection |> Result.bindAsyncInput (checkStatus Status.InGame GameNotJoined) } let getGameInfo (instance:Sc2Instance) = async { // Do the join command - let! gameInfo, status = ProtbufConnection.getGameInfo instance.Connection - assert (status = Status.InGame) - return gameInfo } + return! ProtobufConnection.getGameInfo instance.Connection |> Result.bindAsyncInput (checkStatus Status.InGame GameNotJoined) + } let getObservation disableFog (instance:Sc2Instance) = async { // Do the join command - let! responseObs, status = ProtbufConnection.getObservation disableFog instance.Connection - assert (status = Status.InGame) - return responseObs } + return! ProtobufConnection.getObservation disableFog instance.Connection |> Result.bindAsyncInput (checkStatus Status.InGame NotInGame) + } let doStep stepSize (instance:Sc2Instance) = async { // Do the join command - let! status = ProtbufConnection.doStep stepSize instance.Connection - assert (status = Status.InGame) - return () } + return! ProtobufConnection.doStep stepSize instance.Connection |> Result.bindAsyncInput (checkStatus Status.InGame NotInGame) + } let doActions actions (instance:Sc2Instance) = async { // Send Actions - let! result, status = ProtbufConnection.doActions actions instance.Connection - assert (status = Status.InGame) - return result } \ No newline at end of file + return!ProtobufConnection.doActions actions instance.Connection |> Result.bindAsyncInput (checkStatus Status.InGame NotInGame) + } \ No newline at end of file diff --git a/src/s2client-fsharp/ProtobufConnection.fs b/src/s2client-fsharp/ProtobufConnection.fs index de9e8be..41be80c 100644 --- a/src/s2client-fsharp/ProtobufConnection.fs +++ b/src/s2client-fsharp/ProtobufConnection.fs @@ -1,256 +1,168 @@ namespace Starcraft2 +open System +//open SC2APIProtocol +open Rail -open SC2APIProtocol -open System.Net.WebSockets - -type Agent<'T> = MailboxProcessor<'T> - - -/// Exception for invalid expression types -[] -type ClientDisconnectedException = - inherit System.Exception - new (msg:string, inner:exn) = { - inherit System.Exception(msg, inner) } - new (info:System.Runtime.Serialization.SerializationInfo, context:System.Runtime.Serialization.StreamingContext) = { - inherit System.Exception(info, context) } - - -/// Exception for invalid expression types -[] -type ResponseErrorException = - static member FormatError (msgs:string seq) = - System.String.Join(", ", msgs) - val private errors : string list - - inherit System.Exception - new (msg:string, inner:exn) = { - inherit System.Exception(msg, inner) - errors = [msg] } - new (errors:string seq) = { - inherit System.Exception(ResponseErrorException.FormatError(errors), null) - errors = errors |> Seq.toList } - new (info:System.Runtime.Serialization.SerializationInfo, context:System.Runtime.Serialization.StreamingContext) = { - inherit System.Exception(info, context) - errors = [] - } - member x.Errors with get () = x.errors - -/// Exception for invalid expression types -[] -type TypedResponseErrorException<'T when 'T : enum> = - static member FormatError (error:'T, detail:string) = - let name = System.Enum.GetName(typeof<'T>, error) - sprintf "%s - %s (%d): %s" (typeof<'T>.Name) name (error :> obj :?> int) detail - val private error : 'T - val private detail : string - - inherit ResponseErrorException - new (msg:string, inner:exn) = { - inherit ResponseErrorException(msg, inner) - error = Unchecked.defaultof<'T> - detail = "" } - new (error:'T, detail : string) = { - inherit ResponseErrorException(TypedResponseErrorException.FormatError(error, detail), null) - error = error - detail = detail } - new (info:System.Runtime.Serialization.SerializationInfo, context:System.Runtime.Serialization.StreamingContext) = { - inherit ResponseErrorException(info, context) - error = Unchecked.defaultof<'T> - detail = "" } - member x.Error with get () = x.error - member x.Detail with get () = x.detail - -type PlayerId = uint32 - -// Handle connection via protobuf/websockets -module ProtbufConnection = - type private ClientResponse<'T> = - | Success of 'T - | Error of System.Runtime.ExceptionServices.ExceptionDispatchInfo +module ProtobufConnection = + open System.Net.WebSockets type private ClientRequest = - | SendRequest of SC2APIProtocol.Request * AsyncReplyChannel> - | Disconnect of bool * AsyncReplyChannel - - type Sc2Connection = - private { Client : Agent; _Address : string; _Port : int; _Timeout : System.TimeSpan } - interface System.IDisposable with - member x.Dispose () = - x.Client.PostAndAsyncReply(fun reply -> Disconnect (false, reply)) - |> Async.RunSynchronously - |> ignore - member x.Disconnect (quitInstance: bool) = - x.Client.PostAndAsyncReply(fun reply -> Disconnect(quitInstance, reply)) - |> Async.RunSynchronously - |> ignore - - member x.Address = x._Address - member x.Port = x._Port - member x.Timeout = x._Timeout - - let private sendRequest (cl:Sc2Connection) request = async { - let! response = cl.Client.PostAndAsyncReply(fun reply -> SendRequest (request, reply)) - match response with - | Success res -> return res - | Error dispatch -> dispatch.Throw(); return failwithf "Should not happen." } - - let private checkNullAndWarnings (response:Response) field = - if isNull field then - if isNull response.Error then - failwithf "Unexpected result and no error information!" - raise <| ResponseErrorException response.Error - else - if not (isNull response.Error) then - for error in response.Error do - eprintf "Response warning: %s" error + |SendRequest of SC2APIProtocol.Request * AsyncReplyChannel> - let ping (cl : Sc2Connection) = async { - let request = new SC2APIProtocol.Request() - request.Ping <- RequestPing() - let! response = sendRequest cl request - let pingResponse = response.Ping - checkNullAndWarnings response pingResponse - return pingResponse, response.Status } - - let connect address port timeout tok = async { - let mailbox = - Agent.Start(fun mailbox -> async { - let mutable recover = ignore + type Sc2Connection(address:string, port:int, timeout:TimeSpan, tok) = + let connectedSocket = + let watch = System.Diagnostics.Stopwatch.StartNew() + let rec getConnectedSocket() = try - use cl = new ClientWebSocket() + let clientSock = new ClientWebSocket() let fullAddress = System.Uri (sprintf "ws://%s:%d/sc2api" address port) - let! connected = cl.ConnectAsync(fullAddress, tok) |> Async.AwaitTaskWithoutAggregate - let mutable stayConnected = true - let receiveBuf = System.ArraySegment(Array.zeroCreate (1024*1024)) - let sendBuf = System.ArraySegment(Array.zeroCreate (1024*1024)) - - let writeMessage (req:Request) = async { - use co = new Google.Protobuf.CodedOutputStream(sendBuf.Array) - req.WriteTo(co) - let written = int co.Position - let send = System.ArraySegment(sendBuf.Array, 0, written) - do! cl.SendAsync(send, WebSocketMessageType.Binary, true, tok) |> Async.AwaitTaskWithoutAggregate } - let readMessage () = async { - let mutable finished = false - let mutable curPos = 0 - while not finished do - let left = sendBuf.Array.Length - curPos - if left <= 0 then - failwithf "Our buffer wasn't large enough for the current message!" - let segment = System.ArraySegment(receiveBuf.Array, curPos, left) - let! result = cl.ReceiveAsync(segment, tok) |> Async.AwaitTaskWithoutAggregate - match result.MessageType with - | WebSocketMessageType.Binary -> - curPos <- curPos + result.Count - finished <- result.EndOfMessage - | _ -> - failwithf "Expected a binary response!" - - - let response = Response.Parser.ParseFrom(new System.IO.MemoryStream(receiveBuf.Array, 0, curPos)) - return response } - - - while stayConnected do - let! request = mailbox.Receive() - match request with - | SendRequest (req, reply) -> - recover <- Error >> reply.Reply - do! writeMessage req - let! resp = readMessage() - recover <- ignore - reply.Reply(Success resp) - | Disconnect (sendQuit, reply) -> - recover <- Some >> reply.Reply - stayConnected <- false - if sendQuit then - // Cleanup - let quit = new Request() - quit.Quit <- new RequestQuit() - do! writeMessage quit - recover <- ignore - reply.Reply(None) - - with e -> - // "recover" from a failed request - let catch = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e) - recover catch - - // respond to future requests - while true do - let! request = mailbox.Receive() - match request with - | SendRequest (req, reply) -> reply.Reply(ClientResponse.Error catch) - | Disconnect (_, reply) -> reply.Reply(Some catch) - - // Notify everyone that we are disconnected. - let catch = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(new ClientDisconnectedException("Client was already disconnected", null)) - while true do - let! request = mailbox.Receive() - match request with - | SendRequest (req, reply) -> reply.Reply(ClientResponse.Error catch) - | Disconnect (_, reply) -> reply.Reply(None) - }) - - let con = { Client = mailbox; _Address = address; _Port = port; _Timeout = timeout } - let! _ = ping con - return con } - - let inline checkError (error:'T when 'T : enum) (errorDetails:string) = - if error :> obj :?> int <> 0 then - raise <| TypedResponseErrorException<'T>(error, errorDetails) - - let createGame (createGame: RequestCreateGame) (cl : Sc2Connection) : Async = async { - let request = new SC2APIProtocol.Request() - request.CreateGame <- createGame - let! response = sendRequest cl request - let createGameResponse = response.CreateGame - checkNullAndWarnings response createGameResponse - checkError createGameResponse.Error createGameResponse.ErrorDetails - return response.Status } - - let joinGame (joinGame: RequestJoinGame) (cl : Sc2Connection) : Async = async { - let request = new SC2APIProtocol.Request() - request.JoinGame <- joinGame - let! response = sendRequest cl request - let joinGameResponse = response.JoinGame - checkNullAndWarnings response joinGameResponse - checkError joinGameResponse.Error joinGameResponse.ErrorDetails - return joinGameResponse.PlayerId, response.Status } - - let getGameInfo (cl : Sc2Connection) = async { - let request = new SC2APIProtocol.Request() - request.GameInfo <- new RequestGameInfo() - let! response = sendRequest cl request - let gameInfoResponse = response.GameInfo - checkNullAndWarnings response gameInfoResponse - return gameInfoResponse, response.Status } - - let getObservation disableFog (cl : Sc2Connection) = async { - let request = new SC2APIProtocol.Request() - request.Observation <- new RequestObservation() - request.Observation.DisableFog <- disableFog - let! response = sendRequest cl request - let observationResponse = response.Observation - checkNullAndWarnings response observationResponse - return observationResponse, response.Status } - - let doStep stepSize (cl : Sc2Connection) = async { - let request = new SC2APIProtocol.Request() - request.Step <- new RequestStep() - request.Step.Count <- stepSize - let! response = sendRequest cl request - let stepResponse = response.Step - checkNullAndWarnings response stepResponse - return response.Status } + clientSock.ConnectAsync(fullAddress, tok) |> Async.AwaitTask |> Async.RunSynchronously + clientSock |> Ok + with + |_ when watch.Elapsed < timeout -> + getConnectedSocket() + |ex -> ex.Message |> FailedToEstablishConnection |> Error + getConnectedSocket() + + let receiveBuf = System.ArraySegment(Array.zeroCreate (1024*1024)) + let sendBuf = System.ArraySegment(Array.zeroCreate (1024*1024)) + + let writeMessage (client:ClientWebSocket) (req:SC2APIProtocol.Request) = async { + try + use co = new Google.Protobuf.CodedOutputStream(sendBuf.Array) + req.WriteTo(co) + let written = int co.Position + let send = System.ArraySegment(sendBuf.Array, 0, written) + do! client.SendAsync(send, WebSocketMessageType.Binary, true, tok) |> Async.AwaitTask + return Ok () + with + |ex -> return ex.Message |> FailedToSendMessage |> Error + } + + let readMessage (client:ClientWebSocket) = async { + let rec innerLoop curPos = async { + let left = sendBuf.Array.Length - curPos + if left <= 0 then + return Error SendMessageBufferTooSmall + else + try + let segment = System.ArraySegment(receiveBuf.Array, curPos, left) + let! result = client.ReceiveAsync(segment, tok) |> Async.AwaitTask + match result.MessageType, result.EndOfMessage with + |WebSocketMessageType.Binary, false -> + return! innerLoop (curPos + result.Count) + |WebSocketMessageType.Binary, true -> return Ok (curPos + result.Count) + |_ -> return Error ExpectedBinaryResponse + with + |ex -> return ex.Message |> FailedToReceiveMessage |> Error + } + + let parseFrom finalPos = + SC2APIProtocol.Response.Parser.ParseFrom(new System.IO.MemoryStream(receiveBuf.Array, 0, finalPos)) + + return! + innerLoop 0 + |> Result.mapAsyncInput parseFrom + } + + let getAgent client = + let getResponse() = + readMessage client + MailboxProcessor.Start (fun inbox -> + let rec messageLoop() = async{ + let! msg = inbox.Receive() + + match msg with + |SendRequest (req, replyChannel) -> + let! resp = + writeMessage client req + |> Result.bindAsync getResponse + + replyChannel.Reply resp + + return! messageLoop() + } + messageLoop() + ) + + let postSendRequest req (agent:MailboxProcessor) = + agent.PostAndAsyncReply (fun reply -> SendRequest (req, reply)) + + let sendRequest req = async{ + return! + connectedSocket + |> Result.map getAgent + |> Result.bindAsyncBinder (postSendRequest req) + } + + member this.SendRequest = sendRequest + + let connect address port timeout tok = async{ + return Sc2Connection(address, port, timeout, tok) + } - let doActions actions (cl : Sc2Connection) = async { - let request = new SC2APIProtocol.Request() - request.Action <- new RequestAction() - for action in actions do - request.Action.Actions.Add(action:Action) - let! response = sendRequest cl request - let actionResponse = response.Action - checkNullAndWarnings response actionResponse - return actionResponse.Result, response.Status } \ No newline at end of file + let private applyFieldCheckAndReturnFunction fieldCheck returnFunc (response:SC2APIProtocol.Response) = + match fieldCheck response, response.Error with + |null, null -> + Error NullResultWithNoError + |null, _ -> + Error (NullResultWithError response.Error) + |_, sq when not (isNull sq) -> + sq |> Seq.iter (fun s -> eprintfn "Response warning: %s" s) + response |> returnFunc |> Ok + |_, _ -> + response |> returnFunc |> Ok + + //let inline checkError (error:'T when 'T : enum) (errorDetails:string) = + // if error :> obj :?> int <> 0 then + // raise <| TypedResponseErrorException<'T>(error, errorDetails) + + let private genericInteractionFunction applyRequestField getResponseField getResult (client:Sc2Connection) = async { + let request = SC2APIProtocol.Request() |> applyRequestField + let! responseResult = client.SendRequest request + return + responseResult + |> Result.bind (applyFieldCheckAndReturnFunction getResponseField getResult) + } + + let createGame createGameReq client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.CreateGame <- createGameReq; req) + (fun (resp:SC2APIProtocol.Response) -> resp.CreateGame) + (fun (resp:SC2APIProtocol.Response) -> (), resp.Status) + client + + let joinGame joinGameReq client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.JoinGame <- joinGameReq; req) + (fun (resp:SC2APIProtocol.Response) -> resp.JoinGame) + (fun (resp:SC2APIProtocol.Response) -> resp.JoinGame.PlayerId, resp.Status) + client + + let getGameInfo client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.GameInfo <- SC2APIProtocol.RequestGameInfo(); req) + (fun (resp:SC2APIProtocol.Response) -> resp.GameInfo) + (fun (resp:SC2APIProtocol.Response) -> resp.GameInfo, resp.Status) + client + + let getObservation disableFog client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.Observation <- SC2APIProtocol.RequestObservation(); req.Observation.DisableFog <- disableFog; req) + (fun (resp:SC2APIProtocol.Response) -> resp.Observation) + (fun (resp:SC2APIProtocol.Response) -> resp.Observation, resp.Status) + client + + let doStep stepSize client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.Step <- SC2APIProtocol.RequestStep(); req.Step.Count <- stepSize; req) + (fun (resp:SC2APIProtocol.Response) -> resp.Observation) + (fun (resp:SC2APIProtocol.Response) -> (), resp.Status) + client + + let doActions (actions:SC2APIProtocol.Action seq) client = + genericInteractionFunction + (fun (req:SC2APIProtocol.Request) -> req.Action <- SC2APIProtocol.RequestAction(); actions |> Seq.iter (fun action -> req.Action.Actions.Add(action)); req) + (fun (resp:SC2APIProtocol.Response) -> resp.Action) + (fun (resp:SC2APIProtocol.Response) -> resp.Action.Result, resp.Status) + client \ No newline at end of file diff --git a/src/s2client-fsharp/Railway.fs b/src/s2client-fsharp/Railway.fs new file mode 100644 index 0000000..7287f89 --- /dev/null +++ b/src/s2client-fsharp/Railway.fs @@ -0,0 +1,101 @@ +namespace Rail + +module Result = + + // apply either a success function or failure function + let either successFunc failureFunc twoTrackInput = + match twoTrackInput with + | Ok s -> successFunc s + | Error f -> failureFunc f + + // convert a switch function into a two-track function + //let bind f = + // either f fail + + // convert a one-track function into a switch + let switch f = + f >> Ok + + // convert a one-track function into a two-track function + //let map f = + // either (f >> succeed) fail + + // convert a dead-end function into a one-track function + let tee f x = + f x; x + + // convert a one-track function into a switch with exception handling + let tryCatch f exnHandler x = + try + f x |> Ok + with + | ex -> exnHandler ex |> Error + + // convert two one-track functions into a two-track function + let doubleMap successFunc failureFunc = + either (successFunc >> Ok) (failureFunc >> Error) + + // add two switches in parallel + let plus addSuccess addFailure switch1 switch2 x = + match (switch1 x),(switch2 x) with + | Ok s1, Ok s2 -> Ok (addSuccess s1 s2) + | Error f1, Ok _ -> Error f1 + | Ok _ , Error f2 -> Error f2 + | Error f1, Error f2 -> Error (addFailure f1 f2) + + let bindAsyncInput binder asyncInput = async{ + let! input = asyncInput + return Result.bind binder input + } + + let eitherAsync successFunc failureFunc asyncInput = async{ + let! input = asyncInput + return either successFunc failureFunc input + } + + + let bindAsyncBinder asyncBinder input = async{ + match input with + |Error er -> return Error er + |Ok inp -> return! asyncBinder inp + } + + let bindAsync asyncBinder asyncInput = async { + let! input = asyncInput + return! bindAsyncBinder asyncBinder input + } + + let mapAsyncInput f asyncInput = async { + let! input = asyncInput + return Result.map f input + } + + let mapAsyncMapper asyncMapper input = async { + match input with + |Error er -> return Error er + |Ok inp -> return! asyncMapper inp + } + + let mapAsync asyncMapper asyncInput = async { + let! input = asyncInput + return! mapAsyncMapper asyncMapper input + } + + let listFold resultList = + resultList + |> List.fold (fun resultState resultElem -> + match resultState, resultElem with + |Ok state, Ok elem -> elem::state |> Ok + |Error er, _ -> Error er + |_, Error er -> Error er + ) (Ok []) + +[] +module RailOps = + // pipe a two-track value into a switch function + let (>>=) x f = + Result.bind f x + + // compose two switches into another switch + let (>=>) s1 s2 = + s1 >> Result.bind s2 \ No newline at end of file diff --git a/src/s2client-fsharp/Sc2Game.fs b/src/s2client-fsharp/Sc2Game.fs index 2448aa0..d1459b8 100644 --- a/src/s2client-fsharp/Sc2Game.fs +++ b/src/s2client-fsharp/Sc2Game.fs @@ -2,26 +2,37 @@ namespace Starcraft2 open SC2APIProtocol - type GameState = - { LastObservation : SC2APIProtocol.ResponseObservation option - LastActions : SC2APIProtocol.Action list - NewObservation : SC2APIProtocol.ResponseObservation - // more global state - PlayerId : PlayerId - GameInfo : SC2APIProtocol.ResponseGameInfo - } - static member Empty playerId = - { LastObservation = None - LastActions = [] - NewObservation = null - PlayerId = playerId - GameInfo = null } + { + LastObservation : SC2APIProtocol.ResponseObservation option + LastActions : SC2APIProtocol.Action list + NewObservation : SC2APIProtocol.ResponseObservation + PlayerId : uint32 + GameInfo : SC2APIProtocol.ResponseGameInfo + } + + static member InitialState playerId observation gameInfo = + { + LastObservation = None + LastActions = [] + NewObservation = observation + PlayerId = playerId + GameInfo = gameInfo + } + + member this.NextGameState lastActions observation = + {this with + LastObservation = this.NewObservation |> Some + LastActions = lastActions + NewObservation = observation + } type Sc2Bot = GameState -> SC2APIProtocol.Action list type Sc2Observer = GameState -> unit module Sc2Game = + open Rail + open Instance type Participant = | Participant of Instance.Sc2Instance * Race * Sc2Bot @@ -64,24 +75,26 @@ module Sc2Game = let private setupAndConnect (gameSettings:GameSettings) (participants: Participant list) = async { // Create game with first client - let firstInstance = - participants - |> Seq.tryPick (function - | Participant(instance,_,_) -> Some instance - | Observer(instance,_) -> Some instance - | _ -> None) - let firstInstance = + let validateParticipants() = + let firstInstance = + participants + |> Seq.tryPick (function + | Participant(instance,_,_) -> Some instance + | Observer(instance,_) -> Some instance + | _ -> None) + match firstInstance with - | None -> failwithf "At least one non-computer participant needs to be added!" - | Some s -> s - - + | None -> "At least one non-computer participant needs to be added!" |> ConfigError |> Error + | Some s -> s |> Ok + let simpleParticipants = participants |> List.map (fun p -> p.Simple) - do! Instance.createGame firstInstance gameSettings.Map simpleParticipants gameSettings.Realtime - // Join other instances - let agents = participants |> Seq.sumBy (function Computer _ -> 0 | _ -> 1) - let ports = + let createGame firstInstance = + Instance.createGame firstInstance gameSettings.Map simpleParticipants gameSettings.Realtime + + let joinOtherInstances _ = + let agents = participants |> Seq.sumBy (function Computer _ -> 0 | _ -> 1) + if agents > 1 then let clientPortsRequired = // one is the server @@ -91,86 +104,115 @@ module Sc2Game = let clients = List.init clientPortsRequired (fun _ -> { Instance.ClientPort.BasePort = Instance.getFreePort(); Instance.ClientPort.GamePort = Instance.getFreePort() } ) - { Instance.SharedPort = shared - Instance.ServerPorts = server - Instance.ClientPorts = clients } + { + Instance.SharedPort = shared + Instance.ServerPorts = server + Instance.ClientPorts = clients + } |> Some - else None + else None - let playerIdTasks = + let getPlayerIds ports = participants |> List.map (fun part -> + let attachPart x = part, x match part with - | Participant (instance, _, _) - | Observer (instance, _) -> - Instance.joinGame instance gameSettings.UseFeatureLayers gameSettings.UseRender part.Simple ports - |> Async.StartAsTask - |> Some - | _ -> None) - for playerIdTask in playerIdTasks do - match playerIdTask with - | Some t -> do! t |> Async.AwaitTask |> Async.Ignore - | None -> () - - let playerIds = - playerIdTasks |> List.map (Option.map (fun pit -> pit.Result)) - - return playerIds + |Participant (instance, _, _) + |Observer (instance, _) -> + Instance.joinGame instance gameSettings.UseFeatureLayers gameSettings.UseRender part.Simple ports + |> Async.RunSynchronously + |> Result.map Some + |> Result.map attachPart + |_ -> None |> attachPart |> Ok + ) + |> Result.listFold + + return! + validateParticipants() + |> Result.bindAsyncBinder createGame + |> Result.bindAsyncInput (joinOtherInstances >> Ok) + |> Result.bindAsyncInput getPlayerIds } + type private PlayerData = + { + PlayerId:uint32 + Instance:Sc2Instance + Bot:Sc2Bot + } + static member Create playerId instance bot = + { + PlayerId = playerId + Instance = instance + Bot = bot + } let runGame (gameSettings:GameSettings) (participants: Participant seq) = async { - let participants = participants |> Seq.toList - let! playerIds = setupAndConnect gameSettings participants - - let merged = - List.zip participants playerIds - let state = System.Collections.Concurrent.ConcurrentDictionary<_,GameState>() - let getState playerId = - state.GetOrAdd(playerId, fun _ -> GameState.Empty playerId) - let updateState playerId newState = - state.AddOrUpdate(playerId, newState, (fun _ _ -> newState)) - |> ignore - - let relevantPlayers = - merged - |> List.choose (fun (part, playerId) -> + let getRelevantPlayers players = + players + |> List.choose (fun (part, playerId) -> match part, playerId with - | Participant (instance, _, bot), Some playerId -> - Some (playerId, instance, bot) - | Observer (instance, bot), Some playerId -> - Some (playerId, instance, (fun data -> bot data; [])) - | Computer _, _ -> None - | _ -> failwithf "Expected playerId when participant or observer but not when computer. %A" (part,playerId) - ) + |Participant (instance, _, bot), Some playerId -> + PlayerData.Create playerId instance bot |> Ok |> Some + //(playerId, instance, bot) :: state + |Observer (instance, bot), Some playerId -> + PlayerData.Create playerId instance (fun data -> bot data; []) |> Ok |> Some + |Computer _, _ -> None + | _ -> sprintf "Expected playerId when participant or observer but not when computer. %A" (part,playerId) |> ConfigError |> Error |> Some + ) |> Result.listFold // Get the static gameInfo - for (playerId, instance, bot) in relevantPlayers do - let! gameInfo = Instance.getGameInfo instance - let state = getState playerId - updateState playerId { state with GameInfo = gameInfo } - - // Game loop - while true do - for (playerId, instance, bot) in relevantPlayers do - let! obs = Instance.getObservation false instance - // TODO: Higher level support, GetUnits -> Self -> StartLocation - let lastState = getState playerId - let state = - { lastState with - NewObservation = obs - LastObservation = - if not (isNull lastState.NewObservation) then Some lastState.NewObservation - else None } - - let actions = bot state - if not gameSettings.Realtime then - do! Instance.doStep gameSettings.StepSize instance - - updateState playerId { state with LastActions = actions } - - // Execute actions - for (playerId, instance, bot) in relevantPlayers do - let lastState = getState playerId - do! Instance.doActions lastState.LastActions instance |> Async.Ignore - } \ No newline at end of file + let getStaticGameInfo = + List.map (fun (player:PlayerData) -> + Instance.getGameInfo player.Instance + |> Result.mapAsyncInput (fun gi -> player, gi) + ) >> Async.Parallel >> Async.RunSynchronously >> List.ofArray >> Result.listFold + + let getInitialGameState = + List.map (fun (player:PlayerData, gameInfo:ResponseGameInfo) -> + Instance.getObservation false player.Instance + |> Result.mapAsyncInput (fun obs -> player, GameState.InitialState player.PlayerId obs gameInfo) + ) >> Async.Parallel >> Async.RunSynchronously >> List.ofArray >> Result.listFold + + let rec gameLoop playersResult = + match playersResult with + |Ok players -> + players + |> List.map (fun (player:PlayerData, gameState:GameState) -> + let getActions = Result.tryCatch player.Bot (fun _ -> BotError) + + let executeActions actions = + Instance.doActions actions player.Instance //Travis: would this information (ActionResult) ever be useful to a bot? I see no reason against providing it as part of the game state + |> Result.mapAsyncInput (fun x -> actions) + + + let doStep actions = async{ + if not gameSettings.Realtime then + return! + Instance.doStep gameSettings.StepSize player.Instance + |> Result.mapAsyncInput (fun _ -> actions) + else + return Ok actions + } + + let getNextGameState actions = + Instance.getObservation false player.Instance + |> Result.mapAsyncInput (fun obs -> player, gameState.NextGameState actions obs) + + gameState + |> getActions + |> Result.mapAsyncMapper executeActions + |> Result.mapAsync doStep + |> Result.mapAsync getNextGameState + ) |> Async.Parallel |> Async.RunSynchronously |> List.ofArray |> Result.listFold |> gameLoop + |Error er -> Error er + + let! gameLoopInputs = + participants |> List.ofSeq + |> setupAndConnect gameSettings + |> Result.bindAsyncInput getRelevantPlayers + |> Result.bindAsyncInput getStaticGameInfo + |> Result.bindAsyncInput getInitialGameState + + return gameLoopInputs |> gameLoop + } \ No newline at end of file diff --git a/src/s2client-fsharp/s2client-fsharp.fsproj b/src/s2client-fsharp/s2client-fsharp.fsproj index 247ba2c..87a4995 100644 --- a/src/s2client-fsharp/s2client-fsharp.fsproj +++ b/src/s2client-fsharp/s2client-fsharp.fsproj @@ -6,7 +6,8 @@ s2client-dotnet - + + From e6b59314eb95fc741633bc00016472be6dc27eb7 Mon Sep 17 00:00:00 2001 From: Travis Date: Fri, 3 Aug 2018 17:06:52 +0400 Subject: [PATCH 2/2] Edited as per comments --- src/s2client-fsharp/ErrorDefs.fs | 6 ++--- src/s2client-fsharp/Instance.fs | 9 +++---- src/s2client-fsharp/ProtobufConnection.fs | 12 ++++----- src/s2client-fsharp/Railway.fs | 5 ++-- src/s2client-fsharp/Sc2Game.fs | 14 +++++----- src/s2client-test/Program.fs | 24 ++++++++--------- src/s2client-testCSharp/Program.cs | 32 +++++++++++------------ 7 files changed, 49 insertions(+), 53 deletions(-) diff --git a/src/s2client-fsharp/ErrorDefs.fs b/src/s2client-fsharp/ErrorDefs.fs index 0cd439b..6993fab 100644 --- a/src/s2client-fsharp/ErrorDefs.fs +++ b/src/s2client-fsharp/ErrorDefs.fs @@ -1,11 +1,11 @@ namespace Starcraft2 type ApplicationError = - |FailedToEstablishConnection of string + |FailedToEstablishConnection of exn |SendMessageBufferTooSmall |ExpectedBinaryResponse - |FailedToSendMessage of string - |FailedToReceiveMessage of string + |FailedToSendMessage of exn + |FailedToReceiveMessage of exn |NullResultWithNoError |NullResultWithError of string seq |ExecutableNotFound of string diff --git a/src/s2client-fsharp/Instance.fs b/src/s2client-fsharp/Instance.fs index d832fc8..56a6fdb 100644 --- a/src/s2client-fsharp/Instance.fs +++ b/src/s2client-fsharp/Instance.fs @@ -1,7 +1,6 @@ namespace Starcraft2 open SC2APIProtocol -open Rail // manage a starcraft instance module Instance = @@ -62,8 +61,6 @@ module Instance = else s |> Ok - - let getInstance executable = async { let sc2Dir = executable |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName |> System.IO.Path.GetDirectoryName let supportDir = System.IO.Path.Combine(sc2Dir, "Support64") @@ -100,8 +97,8 @@ module Instance = let createGame (instance:Sc2Instance) mapName (participants:Participant list) realTime = async { let req = new RequestCreateGame() - participants - |> List.iter (fun player -> + + for player in participants do let playerSetup = new PlayerSetup() playerSetup.Type <- player.PlayerType match player with @@ -112,7 +109,7 @@ module Instance = playerSetup.Difficulty <- difficulty | Observer -> () req.PlayerSetup.Add(playerSetup) - ) + req.Realtime <- realTime // map diff --git a/src/s2client-fsharp/ProtobufConnection.fs b/src/s2client-fsharp/ProtobufConnection.fs index 41be80c..c858fc4 100644 --- a/src/s2client-fsharp/ProtobufConnection.fs +++ b/src/s2client-fsharp/ProtobufConnection.fs @@ -1,7 +1,6 @@ namespace Starcraft2 open System //open SC2APIProtocol -open Rail module ProtobufConnection = open System.Net.WebSockets @@ -21,7 +20,7 @@ module ProtobufConnection = with |_ when watch.Elapsed < timeout -> getConnectedSocket() - |ex -> ex.Message |> FailedToEstablishConnection |> Error + |ex -> ex |> FailedToEstablishConnection |> Error getConnectedSocket() let receiveBuf = System.ArraySegment(Array.zeroCreate (1024*1024)) @@ -36,7 +35,7 @@ module ProtobufConnection = do! client.SendAsync(send, WebSocketMessageType.Binary, true, tok) |> Async.AwaitTask return Ok () with - |ex -> return ex.Message |> FailedToSendMessage |> Error + |ex -> return ex |> FailedToSendMessage |> Error } let readMessage (client:ClientWebSocket) = async { @@ -54,7 +53,7 @@ module ProtobufConnection = |WebSocketMessageType.Binary, true -> return Ok (curPos + result.Count) |_ -> return Error ExpectedBinaryResponse with - |ex -> return ex.Message |> FailedToReceiveMessage |> Error + |ex -> return ex |> FailedToReceiveMessage |> Error } let parseFrom finalPos = @@ -85,13 +84,14 @@ module ProtobufConnection = messageLoop() ) + let agent = connectedSocket |> Result.map getAgent + let postSendRequest req (agent:MailboxProcessor) = agent.PostAndAsyncReply (fun reply -> SendRequest (req, reply)) let sendRequest req = async{ return! - connectedSocket - |> Result.map getAgent + agent |> Result.bindAsyncBinder (postSendRequest req) } diff --git a/src/s2client-fsharp/Railway.fs b/src/s2client-fsharp/Railway.fs index 7287f89..22022d0 100644 --- a/src/s2client-fsharp/Railway.fs +++ b/src/s2client-fsharp/Railway.fs @@ -1,6 +1,7 @@ -namespace Rail +namespace Starcraft2 -module Result = +[] +module internal Result = // apply either a success function or failure function let either successFunc failureFunc twoTrackInput = diff --git a/src/s2client-fsharp/Sc2Game.fs b/src/s2client-fsharp/Sc2Game.fs index d1459b8..8e84f23 100644 --- a/src/s2client-fsharp/Sc2Game.fs +++ b/src/s2client-fsharp/Sc2Game.fs @@ -31,7 +31,6 @@ type Sc2Bot = GameState -> SC2APIProtocol.Action list type Sc2Observer = GameState -> unit module Sc2Game = - open Rail open Instance type Participant = @@ -119,13 +118,12 @@ module Sc2Game = match part with |Participant (instance, _, _) |Observer (instance, _) -> - Instance.joinGame instance gameSettings.UseFeatureLayers gameSettings.UseRender part.Simple ports - |> Async.RunSynchronously - |> Result.map Some - |> Result.map attachPart - |_ -> None |> attachPart |> Ok - ) - |> Result.listFold + async{ + let! playerId = Instance.joinGame instance gameSettings.UseFeatureLayers gameSettings.UseRender part.Simple ports + return playerId |> Result.map Some |> Result.map attachPart + } + |_ -> async {return None |> attachPart |> Ok} + ) |> Async.Parallel |> Async.RunSynchronously |> List.ofArray |> Result.listFold return! validateParticipants() diff --git a/src/s2client-test/Program.fs b/src/s2client-test/Program.fs index 46d6f9b..044adfb 100644 --- a/src/s2client-test/Program.fs +++ b/src/s2client-test/Program.fs @@ -2,26 +2,26 @@ open System -open Starcraft2 +//open Starcraft2 open SC2APIProtocol [] let main argv = - let userSettings = Sc2SettingsFile.settingsFromUserDir() + //let userSettings = Sc2SettingsFile.settingsFromUserDir() - let instanceSettings = Instance.StartSettings.OfUserSettings userSettings + //let instanceSettings = Instance.StartSettings.OfUserSettings userSettings - let instance() = Instance.start(instanceSettings) |> Async.RunSynchronously + //let instance() = Instance.start(instanceSettings) |> Async.RunSynchronously - let participants = - [ Sc2Game.Participant(instance(), Race.Terran, (fun _ -> [])) - Sc2Game.Computer(Race.Terran, Difficulty.Hard) ] + //let participants = + // [ Sc2Game.Participant(instance(), Race.Terran, (fun _ -> [])) + // Sc2Game.Computer(Race.Terran, Difficulty.Hard) ] - let settings = - { Sc2Game.GameSettings.OfUserSettings userSettings with - Map = @"Ladder2017Season1\AbyssalReefLE.SC2Map" - Realtime = true } - Sc2Game.runGame settings participants |> Async.RunSynchronously + //let settings = + // { Sc2Game.GameSettings.OfUserSettings userSettings with + // Map = @"Ladder2017Season1\AbyssalReefLE.SC2Map" + // Realtime = true } + //Sc2Game.runGame settings participants |> Async.RunSynchronously 0 // return an integer exit code diff --git a/src/s2client-testCSharp/Program.cs b/src/s2client-testCSharp/Program.cs index cec22d6..32588dd 100644 --- a/src/s2client-testCSharp/Program.cs +++ b/src/s2client-testCSharp/Program.cs @@ -9,28 +9,28 @@ class Program { static void Main(string[] args) { - var userSettings = Sc2SettingsFile.settingsFromUserDir(); + //var userSettings = Sc2SettingsFile.settingsFromUserDir(); - var instanceSettings = Instance.StartSettings.OfUserSettings(userSettings); + //var instanceSettings = Instance.StartSettings.OfUserSettings(userSettings); - Func createInstance = - () => Runner.run(Instance.start(instanceSettings)); + //Func createInstance = + // () => Runner.run(Instance.start(instanceSettings)); - var participants = new Sc2Game.Participant[] { - Sc2Game.Participant.CreateParticipant( - createInstance(), - Race.Terran, - (state => (IEnumerable)new SC2APIProtocol.Action[] {})), - Sc2Game.Participant.CreateComputer(Race.Terran, Difficulty.Hard) - }; + //var participants = new Sc2Game.Participant[] { + // Sc2Game.Participant.CreateParticipant( + // createInstance(), + // Race.Terran, + // (state => (IEnumerable)new SC2APIProtocol.Action[] {})), + // Sc2Game.Participant.CreateComputer(Race.Terran, Difficulty.Hard) + //}; - var gameSettings = - Sc2Game.GameSettings.OfUserSettings(userSettings) - .WithMap(@"Ladder2017Season1\AbyssalReefLE.SC2Map") - .WithRealtime(true); + //var gameSettings = + // Sc2Game.GameSettings.OfUserSettings(userSettings) + // .WithMap(@"Ladder2017Season1\AbyssalReefLE.SC2Map") + // .WithRealtime(true); // Runs the game to the end with the given bots / map and configuration - Runner.run(Sc2Game.runGame(gameSettings, participants)); + //Runner.run(Sc2Game.runGame(gameSettings, participants)); } } }