This is a Virtual Tabletop (VTT) for playing the german card game Schafkopf (sheepshead) – with 4 human players and game types of 6 or 8 dealt cards. Link to the (german text only) sub-website with game access:

Basic Idea

During the Corona lock-down, our regular face-to-face card game round wasn’t possible. So, we looked for online alternatives. And there are quite a few. Most of them well done and with fancy style.
Some have (legit) paywalls. Some are, like, micro-payment bait pools (not so cool). Others – well, we simply couldn’t handle them (not their fault, I guess).
Finally, I decided to try and create an “online card table” for our group myself. It should – at least – run on PC (preferably, browser), and just “simulate the table”. So the 4 of us could freely play special rules and chat/rant over voice chat – as usual.


This list is compiled out of the group’s needs, also considering my personal ideas and preferences (also, some came up late, while developing):

  • Simulate a Schafkopf card game *table* with basic functionality
    (4 players, shuffle/deal, play cards, count points).
  • Leave “high level” decision to players and their online conversation
    (e.g. main player selection, decision of card to play).
  • Do not supervise rules/allowed cards, (virtual) payments and the like.
  • No need for computer players. All participants will be / need to be humans.
  • Shall be able to handle 8-card and also 6-card Schafkopf game types
    (“Langer”, “Kurzer”).
  • Do use as less customer data collection as possible (e.g. no registering),
    and make access easy
  • Move as much logic as possible to an authorative server (“never trust
    the client”).
  • Make it “multi-table” (thought to present it as a general service for different
    groups playing simultaneously).
  • Should run on PC and tablet, optional on mobile (we don’t like those small
    screens for gaming. So, no priority).
  • Utilize some development environment I’ve not used before (at least, partly),
    for learning purposes.
(card pictures by nsv, btw.)

Choice of Weapons (Toolchain)

Decided to make the client a browser app (html, css, javascript) – Unity and such looked like overkill here. I mean, after all, it is “just a few card pics being moved around”. It could all be done with html/css here, I believe. Added Phaser3 and jQuery still, as it made life somewhat easier with positioning, scaling and effects. Packing of the client sources into a single file for delivery happened with webpack.

As for the server, first game versions ran as php scripts on the website’s Apache, connecting to mySQL. Although this worked, it involved frequent polling from the client – and with it a somehow slow game pace (tradeoff: polling frequency vs. game actions update).
Finally, I discovered “long polling” (didn’t know facebook uses this, btw) – and with it, So, switched to nodeJS, using and express framework. Listening on a dedicated port (8080) of the server, so it could run separately from the existing LAMP website infrastructure.

Note: I was too lazy to set up a TypeScript environment and thus went for pure JS. Quite a lot of unnecessary debug sessions happened due to this decision (typos etc).

The supporting tools were as usual

  • Visual Studio Code for editing (separate workspaces for client and server)
  • C4D for some graphics creation/rendering (logo and such)
  • Python for creating/processing card images and atlasses (e.g. combine pictures, add transparency masks)
  • Good ol’ PaintShopPro5 for pixel pushing and graphics resizing (one day, will move to gimp…)

Recently, github allowed for closed private projects. So, I moved “Schafkopf-Runde” to there for code versioning. Also, it was the first time I used GitHub Desktop. Liked it a lot. Really. GitHub is cool.

overview of used components

Data Exchange

To avoid data collection and ease up usage, participants of a game just need a link with a GET parameter for the table ID attached. All players then use this link to “seat” at the same table.
The table ID is created out of current server unix timestamp [ms] with a random string added. This should ensure enough “unique-ness” (considering the numbers of users expected: just a few). Therefore, a table link does look like so:

After initial client-server handshake and exchange of table number and player data (name), the server sends a full state update with each change. It is in JSON format, looking like so:


Note that the state is assembled for each client individually, excluding the cards of other participants. Just to make “sniffing” a bit more difficult So, technically, it is not a full game state.

State Machine

The game’s state machine is considerably simple. After all, we’re just simulating “the table”, not the card game itself. This makes condition checks a lot easier. All state calculations are done on server side, triggered by client input. The clients focus on game representation mainly, not game logic.

Visual Representation

In Phaser3, the base resolution has been set to 1024*768 (4:3). The whole canvas is then dynamically scaled to fit the browser window (keeping aspect ratio). This made work easier (no manual positioning/scaling of elements upon screen size change). But it causes the graphics to show up a bit “blurry”. Should really go for a rework there, so everything looks “more crispy”.

It is always to debate if gui elements (especially dialogs) should be done within phaser, or be part of website (html/css) and shown as an overlay. In my case, the very first dialog (name submission) is overlay html/css. All others are within the Phaser canvas, so they scale with it.


An interesting option would be to run the nodeJS server on e.g. Heroku as a cloud applet. Maybe, I will move to that later. For now, “Schafkopf-Runde” runs as a systemd service on my Debian LAMP server. As it should not interfere with the website, it is listening on port 8080. Some corporate networks may block this, though.

Preferably, SSL/TLS should be used. So, I implemented a “switching” for http/https requests and also internal linkage to let’s encrypt certificates. Those already existed on the server for the main website.
Further info on these two topics (basically, I took the code from there):