A multiplayer library in testing phase

Hi there, Elm community!

Have you ever played a card game or board game in Elm like Orasund/elm-card-game and wished you could play it with your friends online? I have been working on a solution to this problem, and I would like your feedback on the library that I am developing.

I am currently building a Matrix SDK in Elm that will allow you to play games together with your friends, regardless of whether or not you are in the same room. For those who are unfamiliar, Matrix is an open protocol for clients and servers to communicate, and every Matrix server allows you to exchange JSON values with other clients, even if they are not on the same server.

I am facing some challenges with designing the library, and I would like your input on what you think the library needs to look like for you to start using it. Currently, I have not seen an Elm library that allows for connecting to an API without using ports, so I am exploring different design options. Some possible options I am considering include creating a new Program type, offering a Vault type, or providing init, update, view, and subscriptions wrapper functions.

To demonstrate how the library would work, I have created a simple example of a chess client using pilatch/elm-chess and my own library in development. You can try it out by visiting chess.noordstar.me with your Matrix account.

My goal is to make it easy and intuitive for any game to be connected to the Matrix API. What do you need the library to look like to say “and now you can do this with other people” by taking the easiest and most pleasant steps?

I would love to hear your thoughts and feedback!

2 Likes

First wanted to say that I love this!

As far as what the interface should look like, I’d appreciate not having a custom Program because that makes it difficult if not impossible to work with other packages that require some sort of port access or flags access.

It also would be nice to see maybe a rough sketch of what is needed by your init, update, etc.

A rough sketch would be to make something similar like xarvh/elm-slides. It documents the following functions:

  • init : Options -> List Slide -> () -> Url -> Key -> ( Model, Cmd Msg )
  • update : Options -> Msg -> Model -> ( Model, Cmd Msg )
  • subscriptions : Options -> Model -> Sub Msg
  • view : Options -> Model -> Document Msg

In essence, building such function wrappers could allow people to build ports, and potentially even allows users to stack multiple libraries with similar wrapper functions.

For example, the Matrix library could have functions like these:

  • init : (flags -> (model, Cmd msg)) -> (flags -> (Matrix.Model model, Cmd (Matrix.Msg msg))
  • update : (Matrix.Vault -> msg -> model -> ( model, Cmd msg)) -> Matrix.Msg msg -> Matrix.Model model -> (Matrix.Model model, Cmd (Matrix.Msg msg))
  • subscriptions : (model -> Sub msg) -> Matrix.Model model -> Sub (Matrix.Msg msg)
  • view : (Vault -> model -> Html msg) -> Matrix.Model model -> Html (Matrix.Msg msg)

The functions could have types like Matrix.Model model and Matrix.Msg msg for containing the types.

The documentation of the library would then say “you may assume that there’s a connection, and we’ll handle everything else (keeping connection, syncing data, etc.) for you.”.

This type definition seems a bit complex, so let me clarify:

User could still define their own init, update, subscriptions and view like they’re used to. However, when defining their browser, it would look like this:

import Matrix.Browser as B

main =
    Browser.element
        { init = init |> B.init
        , update = update |> B.update
        , subscriptions = subscriptions |> B.subscriptions
        , view = view |> B.view
        }

With their functions defined as follows:

init : () -> ( model, Cmd msg )

update : Matrix.Vault -> msg -> model -> ( model, Cmd msg )

subscriptions : Matrix.Vault -> model -> Sub msg

view : Matrix.Vault -> model -> Html msg

I wonder if it’d make more sense, maybe not, to invert this approach so that I as a user of this package could write something like

import Matrix as M

type Msg = MatrixMsg M.Msg

init flags =
    let
        ( matrixModel, matrixCmd ) =
            M.init
    in
    ( { matrixModel =  matrixModel }
    , Cmd.map MatrixMsg matrixCmd
    )

subscriptions model =
    Sub.map MatrixMsg (M.subscriptions model.matrixModel)

update msg model =
    case msg of
        MatrixMsg msg_ ->
            M.update
                { msg = msg_
                , model = model.matrixModel
                , toMsg = MatrixMsg
                , toModel = \matrixModel -> { model | matrixModel = matrixModel }
                }

That’s a good idea, it also simplifies a few other challenges that I’m facing.

That’s similar to the original Vault type strategy that I had in mind, where users have one black box that they feed with VaultUpdate (read: M.Msg) types to keep it up-to-date.

Aren’t you worried, however, that this approach can be a bit complex? I can imagine that users wouldn’t mind adding a B.init <| in their Browser.element definition but that they might shy away from creating a submodule structure.

Initially it might seem complex, however we use this at work a lot and it means that you can very easily change both the internals of the “submodule” as well as the parent without concerns around breaking the other. It also makes your Matrix package very portable. I can now use it from with elm-pages, Elm Land, etc with minimal effort. With the reverse approach I have to figure out how to modify elm-pages or Elm Land to somehow use your Matrix package which would be much more challenging.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.