Multiplayer game in Mathematica: how to communicate between kernels?
MathLink can be used to establish communication between two kernels on different machines using TCP/IP.
A basic example that illustrates the technique can be found in the Mathematica documentation.
The basic sequence of events is like this:
- The server creates an endpoint using LinkCreate.
- The client connects using LinkConnect.
- When one side wants to send a message to the other, it uses LinkWrite.
- Each side periodically polls to see whether the other has sent a message using LinkReadyQ. If it returns
True
, then the message can be read using LinkRead.
There are some gotchas to be aware of:
- A client should write a message to the server immediately after connecting. This establishes the actual connection. Also, I have experienced erratic behaviour from
LinkConnectedQ
andLinkReadyQ
until that first message arrives at the server. UPDATE @OleksandrR. points out that the undocumented functionLinkActivate
will correct this issue. See his discussion here. - On principle, I recommend reading incoming messages in held form using
LinkRead[..., HoldComplete]
and interpreting them using purpose-built code. If you simply evaluate the messages directly, it opens a window for a prankster (or worse) to execute arbitrary code on your machine. - During the initial debugging stage, it is easy for communicating programs to get out of synch with one another, resulting in hangs. Sometimes the kernel resists aborting or interrupting expressions when engaged in blocking I/O. I have found it useful to create buttons somewhere in a notebook that will close or interrupt the active link when pressed. The button actions run with a separate kernel link and can sometimes be helpful to forcibly terminate a cranky connection.
- The link connection name needs to be communicated from the server to the client by some out-of-band means (e.g. the server player must tell/text/IM/email his chosen IP address and port to the client player).
The following (v9) code implements a toy one-line chat application for a client/server pair. First, a function to create the chat panel used by both sides:
chat[link_] :=
DynamicModule[{button, tick=0, valid=True, ready, connected, in="", out=""}
, button = Function[, Button[##, Enabled -> Dynamic[valid]], HoldAll]
; DynamicWrapper[
Grid[
{ {"Link name:", Dynamic@If[valid, link[[1]], "Closed"]}
, {"Tick:", Dynamic@tick}
, {"Message:", Dynamic[in]}
, {button["Send", LinkWrite[link, out]; out=""], InputField[Dynamic@out, String]}
, {button["Close", LinkClose[link]]}
}
]
, Refresh[
tick += 1
; {valid, connected, ready} =
Quiet @ Check[
{True, LinkConnectedQ @ link, LinkReadyQ @ link}
, {False, False, False}
]
; If[ready, in = LinkRead[link]]
, If[valid, UpdateInterval -> 0, None]
, TrackedSymbols :> {}
]
]
]
Next, a function to create a server-side chat panel that listens on a specified IP port:
chatServer[port_] :=
chat @ LinkCreate[ToString@port, LinkProtocol -> "TCPIP"]
Finally, a function to create a client-side chat panel using the link name shown on the server's panel:
chatClient[linkName_] :=
Module[{link = LinkConnect[linkName, LinkProtocol -> "TCPIP"]}
, LinkActivate[link]
; chat[link]
]
The server creates his panel...
... then communicates the link name to the client by some means. Only the first part of the name needs to be communicated, [email protected]
in the example. The client then uses this name to create his panel:
The two can then communicate until one of them presses the Close button.
The Tick field in this example simulates the background work performed by the game while waiting for communication from its peer. The code polls for messages as fast as possible, but this rate can be throttled by adjusting the UpdateInterval
option to Refresh
in chat
.
As is customary in these toy examples, little attempt is made to recover from error conditions.
Thanks to the new Channel
functionality in Mathematica 11, it is now easy to make (basic) multiplayer games with Mathematica!
To demonstrate, I'll modify my hex game from The Game of Hex in Mathematica. Start by copying all that code into a notebook and evaluate it. Then evaluate the following code:
boardClicked[{i_, j_}] := If[
board[[i, j]] == 0 && currentPlayer == me,
board[[i, j]] = me;
currentPlayer = opponent;
ChannelSend["test", {me, {i, j}}];
]
receiver::"hacks" =
"Incorrect/illegitmate message received, proceed with caution";
attomHACQ = Function[Null, TrueQ@AtomQ@Unevaluated@#, HoldAllComplete];
validHCMQ[hcm_] :=
TrueQ@MatchQ[hcm,
HoldComplete@
System`DisableFormatting@{_Integer?attomHACQ,
{_Integer?attomHACQ, _Integer?attomHACQ}}];
receiver[m_] :=
Module[{sender, i, j, hcm},
hcm = m["Message"];
If[
validHCMQ[hcm]
,
{sender, {i, j}} = ReleaseHold[hcm];
If[sender == opponent && currentPlayer == opponent,
board[[i, j]] = opponent;
currentPlayer = me;]
,
Message[receiver::"hacks"]
]
];
Unprotect[$AllowExternalChannelFunctions];
$AllowExternalChannelFunctions = True;
listener = ChannelListen["test", receiver];
me = 1;
opponent = me /. {1 -> 2, 2 -> 1};
currentPlayer = 1;
board = ConstantArray[0, {11, 11}];
Dynamic[renderBoard[board], TrackedSymbols :> {board}]
Now, open another notebook and assign a different kernel to it. Evaluate the same code in the new notebook as for the first one, except set me = 2
instead of me = 1
. The result should work like this: