[MouseFox logo]
The MouseFox Project
Join The Community Discord Server
[Discord logo]
Edit on GitHub

mousefox

MouseFox

MouseFox is a framework for building turn-based multiplayer games in Python with minimum boilerplate.

See the documentation for details, and be sure to join the community discord server.

Install

pip install git+https://github.com/ArielHorwitz/mousefox.git

Built-in examples

Tic-tac-toe:

import mousefox

mousefox.examples.tictactoe()

Text chat:

import mousefox

mousefox.examples.chat()

Getting started

When working with MouseFox, we will become familiar with PGNet and Kivy. Let's walk through making a simple multiplayer clicker game.

Game logic

We require a pgnet.Game class. This class will be initialized by the server whenever a user creates a new game:

import pgnet

class ClickerGame(pgnet.Game):
    clicks = 0

    def handle_heartbeat(self, packet: pgnet.Packet) -> pgnet.Response:
        # Return game updates
        message = f"Number of clicks: {self.clicks}"
        return pgnet.Response(message)

    def handle_game_packet(self, packet: pgnet.Packet) -> pgnet.Response:
        # Handle requests and actions
        self.clicks += 1
        return pgnet.Response("Clicked.")

Game widget

We require a Kivy Widget class. This class will be initialized by the GUI whenever the user joins a game.

from kivy.uix.label import Label
import pgnet

class ClickerWidget(Label):
    def __init__(self, client: pgnet.Client):
        # Init the kivy widget and bind to client heartbeat
        super().__init__(text="Waiting for data from server...")
        self.client = client
        self.client.on_heartbeat = self.on_heartbeat

    def on_heartbeat(self, heartbeat: pgnet.Response):
        # Use heartbeat to get game updates
        self.text = heartbeat.message

    def on_touch_down(self, touch):
        # Use client to send requests and do actions
        if self.collide_point(*touch.pos):
            self.client.send(pgnet.Packet("Click"))

Running the app

We can now call mousefox.run to run the app:

import mousefox

mousefox.run(
    game_widget=ClickerWidget,
    game_class=ClickerGame,
)

Check out the examples source code.

  1""".. include:: ../README.md
  2
  3# Install
  4```bash
  5pip install git+https://github.com/ArielHorwitz/mousefox.git
  6```
  7
  8# Built-in examples
  9Tic-tac-toe:
 10```python3
 11import mousefox
 12
 13mousefox.examples.tictactoe()
 14```
 15
 16Text chat:
 17```python3
 18import mousefox
 19
 20mousefox.examples.chat()
 21```
 22
 23# Getting started
 24When working with MouseFox, we will become familiar with
 25[PGNet](https://github.com/ArielHorwitz/pgnet) and [Kivy](https://kivy.org/doc/stable/).
 26Let's walk through making a simple multiplayer clicker game.
 27
 28## Game logic
 29We require a `pgnet.Game` class. This class will be initialized by the server whenever a
 30user creates a new game:
 31```python3
 32import pgnet
 33
 34class ClickerGame(pgnet.Game):
 35    clicks = 0
 36
 37    def handle_heartbeat(self, packet: pgnet.Packet) -> pgnet.Response:
 38        # Return game updates
 39        message = f"Number of clicks: {self.clicks}"
 40        return pgnet.Response(message)
 41
 42    def handle_game_packet(self, packet: pgnet.Packet) -> pgnet.Response:
 43        # Handle requests and actions
 44        self.clicks += 1
 45        return pgnet.Response("Clicked.")
 46```
 47
 48## Game widget
 49We require a Kivy `Widget` class. This class will be initialized by the GUI whenever the
 50user joins a game.
 51```python3
 52from kivy.uix.label import Label
 53import pgnet
 54
 55class ClickerWidget(Label):
 56    def __init__(self, client: pgnet.Client):
 57        # Init the kivy widget and bind to client heartbeat
 58        super().__init__(text="Waiting for data from server...")
 59        self.client = client
 60        self.client.on_heartbeat = self.on_heartbeat
 61
 62    def on_heartbeat(self, heartbeat: pgnet.Response):
 63        # Use heartbeat to get game updates
 64        self.text = heartbeat.message
 65
 66    def on_touch_down(self, touch):
 67        # Use client to send requests and do actions
 68        if self.collide_point(*touch.pos):
 69            self.client.send(pgnet.Packet("Click"))
 70```
 71
 72## Running the app
 73We can now call `mousefox.run` to run the app:
 74```python3
 75import mousefox
 76
 77mousefox.run(
 78    game_widget=ClickerWidget,
 79    game_class=ClickerGame,
 80)
 81```
 82
 83Check out the examples
 84[source code](https://github.com/ArielHorwitz/mousefox/tree/master/mousefox/examples/).
 85"""  # noqa: D415
 86
 87
 88import os
 89import sys
 90import asyncio
 91from loguru import logger
 92from . import app
 93from . import examples  # noqa: F401
 94from . import util  # noqa: F401
 95
 96
 97def run(**kwargs):
 98    """Run the app. Takes arguments like `mousefox.AppConfig`.
 99
100    See also: `async_run`.
101    """
102    asyncio.run(async_run(**kwargs))
103
104
105async def async_run(**kwargs):
106    """Coroutine to run the app. Takes arguments like `mousefox.AppConfig`.
107
108    See also: `run`.
109    """
110    logger.info("Starting MouseFox.")
111    config = app.app.AppConfig(**kwargs)
112    mf_app = app.app.App(config)
113    exit_code = await mf_app.async_run()
114    if not config.allow_quit:
115        return
116    # Restart if exit code is -1
117    if exit_code == -1:
118        logger.info("Restarting MouseFox...")
119        os.execl(sys.executable, sys.executable, *sys.argv)
120    logger.info("Closing MouseFox.")
121    quit()
122
123
124AppConfig = app.app.AppConfig
def run(**kwargs):
 98def run(**kwargs):
 99    """Run the app. Takes arguments like `mousefox.AppConfig`.
100
101    See also: `async_run`.
102    """
103    asyncio.run(async_run(**kwargs))

Run the app. Takes arguments like mousefox.AppConfig.

See also: async_run.

async def async_run(**kwargs):
106async def async_run(**kwargs):
107    """Coroutine to run the app. Takes arguments like `mousefox.AppConfig`.
108
109    See also: `run`.
110    """
111    logger.info("Starting MouseFox.")
112    config = app.app.AppConfig(**kwargs)
113    mf_app = app.app.App(config)
114    exit_code = await mf_app.async_run()
115    if not config.allow_quit:
116        return
117    # Restart if exit code is -1
118    if exit_code == -1:
119        logger.info("Restarting MouseFox...")
120        os.execl(sys.executable, sys.executable, *sys.argv)
121    logger.info("Closing MouseFox.")
122    quit()

Coroutine to run the app. Takes arguments like mousefox.AppConfig.

See also: run.

@dataclass
class AppConfig:
22@dataclass
23class AppConfig:
24    """Configuration for MouseFox app."""
25
26    game_widget: kvex.kivy.Widget
27    """Kivy widget for the game."""
28    game_class: Type[pgnet.Game]
29    """Game subclass for the local client and server (see `pgnet.Server`)."""
30    client_class: Type[pgnet.Client] = pgnet.Client
31    """Client subclass for the client (see `pgnet.Client`)."""
32    server_factory: Optional[Callable] = None
33    """The server factory for the local client (see `pgnet.Client.local`)."""
34    disable_local: bool = False
35    """Disable local clients."""
36    disable_remote: bool = False
37    """Disable remote clients."""
38    maximize: bool = False
39    """If app should start maximized."""
40    borderless: bool = False
41    """If app should not have window borders."""
42    size: Optional[tuple[int, int]] = (1280, 768)
43    """App window size in pixels."""
44    offset: Optional[tuple[int, int]] = None
45    """App window offset in pixels."""
46    title: str = "MouseFox"
47    """App window title."""
48    info_text: str = "No info available."
49    """Text to show when ready to connect."""
50    online_info_text: str = "No online info available."
51    """Text to show when ready to connect remotely."""
52    allow_quit: bool = True
53    """Allow MouseFox to quit or restart the script."""
54
55    def __getitem__(self, item):
56        """Get item."""
57        return getattr(self, item)
58
59    def keys(self):
60        """Enables mapping.
61
62        For example:
63        ```python3
64        config = mousefox.AppConfig(...)
65        mousefox.run(**config)
66        ```
67        """
68        return self.__dataclass_fields__.keys()

Configuration for MouseFox app.

AppConfig( game_widget: kivy.uix.widget.Widget, game_class: Type[pgnet.util.Game], client_class: Type[pgnet.client.Client] = <class 'pgnet.client.Client'>, server_factory: Optional[Callable] = None, disable_local: bool = False, disable_remote: bool = False, maximize: bool = False, borderless: bool = False, size: Optional[tuple[int, int]] = (1280, 768), offset: Optional[tuple[int, int]] = None, title: str = 'MouseFox', info_text: str = 'No info available.', online_info_text: str = 'No online info available.', allow_quit: bool = True)
game_widget: kivy.uix.widget.Widget

Kivy widget for the game.

game_class: Type[pgnet.util.Game]

Game subclass for the local client and server (see pgnet.Server).

client_class: Type[pgnet.client.Client] = <class 'pgnet.client.Client'>

Client subclass for the client (see pgnet.Client).

server_factory: Optional[Callable] = None

The server factory for the local client (see pgnet.Client.local).

disable_local: bool = False

Disable local clients.

disable_remote: bool = False

Disable remote clients.

maximize: bool = False

If app should start maximized.

borderless: bool = False

If app should not have window borders.

size: Optional[tuple[int, int]] = (1280, 768)

App window size in pixels.

offset: Optional[tuple[int, int]] = None

App window offset in pixels.

title: str = 'MouseFox'

App window title.

info_text: str = 'No info available.'

Text to show when ready to connect.

online_info_text: str = 'No online info available.'

Text to show when ready to connect remotely.

allow_quit: bool = True

Allow MouseFox to quit or restart the script.

def keys(self):
59    def keys(self):
60        """Enables mapping.
61
62        For example:
63        ```python3
64        config = mousefox.AppConfig(...)
65        mousefox.run(**config)
66        ```
67        """
68        return self.__dataclass_fields__.keys()

Enables mapping.

For example:

config = mousefox.AppConfig(...)
mousefox.run(**config)