This may have been asked already, but I am considering making a game in Mathematica that two people can play. However, the issue with my idea is that the other player can't view the cards of the first player. Does anyone have any suggestions as to how I would tackle this problem? I would use two different computers, both with Mathematica installed.
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, 8000@192.168.1.2
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.
-
3$\begingroup$ Regarding some of your "gotchas": I ran into these myself and discussed it here. John Fultz mentioned in response that the meaning of
LinkConnectedQ
/LinkReadyQ
simply differs for the unconnected link--it tells you whether it is safe to try writing to the link or callingLinkActivate
on it. $\endgroup$ – Oleksandr R. Mar 26 '14 at 11:32 -
$\begingroup$ @OleksandrR. Thanks for that very useful information. I have updated my answer to incorporate it. $\endgroup$ – WReach Mar 26 '14 at 13:05
-
$\begingroup$ Should the server side be client side in the line right above
chatClient[..]:=
? btw +1, very concise guide. $\endgroup$ – Silvia Apr 6 '14 at 19:29 -
-
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:
-
$\begingroup$ It looks like there's another thing I should become familiar with. :) $\endgroup$ – rcollyer Sep 4 '16 at 1:13
-
$\begingroup$ I didn't think Mathematica would be a reasonable tool to use and build a version of Catan...seems I might have been wrong $\endgroup$ – Sascha Sep 4 '16 at 8:57
-
-
$\begingroup$ @C.E. I've edited the answer with a modified version of receiver that validates its input. Please roll back or do another edit if you don't like it. I think
HoldComplete@System`DisableFormatting@patt
can safely be replaced by_@_@patt
if you find it too long. I figured I would explicitly addSystem`DisableFormatting
because it is not in theSystem`
context in a fresh kernel and failing to prefix it withSystem`
gives a namespace error. $\endgroup$ – Jacob Akkerboom Sep 4 '16 at 20:29 -
$\begingroup$ @JacobAkkerboom It's a good edit, showing how to match messages is useful also for cases where several different messages may be sent. $\endgroup$ – C. E. Sep 4 '16 at 20:48
MathLink
in the docs and see if that's what you're looking for. Especially there is an article in the docs named "Using Mathlink to communicate between Mathematica sessions" $\endgroup$ – C. E. Mar 14 '14 at 22:13