From d489a4acca6d310188f5d6f967ade66636b7c0b1 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 14:01:53 +0100 Subject: [PATCH 01/17] feat: add support for generating games using the GAMUT suite via load_gamut --- doc/catalog.rst | 27 +++++++++++ doc/pygambit.api.rst | 1 + src/pygambit/catalog.py | 100 ++++++++++++++++++++++++++++++++++++++++ tests/test_catalog.py | 96 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+) diff --git a/doc/catalog.rst b/doc/catalog.rst index dfb0875e2..1af0074ff 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -6,6 +6,7 @@ Catalog of games Below is a complete list of games included in Gambit's catalog. Check out the :ref:`pygambit API reference ` for instructions on how to search and load these games in Python, and the :ref:`Updating the games catalog ` guide for instructions on how to contribute new games to the catalog. Games from the OpenSpiel library are also available; see :ref:`Loading OpenSpiel games `. +Games can also be generated on the fly from the GAMUT suite; see :ref:`Generating GAMUT games `. .. include:: catalog_table.rst @@ -31,3 +32,29 @@ not available on Windows). The game is exported to EFG or NFG format on the fly and loaded into Gambit. Not all OpenSpiel games can be exported; a :class:`ValueError` is raised for games that are incompatible with either format. See the :doc:`OpenSpiel interoperability tutorial ` for worked examples. + +.. _catalog-gamut: + +.. rubric:: Generating GAMUT games + +Games from the `GAMUT `_ generator suite can be created +using :func:`pygambit.catalog.load_gamut`: + +.. code-block:: python + + pygambit.catalog.load_gamut("RandomGame", params={"players": 2, "actions": 3}) + pygambit.catalog.load_gamut("CovariantGame", params={"players": 2, "actions": [3, 3]}) + pygambit.catalog.load_gamut( + "RandomGame", + params={"players": 3, "actions": 4, "normalize": True, "min_payoff": 0, "max_payoff": 100}, + ) + +The ``params`` argument maps directly to GAMUT command-line flags. Boolean flags such as +``-normalize`` and ``-int_payoffs`` are passed as ``True``; list values for ``-actions`` +expand to space-separated tokens. See the +`GAMUT documentation `_ for the full list +of game classes and parameters. + +GAMUT requires Java and ``gamut.jar`` to be installed. Provide the path to ``gamut.jar`` +via the ``gamut_jar`` argument or the ``GAMUT_JAR`` environment variable. Download +GAMUT from http://gamut.stanford.edu/. diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index da49ec907..9ac6aefd4 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -350,4 +350,5 @@ Catalog of games load load_openspiel + load_gamut games diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 2b6aadaeb..f35e3172b 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -1,4 +1,8 @@ import io +import os +import shutil +import subprocess +import tempfile from importlib.resources import as_file, files from pathlib import Path from typing import Any @@ -95,6 +99,102 @@ def load_openspiel(game_name: str, params: dict | None = None) -> gbt.Game: ) +def load_gamut( + game_class: str, + params: dict | None = None, + gamut_jar: Path | str | None = None, +) -> gbt.Game: + """ + Generate a game using the GAMUT game generator. + + GAMUT (http://gamut.stanford.edu) is a Java-based suite of parameterised + game generators. This function calls GAMUT as an external process and + returns the resulting game as a :class:`~pygambit.Game` object. + + Parameters + ---------- + game_class : str + The GAMUT game class name (e.g. ``"RandomGame"``, ``"CovariantGame"``). + See the `GAMUT documentation `_ + for available game classes and their parameters. + params : dict, optional + Parameters forwarded to GAMUT as command-line flags. Each key becomes + ``-key``. Values are handled as follows: + + - ``True`` → flag with no value token + (e.g. ``{"normalize": True}`` → ``-normalize``) + - list or tuple → space-separated tokens + (e.g. ``{"actions": [3, 3]}`` → ``-actions 3 3``) + - anything else → single token + (e.g. ``{"players": 2}`` → ``-players 2``) + + Multi-word GAMUT flags use underscores: + ``{"min_payoff": 0, "max_payoff": 100}``. + gamut_jar : pathlib.Path or str, optional + Path to ``gamut.jar``. If omitted, the ``GAMUT_JAR`` environment + variable is used. Raises :class:`FileNotFoundError` if neither is + supplied. + + Returns + ------- + Game + The generated game. + + Raises + ------ + RuntimeError + If ``java`` is not found on the system PATH. + FileNotFoundError + If ``gamut.jar`` cannot be located. + ValueError + If GAMUT exits with a non-zero return code (e.g. invalid game class + or parameters). + """ + if shutil.which("java") is None: + raise RuntimeError( + "Java is required to run GAMUT. " + "Install Java and ensure 'java' is on your PATH." + ) + if gamut_jar is None: + env_jar = os.environ.get("GAMUT_JAR") + if env_jar is None: + raise FileNotFoundError( + "gamut.jar not found. Provide the path via the 'gamut_jar' argument " + "or set the GAMUT_JAR environment variable. " + "Download GAMUT from http://gamut.stanford.edu/." + ) + gamut_jar = Path(env_jar) + else: + gamut_jar = Path(gamut_jar) + if not gamut_jar.is_file(): + raise FileNotFoundError(f"gamut.jar not found at {gamut_jar}") + + cmd = ["java", "-jar", str(gamut_jar), "-g", game_class, "-output", "GambitOutput"] + if params: + for key, value in params.items(): + cmd.append(f"-{key}") + if value is True: + pass # boolean flags like -normalize, -int_payoffs take no value token + elif isinstance(value, (list, tuple)): + cmd.extend(str(v) for v in value) + else: + cmd.append(str(value)) + + with tempfile.NamedTemporaryFile(suffix=".nfg", delete=False) as tmp: + out_path = Path(tmp.name) + try: + cmd.extend(["-f", str(out_path)]) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise ValueError( + f"GAMUT failed for game class '{game_class}': " + f"{result.stderr.strip() or result.stdout.strip()}" + ) + return gbt.read_nfg(str(out_path)) + finally: + out_path.unlink(missing_ok=True) + + def load(slug: str) -> gbt.Game: """ Load a game from the package catalog. diff --git a/tests/test_catalog.py b/tests/test_catalog.py index f807fdde2..3470da94e 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,4 +1,5 @@ import sys +from pathlib import Path from unittest.mock import MagicMock import pandas as pd @@ -307,3 +308,98 @@ def test_openspiel_load_with_params(monkeypatch): ) gbt.catalog.load_openspiel("blotto", params={"players": 2, "coins": 3, "fields": 2}) mock_ps.load_game.assert_called_once_with("blotto", {"players": 2, "coins": 3, "fields": 2}) + + +# --------------------------------------------------------------------------- +# GAMUT game generation tests (all mocked; Java and gamut.jar not required) +# --------------------------------------------------------------------------- + + +def _fake_gamut_run(nfg_content, returncode=0, stderr=""): + """Return a fake subprocess.run callable that writes nfg_content to the -f path.""" + def _run(cmd, **kwargs): + if returncode == 0: + f_idx = cmd.index("-f") + Path(cmd[f_idx + 1]).write_text(nfg_content) + result = MagicMock() + result.returncode = returncode + result.stderr = stderr + result.stdout = "" + return result + return _run + + +def test_load_gamut_success(monkeypatch, tmp_path): + """Happy path: GAMUT writes an NFG file and load_gamut returns a Game.""" + fake_jar = tmp_path / "gamut.jar" + fake_jar.touch() + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") + monkeypatch.setattr("pygambit.catalog.subprocess.run", _fake_gamut_run(_MOCK_NFG)) + game = gbt.catalog.load_gamut("RandomGame", gamut_jar=fake_jar) + assert isinstance(game, gbt.Game) + + +def test_load_gamut_with_params(monkeypatch, tmp_path): + """params dict is translated correctly to GAMUT command-line flags.""" + fake_jar = tmp_path / "gamut.jar" + fake_jar.touch() + captured = {} + + def _run(cmd, **kwargs): + captured["cmd"] = cmd + Path(cmd[cmd.index("-f") + 1]).write_text(_MOCK_NFG) + result = MagicMock() + result.returncode = 0 + return result + + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") + monkeypatch.setattr("pygambit.catalog.subprocess.run", _run) + gbt.catalog.load_gamut( + "RandomGame", + params={"players": 2, "actions": [3, 3], "normalize": True, "min_payoff": 0}, + gamut_jar=fake_jar, + ) + cmd = captured["cmd"] + assert "-players" in cmd and cmd[cmd.index("-players") + 1] == "2" + assert "-actions" in cmd + actions_idx = cmd.index("-actions") + assert cmd[actions_idx + 1] == "3" and cmd[actions_idx + 2] == "3" + assert "-normalize" in cmd + normalize_idx = cmd.index("-normalize") + assert cmd[normalize_idx + 1] == "-min_payoff" # no value token after -normalize + assert "-min_payoff" in cmd and cmd[cmd.index("-min_payoff") + 1] == "0" + + +def test_load_gamut_no_java(monkeypatch, tmp_path): + """Missing java on PATH raises RuntimeError.""" + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: None) + with pytest.raises(RuntimeError, match="Java is required"): + gbt.catalog.load_gamut("RandomGame", gamut_jar=tmp_path / "gamut.jar") + + +def test_load_gamut_jar_not_found(monkeypatch, tmp_path): + """Non-existent gamut_jar path raises FileNotFoundError.""" + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") + with pytest.raises(FileNotFoundError, match="gamut.jar not found at"): + gbt.catalog.load_gamut("RandomGame", gamut_jar=tmp_path / "missing.jar") + + +def test_load_gamut_no_jar_env(monkeypatch): + """With gamut_jar=None and GAMUT_JAR unset, raises FileNotFoundError.""" + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") + monkeypatch.delenv("GAMUT_JAR", raising=False) + with pytest.raises(FileNotFoundError, match="GAMUT_JAR"): + gbt.catalog.load_gamut("RandomGame") + + +def test_load_gamut_gamut_fails(monkeypatch, tmp_path): + """Non-zero returncode from GAMUT raises ValueError naming the game class.""" + fake_jar = tmp_path / "gamut.jar" + fake_jar.touch() + monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") + monkeypatch.setattr( + "pygambit.catalog.subprocess.run", + _fake_gamut_run("", returncode=1, stderr="Unknown game class 'Bogus'"), + ) + with pytest.raises(ValueError, match="Bogus"): + gbt.catalog.load_gamut("Bogus", gamut_jar=fake_jar) From 5776eb0c43540abfa579a872ff36a118804f9e5f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 14:05:51 +0100 Subject: [PATCH 02/17] docs: add gamut_jar parameter to catalog examples --- doc/catalog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/catalog.rst b/doc/catalog.rst index 1af0074ff..81b8858ee 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -42,11 +42,12 @@ using :func:`pygambit.catalog.load_gamut`: .. code-block:: python - pygambit.catalog.load_gamut("RandomGame", params={"players": 2, "actions": 3}) - pygambit.catalog.load_gamut("CovariantGame", params={"players": 2, "actions": [3, 3]}) + pygambit.catalog.load_gamut("RandomGame", params={"players": 2, "actions": 3}, gamut_jar="/path/to/gamut.jar") + pygambit.catalog.load_gamut("CovariantGame", params={"players": 2, "actions": [3, 3]}, gamut_jar="/path/to/gamut.jar") pygambit.catalog.load_gamut( "RandomGame", params={"players": 3, "actions": 4, "normalize": True, "min_payoff": 0, "max_payoff": 100}, + gamut_jar="/path/to/gamut.jar", ) The ``params`` argument maps directly to GAMUT command-line flags. Boolean flags such as From af1db2e8b4453802bf7c716745ffd5febeaa7c90 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 14:08:42 +0100 Subject: [PATCH 03/17] feat: expand home directory paths for GAMUT jar --- src/pygambit/catalog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index f35e3172b..9fda6ad83 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -163,9 +163,9 @@ def load_gamut( "or set the GAMUT_JAR environment variable. " "Download GAMUT from http://gamut.stanford.edu/." ) - gamut_jar = Path(env_jar) + gamut_jar = Path(env_jar).expanduser() else: - gamut_jar = Path(gamut_jar) + gamut_jar = Path(gamut_jar).expanduser() if not gamut_jar.is_file(): raise FileNotFoundError(f"gamut.jar not found at {gamut_jar}") From 0e905ba39e0f46e6723591170fa99319e8b66045 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 14:15:13 +0100 Subject: [PATCH 04/17] refactor: rename load_gamut to generate_gamut and tidy GAMUT user documentation --- doc/catalog.rst | 15 ++++++++------- doc/pygambit.api.rst | 2 +- src/pygambit/catalog.py | 2 +- tests/test_catalog.py | 26 +++++++++++++------------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/doc/catalog.rst b/doc/catalog.rst index 81b8858ee..b28952e2e 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -38,13 +38,13 @@ See the :doc:`OpenSpiel interoperability tutorial `_ generator suite can be created -using :func:`pygambit.catalog.load_gamut`: +using :func:`pygambit.catalog.generate_gamut`: .. code-block:: python - pygambit.catalog.load_gamut("RandomGame", params={"players": 2, "actions": 3}, gamut_jar="/path/to/gamut.jar") - pygambit.catalog.load_gamut("CovariantGame", params={"players": 2, "actions": [3, 3]}, gamut_jar="/path/to/gamut.jar") - pygambit.catalog.load_gamut( + pygambit.catalog.generate_gamut("RandomGame", params={"players": 2, "actions": 3}, gamut_jar="/path/to/gamut.jar") + pygambit.catalog.generate_gamut("CovariantGame", params={"players": 2, "actions": [3, 3]}, gamut_jar="/path/to/gamut.jar") + pygambit.catalog.generate_gamut( "RandomGame", params={"players": 3, "actions": 4, "normalize": True, "min_payoff": 0, "max_payoff": 100}, gamut_jar="/path/to/gamut.jar", @@ -56,6 +56,7 @@ expand to space-separated tokens. See the `GAMUT documentation `_ for the full list of game classes and parameters. -GAMUT requires Java and ``gamut.jar`` to be installed. Provide the path to ``gamut.jar`` -via the ``gamut_jar`` argument or the ``GAMUT_JAR`` environment variable. Download -GAMUT from http://gamut.stanford.edu/. +GAMUT requires Java (install from `java.com `_) and +``gamut.jar`` to be installed. Provide the path to ``gamut.jar`` via the ``gamut_jar`` +argument or the ``GAMUT_JAR`` environment variable. Download GAMUT from +http://gamut.stanford.edu/. diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 9ac6aefd4..b5fc13c21 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -350,5 +350,5 @@ Catalog of games load load_openspiel - load_gamut + generate_gamut games diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 9fda6ad83..a634c3dc0 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -99,7 +99,7 @@ def load_openspiel(game_name: str, params: dict | None = None) -> gbt.Game: ) -def load_gamut( +def generate_gamut( game_class: str, params: dict | None = None, gamut_jar: Path | str | None = None, diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 3470da94e..c32f7a0e2 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -329,17 +329,17 @@ def _run(cmd, **kwargs): return _run -def test_load_gamut_success(monkeypatch, tmp_path): - """Happy path: GAMUT writes an NFG file and load_gamut returns a Game.""" +def test_generate_gamut_success(monkeypatch, tmp_path): + """Happy path: GAMUT writes an NFG file and generate_gamut returns a Game.""" fake_jar = tmp_path / "gamut.jar" fake_jar.touch() monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") monkeypatch.setattr("pygambit.catalog.subprocess.run", _fake_gamut_run(_MOCK_NFG)) - game = gbt.catalog.load_gamut("RandomGame", gamut_jar=fake_jar) + game = gbt.catalog.generate_gamut("RandomGame", gamut_jar=fake_jar) assert isinstance(game, gbt.Game) -def test_load_gamut_with_params(monkeypatch, tmp_path): +def test_generate_gamut_with_params(monkeypatch, tmp_path): """params dict is translated correctly to GAMUT command-line flags.""" fake_jar = tmp_path / "gamut.jar" fake_jar.touch() @@ -354,7 +354,7 @@ def _run(cmd, **kwargs): monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") monkeypatch.setattr("pygambit.catalog.subprocess.run", _run) - gbt.catalog.load_gamut( + gbt.catalog.generate_gamut( "RandomGame", params={"players": 2, "actions": [3, 3], "normalize": True, "min_payoff": 0}, gamut_jar=fake_jar, @@ -370,29 +370,29 @@ def _run(cmd, **kwargs): assert "-min_payoff" in cmd and cmd[cmd.index("-min_payoff") + 1] == "0" -def test_load_gamut_no_java(monkeypatch, tmp_path): +def test_generate_gamut_no_java(monkeypatch, tmp_path): """Missing java on PATH raises RuntimeError.""" monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: None) with pytest.raises(RuntimeError, match="Java is required"): - gbt.catalog.load_gamut("RandomGame", gamut_jar=tmp_path / "gamut.jar") + gbt.catalog.generate_gamut("RandomGame", gamut_jar=tmp_path / "gamut.jar") -def test_load_gamut_jar_not_found(monkeypatch, tmp_path): +def test_generate_gamut_jar_not_found(monkeypatch, tmp_path): """Non-existent gamut_jar path raises FileNotFoundError.""" monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") with pytest.raises(FileNotFoundError, match="gamut.jar not found at"): - gbt.catalog.load_gamut("RandomGame", gamut_jar=tmp_path / "missing.jar") + gbt.catalog.generate_gamut("RandomGame", gamut_jar=tmp_path / "missing.jar") -def test_load_gamut_no_jar_env(monkeypatch): +def test_generate_gamut_no_jar_env(monkeypatch): """With gamut_jar=None and GAMUT_JAR unset, raises FileNotFoundError.""" monkeypatch.setattr("pygambit.catalog.shutil.which", lambda _: "/usr/bin/java") monkeypatch.delenv("GAMUT_JAR", raising=False) with pytest.raises(FileNotFoundError, match="GAMUT_JAR"): - gbt.catalog.load_gamut("RandomGame") + gbt.catalog.generate_gamut("RandomGame") -def test_load_gamut_gamut_fails(monkeypatch, tmp_path): +def test_generate_gamut_gamut_fails(monkeypatch, tmp_path): """Non-zero returncode from GAMUT raises ValueError naming the game class.""" fake_jar = tmp_path / "gamut.jar" fake_jar.touch() @@ -402,4 +402,4 @@ def test_load_gamut_gamut_fails(monkeypatch, tmp_path): _fake_gamut_run("", returncode=1, stderr="Unknown game class 'Bogus'"), ) with pytest.raises(ValueError, match="Bogus"): - gbt.catalog.load_gamut("Bogus", gamut_jar=fake_jar) + gbt.catalog.generate_gamut("Bogus", gamut_jar=fake_jar) From c6c4e39cbebd472f3b894ca3f2e5b391c5e98754 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 15:01:54 +0100 Subject: [PATCH 05/17] Add gamut tutorial --- doc/catalog.rst | 2 + doc/pygambit.rst | 1 + .../interoperability_tutorials/gamut.ipynb | 497 ++++++++++++++++++ 3 files changed, 500 insertions(+) create mode 100644 doc/tutorials/interoperability_tutorials/gamut.ipynb diff --git a/doc/catalog.rst b/doc/catalog.rst index b28952e2e..32754a4b2 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -60,3 +60,5 @@ GAMUT requires Java (install from `java.com ` ``gamut.jar`` to be installed. Provide the path to ``gamut.jar`` via the ``gamut_jar`` argument or the ``GAMUT_JAR`` environment variable. Download GAMUT from http://gamut.stanford.edu/. + +See the :doc:`GAMUT interoperability tutorial ` for worked examples. diff --git a/doc/pygambit.rst b/doc/pygambit.rst index d2c8006cb..ce251cb5b 100644 --- a/doc/pygambit.rst +++ b/doc/pygambit.rst @@ -56,6 +56,7 @@ These tutorials assume you have read the new user tutorials and are familiar wit :maxdepth: 2 tutorials/interoperability_tutorials/openspiel + tutorials/interoperability_tutorials/gamut API documentation ---------------- diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb new file mode 100644 index 000000000..c153e9cf6 --- /dev/null +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -0,0 +1,497 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "gamut-intro", + "metadata": {}, + "source": [ + "# Generating games with GAMUT\n", + "\n", + "[GAMUT](http://gamut.stanford.edu/) is a suite of parameterised game generators covering a wide range of game families studied in the game theory literature. Written in Java, GAMUT can generate instances of 35 game classes, including random games, coordination games, covariant games, voting games, and many more.\n", + "\n", + "PyGambit's `generate_gamut` function calls GAMUT as an external subprocess and returns the resulting game as a `Game` object, ready for analysis with PyGambit's full suite of tools. Before running this tutorial, you will need Java and `gamut.jar` installed; see the [catalog documentation](../../catalog.html#catalog-gamut) for full installation instructions.\n", + "\n", + "> **Note:** The cell outputs in this notebook were generated locally. To reproduce them, update the `gamut_jar` argument in each cell to the path of your local `gamut.jar`.\n", + "\n", + "This tutorial covers:\n", + "- Generating classic two-player games with no additional parameters\n", + "- Generating parameterised random normal-form games\n", + "- Exploring how the covariance parameter in `CovariantGame` affects equilibrium structure\n", + "- Generating multi-player games\n", + "- Controlling payoff normalisation and obtaining integer payoffs\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gamut-imports", + "metadata": {}, + "outputs": [], + "source": [ + "import pygambit as gbt" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-bos-intro", + "metadata": {}, + "source": [ + "## Classic two-player games\n", + "\n", + "Many of GAMUT's game classes correspond directly to well-known games from the game theory literature and require no additional parameters to generate. Here we generate Battle of the Sexes, a canonical 2×2 coordination game.\n", + "\n", + "In Battle of the Sexes, two players must independently decide whether to go to the Opera or a Football match. Both prefer to attend the same event, but player 1 prefers the Opera while player 2 prefers Football. The game has two pure-strategy Nash equilibria — both go to the Opera, or both go to Football — and one mixed-strategy equilibrium.\n", + "\n", + "We use the `normalize`, `min_payoff`, and `max_payoff` parameters to rescale payoffs to the range [0, 100], making them easier to read:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "gamut-bos-gen", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Battle of the Sexes

\n", + "
Player2
OperaFootball
Player1Opera100.0,44.276217698093040.0,0.0
Football0.0,0.044.27621769809304,100.0
\n" + ], + "text/plain": [ + "Game(title='Battle of the Sexes')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g_bos = gbt.catalog.generate_gamut(\n", + " \"BattleOfTheSexes\",\n", + " params={\"normalize\": True, \"min_payoff\": 0, \"max_payoff\": 100},\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_bos.title = \"Battle of the Sexes\"\n", + "for i, label in enumerate([\"Opera\", \"Football\"]):\n", + " g_bos.players[0].strategies[i].label = label\n", + " g_bos.players[1].strategies[i].label = label\n", + "g_bos\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-bos-arrays-intro", + "metadata": {}, + "source": [ + "We can inspect the payoff matrices directly using `to_arrays`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "gamut-bos-arrays", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player 1 payoffs:\n", + " [[100.0 0.0]\n", + " [0.0 44.27621769809304]]\n", + "\n", + "Player 2 payoffs:\n", + " [[44.27621769809304 0.0]\n", + " [0.0 100.0]]\n" + ] + } + ], + "source": [ + "p1, p2 = g_bos.to_arrays(dtype=float)\n", + "print(\"Player 1 payoffs:\\n\", p1)\n", + "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-bos-eqm-intro", + "metadata": {}, + "source": [ + "Now let's compute all Nash equilibria using the linear complementarity method, which is well-suited to two-player games:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "gamut-bos-eqm", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]],\n", + " [[Rational(1250000000000000, 1803452721226163), Rational(553452721226163, 1803452721226163)], [Rational(553452721226163, 1803452721226163), Rational(1250000000000000, 1803452721226163)]],\n", + " [[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_bos = gbt.nash.lcp_solve(g_bos)\n", + "result_bos.equilibria\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-bos-interp", + "metadata": {}, + "source": [ + "As expected, the solver finds three equilibria: two pure-strategy equilibria (both play Opera, or both play Football) and one mixed-strategy equilibrium in which each player randomises between the two options.\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-random-intro", + "metadata": {}, + "source": [ + "## Parameterised random normal-form games\n", + "\n", + "Most GAMUT game classes accept parameters to control the number of players, the number of actions, and the structure of payoffs. These are passed as a dictionary to the `params` argument; each key maps to a GAMUT command-line flag.\n", + "\n", + "`RandomGame` is the most general generator: payoffs are drawn independently and uniformly at random from a fixed range. It serves as a useful baseline for testing algorithms. The `players` parameter controls the number of players, and `actions` controls the number of actions per player. When `actions` is a list, each element gives the action count for the corresponding player, allowing asymmetric games:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "gamut-random-gen", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Random Game (2 players, 3x4)

\n", + "
Player2
1234
Player11-24.922576315638636,-46.4693271442734131.29134905292773,55.8212224330570215.412579872383247,65.14942284746837-18.498230186173046,49.38832148801541
2-15.604513815064706,21.639090280011786-72.86583393575657,98.13534287102638-60.67300378440537,-82.9440742821497638.1610749100424,-27.91660410384405
3-21.700394801679934,95.96570634744992-10.471762471204386,-51.88216860878658683.02732785577425,48.05319974223067427.372274400224896,-96.1233605871024
\n" + ], + "text/plain": [ + "Game(title='Random Game (2 players, 3x4)')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g_rand = gbt.catalog.generate_gamut(\n", + " \"RandomGame\",\n", + " params={\"players\": 2, \"actions\": [3, 4]},\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_rand.title = \"Random Game (2 players, 3x4)\"\n", + "g_rand\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "gamut-random-arrays", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player 1 payoffs:\n", + " [[-24.922576315638636 31.29134905292773 15.412579872383247\n", + " -18.498230186173046]\n", + " [-15.604513815064706 -72.86583393575657 -60.67300378440537\n", + " 38.1610749100424]\n", + " [-21.700394801679934 -10.471762471204386 83.02732785577425\n", + " 27.372274400224896]]\n", + "\n", + "Player 2 payoffs:\n", + " [[-46.46932714427341 55.82122243305702 65.14942284746837\n", + " 49.38832148801541]\n", + " [21.639090280011786 98.13534287102638 -82.94407428214976\n", + " -27.91660410384405]\n", + " [95.96570634744992 -51.882168608786586 48.053199742230674\n", + " -96.1233605871024]]\n" + ] + } + ], + "source": [ + "p1, p2 = g_rand.to_arrays(dtype=float)\n", + "print(\"Player 1 payoffs:\\n\", p1)\n", + "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "gamut-random-eqm", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[\\frac{95637629221277359420347717551372}{249827048381960319980942015377107},\\frac{38671994530917040822304290079883}{166551365587973546653961343584738},\\frac{192362854728614798654275725411821}{499654096763920639961884030754214}\\right],\\left[\\frac{2555033098519244082676003505069118}{2787217584178145372304577207470169},\\frac{218800002201863770017740795011731}{2787217584178145372304577207470169},\\frac{13384483457037519610832907389320}{2787217584178145372304577207470169},0\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(95637629221277359420347717551372, 249827048381960319980942015377107), Rational(38671994530917040822304290079883, 166551365587973546653961343584738), Rational(192362854728614798654275725411821, 499654096763920639961884030754214)], [Rational(2555033098519244082676003505069118, 2787217584178145372304577207470169), Rational(218800002201863770017740795011731, 2787217584178145372304577207470169), Rational(13384483457037519610832907389320, 2787217584178145372304577207470169), Rational(0, 1)]]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gbt.nash.lcp_solve(g_rand).equilibria[0]\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-covariant-intro", + "metadata": {}, + "source": [ + "## Covariant games\n", + "\n", + "`CovariantGame` generates two-player games in which the degree of alignment between players' interests is controlled by a covariance parameter `r`.\n", + "\n", + "- When `r = 1` the game is a common-payoff game: both players receive identical payoffs.\n", + "- When `r = 0` payoffs are independent — equivalent to `RandomGame`.\n", + "- As `r` approaches `-1` (the minimum for a two-player game) the game approaches a zero-sum game.\n", + "\n", + "Let's compare an equilibrium under high positive covariance (nearly a coordination game) with one under negative covariance (a more adversarial setting):\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "gamut-cov-pos", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[1,0,0\\right],\\left[1,0,0\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g_cov_pos = gbt.catalog.generate_gamut(\n", + " \"CovariantGame\",\n", + " params={\"players\": 2, \"actions\": [3, 3], \"r\": 0.8},\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_cov_pos.title = \"Covariant Game (r=0.8)\"\n", + "eqm_pos = gbt.nash.lcp_solve(g_cov_pos).equilibria[0]\n", + "eqm_pos\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "gamut-cov-neg", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[0,0,1\\right],\\left[0,1,0\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(0, 1), Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1)]]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g_cov_neg = gbt.catalog.generate_gamut(\n", + " \"CovariantGame\",\n", + " params={\"players\": 2, \"actions\": [3, 3], \"r\": -0.5},\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_cov_neg.title = \"Covariant Game (r=-0.5)\"\n", + "eqm_neg = gbt.nash.lcp_solve(g_cov_neg).equilibria[0]\n", + "eqm_neg\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-cov-interp", + "metadata": {}, + "source": [ + "With high positive covariance (`r = 0.8`) the game is close to a coordination game, so an equilibrium in pure or near-pure strategies is typical. With negative covariance (`r = -0.5`) the game is more adversarial, making genuinely mixed equilibria more likely.\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-multi-intro", + "metadata": {}, + "source": [ + "## Multi-player games\n", + "\n", + "GAMUT includes several n-player game families. Here we use `MajorityVoting`, a model of a committee in which each player votes for one of several candidates. The candidate who receives the most votes wins, and each player has a privately known preferred candidate.\n", + "\n", + "For games with more than two players, `gbt.nash.support_enumeration_solve` finds Nash equilibria by exhaustive enumeration over strategy supports:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "gamut-majority-gen", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Majority Voting (3 players, 3 candidates)

\n", + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671
2-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-10.684139280330584,10.214920409422206,89.42468636332671
3-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-15.05689632165172,-91.74803663814394,1.8356695825329865
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-10.684139280330584,10.214920409422206,89.42468636332671
2-77.76224010989428,-10.85216088077641,-96.80140402265067-77.76224010989428,-10.85216088077641,-96.80140402265067-77.76224010989428,-10.85216088077641,-96.80140402265067
3-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-15.05689632165172,-91.74803663814394,1.8356695825329865
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-15.05689632165172,-91.74803663814394,1.8356695825329865
2-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-15.05689632165172,-91.74803663814394,1.8356695825329865
3-15.05689632165172,-91.74803663814394,1.8356695825329865-15.05689632165172,-91.74803663814394,1.8356695825329865-15.05689632165172,-91.74803663814394,1.8356695825329865
\n" + ], + "text/plain": [ + "Game(title='Majority Voting (3 players, 3 candidates)')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g_mv = gbt.catalog.generate_gamut(\n", + " \"MajorityVoting\",\n", + " params={\"players\": 3, \"actions\": 3},\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_mv.title = \"Majority Voting (3 players, 3 candidates)\"\n", + "g_mv\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "gamut-majority-eqm", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "module 'pygambit.nash' has no attribute 'support_enumeration_solve'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m result_mv = gbt.nash.support_enumeration_solve(g_mv)\n\u001b[32m 2\u001b[39m print(f\"Found {len(result_mv.equilibria)} equilibria\")\n\u001b[32m 3\u001b[39m result_mv.equilibria[\u001b[32m0\u001b[39m]\n", + "\u001b[31mAttributeError\u001b[39m: module 'pygambit.nash' has no attribute 'support_enumeration_solve'" + ] + } + ], + "source": [ + "result_mv = gbt.nash.support_enumeration_solve(g_mv)\n", + "print(f\"Found {len(result_mv.equilibria)} equilibria\")\n", + "result_mv.equilibria[0]\n" + ] + }, + { + "cell_type": "markdown", + "id": "gamut-norm-intro", + "metadata": {}, + "source": [ + "## Payoff normalisation and integer payoffs\n", + "\n", + "By default, GAMUT draws payoffs uniformly from the range [-100, 100]. Four global parameters let you rescale and discretise payoffs:\n", + "\n", + "| Parameter | Type | Effect |\n", + "|---|---|---|\n", + "| `normalize` | boolean flag | Enable rescaling to [`min_payoff`, `max_payoff`] |\n", + "| `min_payoff` | number | Lower bound after normalisation |\n", + "| `max_payoff` | number | Upper bound after normalisation |\n", + "| `int_payoffs` | boolean flag | Round all payoffs to integers |\n", + "\n", + "Boolean flags are passed with value `True`; the flag is then included on the GAMUT command line without a value token (e.g. `{\"normalize\": True}` becomes `-normalize`, `{\"int_payoffs\": True}` becomes `-int_payoffs`).\n", + "\n", + "Integer payoffs are useful when working with Gambit's exact rational arithmetic:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "gamut-int-gen", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player 1 payoffs:\n", + " [[Rational(22282, 1) Rational(58087, 1) Rational(52134, 1)]\n", + " [Rational(9437, 1) Rational(0, 1) Rational(56473, 1)]\n", + " [Rational(84052, 1) Rational(22670, 1) Rational(71012, 1)]]\n", + "\n", + "Player 2 payoffs:\n", + " [[Rational(37340, 1) Rational(100000, 1) Rational(34593, 1)]\n", + " [Rational(61289, 1) Rational(60433, 1) Rational(44482, 1)]\n", + " [Rational(41379, 1) Rational(93780, 1) Rational(48727, 1)]]\n" + ] + } + ], + "source": [ + "g_int = gbt.catalog.generate_gamut(\n", + " \"RandomGame\",\n", + " params={\n", + " \"players\": 2,\n", + " \"actions\": 3,\n", + " \"normalize\": True,\n", + " \"min_payoff\": 0,\n", + " \"max_payoff\": 10,\n", + " \"int_payoffs\": True,\n", + " },\n", + " gamut_jar=\"~/Downloads/gamut.jar\",\n", + ")\n", + "g_int.title = \"Random Game (integer payoffs, 0-10)\"\n", + "p1, p2 = g_int.to_arrays()\n", + "print(\"Player 1 payoffs:\\n\", p1)\n", + "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dtpure", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.13" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b0d5edb14d8ba86db27b31300146b13f14bafe21 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 15:08:06 +0100 Subject: [PATCH 06/17] use enumpure_solve --- .../interoperability_tutorials/gamut.ipynb | 111 +++++++++--------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index c153e9cf6..d07407d61 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "gamut-imports", "metadata": {}, "outputs": [], @@ -55,7 +55,7 @@ "data": { "text/html": [ "

Battle of the Sexes

\n", - "
Player2
OperaFootball
Player1Opera100.0,44.276217698093040.0,0.0
Football0.0,0.044.27621769809304,100.0
\n" + "
Player2
OperaFootball
Player1Opera5.9929944932832235,100.00.0,0.0
Football0.0,0.0100.0,5.9929944932832235
\n" ], "text/plain": [ "Game(title='Battle of the Sexes')" @@ -98,12 +98,12 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[100.0 0.0]\n", - " [0.0 44.27621769809304]]\n", + " [[5.9929944932832235 0.0]\n", + " [0.0 100.0]]\n", "\n", "Player 2 payoffs:\n", - " [[44.27621769809304 0.0]\n", - " [0.0 100.0]]\n" + " [[100.0 0.0]\n", + " [0.0 5.9929944932832235]]\n" ] } ], @@ -131,7 +131,7 @@ "data": { "text/plain": [ "[[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]],\n", - " [[Rational(1250000000000000, 1803452721226163), Rational(553452721226163, 1803452721226163)], [Rational(553452721226163, 1803452721226163), Rational(1250000000000000, 1803452721226163)]],\n", + " [[Rational(11985988986566447, 211985988986566447), Rational(200000000000000000, 211985988986566447)], [Rational(200000000000000000, 211985988986566447), Rational(11985988986566447, 211985988986566447)]],\n", " [[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]]" ] }, @@ -167,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "gamut-random-gen", "metadata": {}, "outputs": [ @@ -175,13 +175,13 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
1234
Player11-24.922576315638636,-46.4693271442734131.29134905292773,55.8212224330570215.412579872383247,65.14942284746837-18.498230186173046,49.38832148801541
2-15.604513815064706,21.639090280011786-72.86583393575657,98.13534287102638-60.67300378440537,-82.9440742821497638.1610749100424,-27.91660410384405
3-21.700394801679934,95.96570634744992-10.471762471204386,-51.88216860878658683.02732785577425,48.05319974223067427.372274400224896,-96.1233605871024
\n" + "
Player2
1234
Player117.09879029828295,32.0973485653900927.689537794842863,34.12451016558521-22.938467441167546,10.42607163378146142.53072437064441,27.7180677867141
282.25942400799374,63.65136522074536-69.1686432869949,64.1405952821330396.1110308181407,-69.44692451793762-79.64396641034494,9.130751590062275
3-9.314488467453216,-1.253625072125189-7.739226846459999,64.00556804214489-83.75467277721359,13.17375352000378562.6622977326341,-22.167235616074493
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -198,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "gamut-random-arrays", "metadata": {}, "outputs": [ @@ -207,20 +207,18 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[-24.922576315638636 31.29134905292773 15.412579872383247\n", - " -18.498230186173046]\n", - " [-15.604513815064706 -72.86583393575657 -60.67300378440537\n", - " 38.1610749100424]\n", - " [-21.700394801679934 -10.471762471204386 83.02732785577425\n", - " 27.372274400224896]]\n", + " [[7.09879029828295 27.689537794842863 -22.938467441167546\n", + " 42.53072437064441]\n", + " [82.25942400799374 -69.1686432869949 96.1110308181407 -79.64396641034494]\n", + " [-9.314488467453216 -7.739226846459999 -83.75467277721359\n", + " 62.6622977326341]]\n", "\n", "Player 2 payoffs:\n", - " [[-46.46932714427341 55.82122243305702 65.14942284746837\n", - " 49.38832148801541]\n", - " [21.639090280011786 98.13534287102638 -82.94407428214976\n", - " -27.91660410384405]\n", - " [95.96570634744992 -51.882168608786586 48.053199742230674\n", - " -96.1233605871024]]\n" + " [[32.09734856539009 34.12451016558521 10.426071633781461 27.7180677867141]\n", + " [63.65136522074536 64.14059528213303 -69.44692451793762\n", + " 9.130751590062275]\n", + " [-1.253625072125189 64.00556804214489 13.173753520003785\n", + " -22.167235616074493]]\n" ] } ], @@ -232,20 +230,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "gamut-random-eqm", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{95637629221277359420347717551372}{249827048381960319980942015377107},\\frac{38671994530917040822304290079883}{166551365587973546653961343584738},\\frac{192362854728614798654275725411821}{499654096763920639961884030754214}\\right],\\left[\\frac{2555033098519244082676003505069118}{2787217584178145372304577207470169},\\frac{218800002201863770017740795011731}{2787217584178145372304577207470169},\\frac{13384483457037519610832907389320}{2787217584178145372304577207470169},0\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[0,1,0,0\\right]\\right]$" ], "text/plain": [ - "[[Rational(95637629221277359420347717551372, 249827048381960319980942015377107), Rational(38671994530917040822304290079883, 166551365587973546653961343584738), Rational(192362854728614798654275725411821, 499654096763920639961884030754214)], [Rational(2555033098519244082676003505069118, 2787217584178145372304577207470169), Rational(218800002201863770017740795011731, 2787217584178145372304577207470169), Rational(13384483457037519610832907389320, 2787217584178145372304577207470169), Rational(0, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -272,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "gamut-cov-pos", "metadata": {}, "outputs": [ @@ -285,7 +283,7 @@ "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -303,20 +301,20 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "gamut-cov-neg", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[0,0,1\\right],\\left[0,1,0\\right]\\right]$" + "$\\left[\\left[\\frac{9681608339803400}{125915468067226217},0,\\frac{116233859727422817}{125915468067226217}\\right],\\left[0,\\frac{170889485790150613}{2476491141850744413},\\frac{2305601656060593800}{2476491141850744413}\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1)]]" + "[[Rational(9681608339803400, 125915468067226217), Rational(0, 1), Rational(116233859727422817, 125915468067226217)], [Rational(0, 1), Rational(170889485790150613, 2476491141850744413), Rational(2305601656060593800, 2476491141850744413)]]" ] }, - "execution_count": 10, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -349,12 +347,12 @@ "\n", "GAMUT includes several n-player game families. Here we use `MajorityVoting`, a model of a committee in which each player votes for one of several candidates. The candidate who receives the most votes wins, and each player has a privately known preferred candidate.\n", "\n", - "For games with more than two players, `gbt.nash.support_enumeration_solve` finds Nash equilibria by exhaustive enumeration over strategy supports:\n" + "Voting games naturally lend themselves to pure-strategy analysis — each player simply votes for their preferred candidate. `gbt.nash.enumpure_solve` searches exhaustively for pure-strategy Nash equilibria and works for games with any number of players:\n" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "gamut-majority-gen", "metadata": {}, "outputs": [ @@ -362,13 +360,13 @@ "data": { "text/html": [ "

Majority Voting (3 players, 3 candidates)

\n", - "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671
2-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-10.684139280330584,10.214920409422206,89.42468636332671
3-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-15.05689632165172,-91.74803663814394,1.8356695825329865
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-10.684139280330584,10.214920409422206,89.42468636332671
2-77.76224010989428,-10.85216088077641,-96.80140402265067-77.76224010989428,-10.85216088077641,-96.80140402265067-77.76224010989428,-10.85216088077641,-96.80140402265067
3-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-15.05689632165172,-91.74803663814394,1.8356695825329865
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-10.684139280330584,10.214920409422206,89.42468636332671-10.684139280330584,10.214920409422206,89.42468636332671-15.05689632165172,-91.74803663814394,1.8356695825329865
2-10.684139280330584,10.214920409422206,89.42468636332671-77.76224010989428,-10.85216088077641,-96.80140402265067-15.05689632165172,-91.74803663814394,1.8356695825329865
3-15.05689632165172,-91.74803663814394,1.8356695825329865-15.05689632165172,-91.74803663814394,1.8356695825329865-15.05689632165172,-91.74803663814394,1.8356695825329865
\n" + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.92386198864361
2-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.467188966176366-1.6980980089939663,83.55114791881027,37.92386198864361
3-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.9238619886436185.46621066361092,77.40715476278203,9.648643632069366
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.467188966176366-1.6980980089939663,83.55114791881027,37.92386198864361
281.35705988292753,-54.63128226788303,17.46718896617636681.35705988292753,-54.63128226788303,17.46718896617636681.35705988292753,-54.63128226788303,17.467188966176366
3-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.46718896617636685.46621066361092,77.40715476278203,9.648643632069366
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.9238619886436185.46621066361092,77.40715476278203,9.648643632069366
2-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.46718896617636685.46621066361092,77.40715476278203,9.648643632069366
385.46621066361092,77.40715476278203,9.64864363206936685.46621066361092,77.40715476278203,9.64864363206936685.46621066361092,77.40715476278203,9.648643632069366
\n" ], "text/plain": [ "Game(title='Majority Voting (3 players, 3 candidates)')" ] }, - "execution_count": 11, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -385,25 +383,26 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "gamut-majority-eqm", "metadata": {}, "outputs": [ { - "ename": "AttributeError", - "evalue": "module 'pygambit.nash' has no attribute 'support_enumeration_solve'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m result_mv = gbt.nash.support_enumeration_solve(g_mv)\n\u001b[32m 2\u001b[39m print(f\"Found {len(result_mv.equilibria)} equilibria\")\n\u001b[32m 3\u001b[39m result_mv.equilibria[\u001b[32m0\u001b[39m]\n", - "\u001b[31mAttributeError\u001b[39m: module 'pygambit.nash' has no attribute 'support_enumeration_solve'" - ] + "data": { + "text/latex": [ + "$\\left[\\left[1,0,0\\right],\\left[1,0,0\\right],\\left[1,0,0\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "result_mv = gbt.nash.support_enumeration_solve(g_mv)\n", - "print(f\"Found {len(result_mv.equilibria)} equilibria\")\n", + "result_mv = gbt.nash.enumpure_solve(g_mv)\n", "result_mv.equilibria[0]\n" ] }, @@ -430,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "gamut-int-gen", "metadata": {}, "outputs": [ @@ -439,14 +438,14 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[Rational(22282, 1) Rational(58087, 1) Rational(52134, 1)]\n", - " [Rational(9437, 1) Rational(0, 1) Rational(56473, 1)]\n", - " [Rational(84052, 1) Rational(22670, 1) Rational(71012, 1)]]\n", + " [[Rational(48751, 1) Rational(39877, 1) Rational(15655, 1)]\n", + " [Rational(45311, 1) Rational(82068, 1) Rational(8227, 1)]\n", + " [Rational(56758, 1) Rational(35125, 1) Rational(80353, 1)]]\n", "\n", "Player 2 payoffs:\n", - " [[Rational(37340, 1) Rational(100000, 1) Rational(34593, 1)]\n", - " [Rational(61289, 1) Rational(60433, 1) Rational(44482, 1)]\n", - " [Rational(41379, 1) Rational(93780, 1) Rational(48727, 1)]]\n" + " [[Rational(8674, 1) Rational(0, 1) Rational(100000, 1)]\n", + " [Rational(45750, 1) Rational(15592, 1) Rational(8896, 1)]\n", + " [Rational(24363, 1) Rational(50767, 1) Rational(96708, 1)]]\n" ] } ], From fa1b48c5121e87f8aa9ee1b9ff09a3af0d121d4e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 5 Jun 2026 15:34:48 +0100 Subject: [PATCH 07/17] test: skip GAMUT notebook test --- tests/test_tutorials.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index e15171ebe..6d6a4bb9e 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -53,6 +53,10 @@ def test_execute_notebook(nb_path): if sys.platform == "win32" and "openspiel" in nb_path.name.lower(): pytest.skip("OpenSpiel notebook requires OpenSpiel, which is not available on Windows") + # GAMUT notebook requires Java and gamut.jar; outputs are pre-saved for docs builds + if "gamut" in nb_path.name.lower(): + pytest.skip("GAMUT notebook requires Java and gamut.jar (see catalog documentation)") + nb = nbformat.read(str(nb_path), as_version=4) # Prefer the notebook's kernelspec if provided, otherwise let nbclient pick the default. From 7d51ec499923d45e7cced75510d599cf6ad24b82 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 10:24:43 +0100 Subject: [PATCH 08/17] Set max payoff in battle of the sexes example to 3 (not 100) --- .../interoperability_tutorials/gamut.ipynb | 100 +++++++++--------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index d07407d61..9934936c2 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "gamut-imports", "metadata": {}, "outputs": [], @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "gamut-bos-gen", "metadata": {}, "outputs": [ @@ -55,13 +55,13 @@ "data": { "text/html": [ "

Battle of the Sexes

\n", - "
Player2
OperaFootball
Player1Opera5.9929944932832235,100.00.0,0.0
Football0.0,0.0100.0,5.9929944932832235
\n" + "
Player2
OperaFootball
Player1Opera2.0573508175615163,3.00.0,0.0
Football0.0,0.03.0,2.0573508175615163
\n" ], "text/plain": [ "Game(title='Battle of the Sexes')" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -69,7 +69,7 @@ "source": [ "g_bos = gbt.catalog.generate_gamut(\n", " \"BattleOfTheSexes\",\n", - " params={\"normalize\": True, \"min_payoff\": 0, \"max_payoff\": 100},\n", + " params={\"normalize\": True, \"min_payoff\": 0, \"max_payoff\": 3},\n", " gamut_jar=\"~/Downloads/gamut.jar\",\n", ")\n", "g_bos.title = \"Battle of the Sexes\"\n", @@ -89,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "gamut-bos-arrays", "metadata": {}, "outputs": [ @@ -98,12 +98,12 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[5.9929944932832235 0.0]\n", - " [0.0 100.0]]\n", + " [[2.0573508175615163 0.0]\n", + " [0.0 3.0]]\n", "\n", "Player 2 payoffs:\n", - " [[100.0 0.0]\n", - " [0.0 5.9929944932832235]]\n" + " [[3.0 0.0]\n", + " [0.0 2.0573508175615163]]\n" ] } ], @@ -123,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "gamut-bos-eqm", "metadata": {}, "outputs": [ @@ -131,11 +131,11 @@ "data": { "text/plain": [ "[[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]],\n", - " [[Rational(11985988986566447, 211985988986566447), Rational(200000000000000000, 211985988986566447)], [Rational(200000000000000000, 211985988986566447), Rational(11985988986566447, 211985988986566447)]],\n", + " [[Rational(20573508175615163, 50573508175615163), Rational(30000000000000000, 50573508175615163)], [Rational(30000000000000000, 50573508175615163), Rational(20573508175615163, 50573508175615163)]],\n", " [[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]]" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -167,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "gamut-random-gen", "metadata": {}, "outputs": [ @@ -175,13 +175,13 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
1234
Player117.09879029828295,32.0973485653900927.689537794842863,34.12451016558521-22.938467441167546,10.42607163378146142.53072437064441,27.7180677867141
282.25942400799374,63.65136522074536-69.1686432869949,64.1405952821330396.1110308181407,-69.44692451793762-79.64396641034494,9.130751590062275
3-9.314488467453216,-1.253625072125189-7.739226846459999,64.00556804214489-83.75467277721359,13.17375352000378562.6622977326341,-22.167235616074493
\n" + "
Player2
1234
Player1168.56928371547713,89.95822670719897-13.84888774787018,-92.69098302644319-62.83968060966338,-62.03091918828149446.44486101019001,-99.12120596009844
2-4.10351222302377,17.45182910236431490.97402206856259,-81.0450184503617-21.3974759367011,35.65877857577641-70.82054914510243,-78.92583140647133
397.31718271179199,58.0643065186524789.44237764706168,-3.984756569246201329.371719829107974,-58.027809994946146-79.53021124001789,-82.0236762608804
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -198,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "gamut-random-arrays", "metadata": {}, "outputs": [ @@ -207,18 +207,20 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[7.09879029828295 27.689537794842863 -22.938467441167546\n", - " 42.53072437064441]\n", - " [82.25942400799374 -69.1686432869949 96.1110308181407 -79.64396641034494]\n", - " [-9.314488467453216 -7.739226846459999 -83.75467277721359\n", - " 62.6622977326341]]\n", + " [[68.56928371547713 -13.84888774787018 -62.83968060966338\n", + " 46.44486101019001]\n", + " [-4.10351222302377 90.97402206856259 -21.3974759367011\n", + " -70.82054914510243]\n", + " [97.31718271179199 89.44237764706168 29.371719829107974\n", + " -79.53021124001789]]\n", "\n", "Player 2 payoffs:\n", - " [[32.09734856539009 34.12451016558521 10.426071633781461 27.7180677867141]\n", - " [63.65136522074536 64.14059528213303 -69.44692451793762\n", - " 9.130751590062275]\n", - " [-1.253625072125189 64.00556804214489 13.173753520003785\n", - " -22.167235616074493]]\n" + " [[89.95822670719897 -92.69098302644319 -62.030919188281494\n", + " -99.12120596009844]\n", + " [17.451829102364314 -81.0450184503617 35.65877857577641\n", + " -78.92583140647133]\n", + " [58.06430651865247 -3.9847565692462013 -58.027809994946146\n", + " -82.0236762608804]]\n" ] } ], @@ -230,20 +232,20 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "gamut-random-eqm", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[1,0,0\\right],\\left[0,1,0,0\\right]\\right]$" + "$\\left[\\left[0,0,1\\right],\\left[1,0,0,0\\right]\\right]$" ], "text/plain": [ - "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" + "[[Rational(0, 1), Rational(0, 1), Rational(1, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -270,7 +272,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "gamut-cov-pos", "metadata": {}, "outputs": [ @@ -283,7 +285,7 @@ "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -301,20 +303,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "gamut-cov-neg", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{9681608339803400}{125915468067226217},0,\\frac{116233859727422817}{125915468067226217}\\right],\\left[0,\\frac{170889485790150613}{2476491141850744413},\\frac{2305601656060593800}{2476491141850744413}\\right]\\right]$" + "$\\left[\\left[0,\\frac{16997671701005265}{24287024099647667},\\frac{7289352398642402}{24287024099647667}\\right],\\left[0,\\frac{107697771801459357}{249323213303597507},\\frac{141625441502138150}{249323213303597507}\\right]\\right]$" ], "text/plain": [ - "[[Rational(9681608339803400, 125915468067226217), Rational(0, 1), Rational(116233859727422817, 125915468067226217)], [Rational(0, 1), Rational(170889485790150613, 2476491141850744413), Rational(2305601656060593800, 2476491141850744413)]]" + "[[Rational(0, 1), Rational(16997671701005265, 24287024099647667), Rational(7289352398642402, 24287024099647667)], [Rational(0, 1), Rational(107697771801459357, 249323213303597507), Rational(141625441502138150, 249323213303597507)]]" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -352,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "gamut-majority-gen", "metadata": {}, "outputs": [ @@ -360,13 +362,13 @@ "data": { "text/html": [ "

Majority Voting (3 players, 3 candidates)

\n", - "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.92386198864361
2-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.467188966176366-1.6980980089939663,83.55114791881027,37.92386198864361
3-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.9238619886436185.46621066361092,77.40715476278203,9.648643632069366
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.467188966176366-1.6980980089939663,83.55114791881027,37.92386198864361
281.35705988292753,-54.63128226788303,17.46718896617636681.35705988292753,-54.63128226788303,17.46718896617636681.35705988292753,-54.63128226788303,17.467188966176366
3-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.46718896617636685.46621066361092,77.40715476278203,9.648643632069366
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-1.6980980089939663,83.55114791881027,37.92386198864361-1.6980980089939663,83.55114791881027,37.9238619886436185.46621066361092,77.40715476278203,9.648643632069366
2-1.6980980089939663,83.55114791881027,37.9238619886436181.35705988292753,-54.63128226788303,17.46718896617636685.46621066361092,77.40715476278203,9.648643632069366
385.46621066361092,77.40715476278203,9.64864363206936685.46621066361092,77.40715476278203,9.64864363206936685.46621066361092,77.40715476278203,9.648643632069366
\n" + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.286100644828693
2-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.5733300813682405-83.0567977124171,-13.768973633037334,-12.286100644828693
3-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.28610064482869335.288886375765316,92.71988676487717,-35.71982941826448
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.5733300813682405-83.0567977124171,-13.768973633037334,-12.286100644828693
2-64.01576085834668,-16.419389713268245,-5.5733300813682405-64.01576085834668,-16.419389713268245,-5.5733300813682405-64.01576085834668,-16.419389713268245,-5.5733300813682405
3-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.573330081368240535.288886375765316,92.71988676487717,-35.71982941826448
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.28610064482869335.288886375765316,92.71988676487717,-35.71982941826448
2-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.573330081368240535.288886375765316,92.71988676487717,-35.71982941826448
335.288886375765316,92.71988676487717,-35.7198294182644835.288886375765316,92.71988676487717,-35.7198294182644835.288886375765316,92.71988676487717,-35.71982941826448
\n" ], "text/plain": [ "Game(title='Majority Voting (3 players, 3 candidates)')" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -383,7 +385,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "gamut-majority-eqm", "metadata": {}, "outputs": [ @@ -396,7 +398,7 @@ "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -429,7 +431,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "gamut-int-gen", "metadata": {}, "outputs": [ @@ -438,14 +440,14 @@ "output_type": "stream", "text": [ "Player 1 payoffs:\n", - " [[Rational(48751, 1) Rational(39877, 1) Rational(15655, 1)]\n", - " [Rational(45311, 1) Rational(82068, 1) Rational(8227, 1)]\n", - " [Rational(56758, 1) Rational(35125, 1) Rational(80353, 1)]]\n", + " [[Rational(6655, 1) Rational(40765, 1) Rational(100000, 1)]\n", + " [Rational(12703, 1) Rational(51749, 1) Rational(27475, 1)]\n", + " [Rational(9075, 1) Rational(56649, 1) Rational(6230, 1)]]\n", "\n", "Player 2 payoffs:\n", - " [[Rational(8674, 1) Rational(0, 1) Rational(100000, 1)]\n", - " [Rational(45750, 1) Rational(15592, 1) Rational(8896, 1)]\n", - " [Rational(24363, 1) Rational(50767, 1) Rational(96708, 1)]]\n" + " [[Rational(27894, 1) Rational(20576, 1) Rational(56773, 1)]\n", + " [Rational(0, 1) Rational(64373, 1) Rational(72772, 1)]\n", + " [Rational(353, 1) Rational(28614, 1) Rational(18748, 1)]]\n" ] } ], From d19a37fe3d79f946834be5a7de8878c97e10b4b0 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 10:36:57 +0100 Subject: [PATCH 09/17] remove superfluous lines --- .../interoperability_tutorials/gamut.ipynb | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index 9934936c2..d3b757ac7 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -79,40 +79,6 @@ "g_bos\n" ] }, - { - "cell_type": "markdown", - "id": "gamut-bos-arrays-intro", - "metadata": {}, - "source": [ - "We can inspect the payoff matrices directly using `to_arrays`:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "gamut-bos-arrays", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Player 1 payoffs:\n", - " [[2.0573508175615163 0.0]\n", - " [0.0 3.0]]\n", - "\n", - "Player 2 payoffs:\n", - " [[3.0 0.0]\n", - " [0.0 2.0573508175615163]]\n" - ] - } - ], - "source": [ - "p1, p2 = g_bos.to_arrays(dtype=float)\n", - "print(\"Player 1 payoffs:\\n\", p1)\n", - "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" - ] - }, { "cell_type": "markdown", "id": "gamut-bos-eqm-intro", From bf350381827b59399e2ecf24a837b638c6f65bf6 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 10:41:16 +0100 Subject: [PATCH 10/17] review classic 2p game tutorial section --- .../interoperability_tutorials/gamut.ipynb | 84 +++++++++++++++++-- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index d3b757ac7..000f90554 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -89,26 +89,24 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 14, "id": "gamut-bos-eqm", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]],\n", - " [[Rational(20573508175615163, 50573508175615163), Rational(30000000000000000, 50573508175615163)], [Rational(30000000000000000, 50573508175615163), Rational(20573508175615163, 50573508175615163)]],\n", - " [[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]]" + "3" ] }, - "execution_count": 5, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "result_bos = gbt.nash.lcp_solve(g_bos)\n", - "result_bos.equilibria\n" + "len(result_bos.equilibria)\n" ] }, { @@ -116,7 +114,79 @@ "id": "gamut-bos-interp", "metadata": {}, "source": [ - "As expected, the solver finds three equilibria: two pure-strategy equilibria (both play Opera, or both play Football) and one mixed-strategy equilibrium in which each player randomises between the two options.\n" + "As expected, the solver finds three equilibria: two pure-strategy equilibria (both play Opera, or both play Football) and one mixed-strategy equilibrium in which each player randomises between the two options:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "37294505", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[1,0\\right],\\left[1,0\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_bos.equilibria[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "293a5436", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[\\frac{300000000000000000}{348292711498745977},\\frac{48292711498745977}{348292711498745977}\\right],\\left[\\frac{48292711498745977}{348292711498745977},\\frac{300000000000000000}{348292711498745977}\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(300000000000000000, 348292711498745977), Rational(48292711498745977, 348292711498745977)], [Rational(48292711498745977, 348292711498745977), Rational(300000000000000000, 348292711498745977)]]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_bos.equilibria[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d2e7a8e8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[0,1\\right],\\left[0,1\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_bos.equilibria[2]" ] }, { From 7d0696081943eefde0e6aa08f8d18b1d8aec27ad Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 10:52:28 +0100 Subject: [PATCH 11/17] simplify paramaterised section --- .../interoperability_tutorials/gamut.ipynb | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index 000f90554..da0e62ffb 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -203,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 18, "id": "gamut-random-gen", "metadata": {}, "outputs": [ @@ -211,13 +211,13 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
1234
Player1168.56928371547713,89.95822670719897-13.84888774787018,-92.69098302644319-62.83968060966338,-62.03091918828149446.44486101019001,-99.12120596009844
2-4.10351222302377,17.45182910236431490.97402206856259,-81.0450184503617-21.3974759367011,35.65877857577641-70.82054914510243,-78.92583140647133
397.31718271179199,58.0643065186524789.44237764706168,-3.984756569246201329.371719829107974,-58.027809994946146-79.53021124001789,-82.0236762608804
\n" + "
Player2
123
Player11-75.10322427964049,36.33244826873653-21.47551516851219,-65.18239428602757-30.16959855727623,86.2561928245076
2-0.6696618965899859,18.992742334668236-41.290392171346156,-45.665869724219334-45.737846989851036,-79.06719185411306
336.501233165671806,87.7057708263955146.46627658883497,15.1156306673482-72.8865340990639,-56.89485415905003
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" ] }, - "execution_count": 6, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -225,7 +225,7 @@ "source": [ "g_rand = gbt.catalog.generate_gamut(\n", " \"RandomGame\",\n", - " params={\"players\": 2, \"actions\": [3, 4]},\n", + " params={\"players\": 2, \"actions\": [3, 3]},\n", " gamut_jar=\"~/Downloads/gamut.jar\",\n", ")\n", "g_rand.title = \"Random Game (2 players, 3x4)\"\n", @@ -234,54 +234,20 @@ }, { "cell_type": "code", - "execution_count": 7, - "id": "gamut-random-arrays", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Player 1 payoffs:\n", - " [[68.56928371547713 -13.84888774787018 -62.83968060966338\n", - " 46.44486101019001]\n", - " [-4.10351222302377 90.97402206856259 -21.3974759367011\n", - " -70.82054914510243]\n", - " [97.31718271179199 89.44237764706168 29.371719829107974\n", - " -79.53021124001789]]\n", - "\n", - "Player 2 payoffs:\n", - " [[89.95822670719897 -92.69098302644319 -62.030919188281494\n", - " -99.12120596009844]\n", - " [17.451829102364314 -81.0450184503617 35.65877857577641\n", - " -78.92583140647133]\n", - " [58.06430651865247 -3.9847565692462013 -58.027809994946146\n", - " -82.0236762608804]]\n" - ] - } - ], - "source": [ - "p1, p2 = g_rand.to_arrays(dtype=float)\n", - "print(\"Player 1 payoffs:\\n\", p1)\n", - "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, + "execution_count": 19, "id": "gamut-random-eqm", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[0,0,1\\right],\\left[1,0,0,0\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(0, 1), Rational(1, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1), Rational(0, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 8, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } From 2b4c3a035b0dbada1930f803215e1069d68b7b24 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 11:14:28 +0100 Subject: [PATCH 12/17] reorder notebook --- .../interoperability_tutorials/gamut.ipynb | 165 +++++++----------- 1 file changed, 66 insertions(+), 99 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index da0e62ffb..00b5d2252 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "gamut-imports", "metadata": {}, "outputs": [], @@ -40,14 +40,12 @@ "\n", "Many of GAMUT's game classes correspond directly to well-known games from the game theory literature and require no additional parameters to generate. Here we generate Battle of the Sexes, a canonical 2×2 coordination game.\n", "\n", - "In Battle of the Sexes, two players must independently decide whether to go to the Opera or a Football match. Both prefer to attend the same event, but player 1 prefers the Opera while player 2 prefers Football. The game has two pure-strategy Nash equilibria — both go to the Opera, or both go to Football — and one mixed-strategy equilibrium.\n", - "\n", - "We use the `normalize`, `min_payoff`, and `max_payoff` parameters to rescale payoffs to the range [0, 100], making them easier to read:\n" + "In Battle of the Sexes, two players must independently decide whether to go to the Opera or a Football match. Both prefer to attend the same event, but player 1 prefers the Opera while player 2 prefers Football. The game has two pure-strategy Nash equilibria — both go to the Opera, or both go to Football — and one mixed-strategy equilibrium.\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "gamut-bos-gen", "metadata": {}, "outputs": [ @@ -55,13 +53,13 @@ "data": { "text/html": [ "

Battle of the Sexes

\n", - "
Player2
OperaFootball
Player1Opera2.0573508175615163,3.00.0,0.0
Football0.0,0.03.0,2.0573508175615163
\n" + "
Player2
OperaFootball
Player1Opera3,20,0
Football0,02,3
\n" ], "text/plain": [ "Game(title='Battle of the Sexes')" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -69,7 +67,13 @@ "source": [ "g_bos = gbt.catalog.generate_gamut(\n", " \"BattleOfTheSexes\",\n", - " params={\"normalize\": True, \"min_payoff\": 0, \"max_payoff\": 3},\n", + " params={\n", + " \"int_payoffs\": True,\n", + " \"int_mult\": 1,\n", + " \"normalize\": True,\n", + " \"min_payoff\": 0,\n", + " \"max_payoff\": 3\n", + " },\n", " gamut_jar=\"~/Downloads/gamut.jar\",\n", ")\n", "g_bos.title = \"Battle of the Sexes\"\n", @@ -89,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 3, "id": "gamut-bos-eqm", "metadata": {}, "outputs": [ @@ -99,7 +103,7 @@ "3" ] }, - "execution_count": 14, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -119,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 4, "id": "37294505", "metadata": {}, "outputs": [ @@ -132,7 +136,7 @@ "[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]]" ] }, - "execution_count": 15, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -143,20 +147,20 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 5, "id": "293a5436", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{300000000000000000}{348292711498745977},\\frac{48292711498745977}{348292711498745977}\\right],\\left[\\frac{48292711498745977}{348292711498745977},\\frac{300000000000000000}{348292711498745977}\\right]\\right]$" + "$\\left[\\left[\\frac{3}{5},\\frac{2}{5}\\right],\\left[\\frac{2}{5},\\frac{3}{5}\\right]\\right]$" ], "text/plain": [ - "[[Rational(300000000000000000, 348292711498745977), Rational(48292711498745977, 348292711498745977)], [Rational(48292711498745977, 348292711498745977), Rational(300000000000000000, 348292711498745977)]]" + "[[Rational(3, 5), Rational(2, 5)], [Rational(2, 5), Rational(3, 5)]]" ] }, - "execution_count": 16, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -167,7 +171,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 6, "id": "d2e7a8e8", "metadata": {}, "outputs": [ @@ -180,7 +184,7 @@ "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 17, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -189,6 +193,26 @@ "result_bos.equilibria[2]" ] }, + { + "cell_type": "markdown", + "id": "gamut-norm-intro", + "metadata": {}, + "source": [ + "## Payoff normalisation and integer payoffs\n", + "\n", + "By default, GAMUT draws payoffs uniformly from the range [-100, 100]. These global parameters let you rescale and discretise payoffs:\n", + "\n", + "| Parameter | Type | Effect |\n", + "|---|---|---|\n", + "| `normalize` | boolean flag | Enable rescaling to [`min_payoff`, `max_payoff`] |\n", + "| `min_payoff` | number | Lower bound after normalisation |\n", + "| `max_payoff` | number | Upper bound after normalisation |\n", + "| `int_payoffs` | boolean flag | Round all payoffs to integers |\n", + "| `int_mult` | boolean flag | Multiplier used before rounding when converting from double to integer payoffs |\n", + "\n", + "See the [GAMUT documentation](http://gamut.stanford.edu/) for more info on the available Global parameters.\n" + ] + }, { "cell_type": "markdown", "id": "gamut-random-intro", @@ -203,7 +227,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 7, "id": "gamut-random-gen", "metadata": {}, "outputs": [ @@ -211,13 +235,13 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
123
Player11-75.10322427964049,36.33244826873653-21.47551516851219,-65.18239428602757-30.16959855727623,86.2561928245076
2-0.6696618965899859,18.992742334668236-41.290392171346156,-45.665869724219334-45.737846989851036,-79.06719185411306
336.501233165671806,87.7057708263955146.46627658883497,15.1156306673482-72.8865340990639,-56.89485415905003
\n" + "
Player2
123
Player118.449452336790017,50.65136471841282-92.79927704334212,-3.95570812661334571.7458456145082,-80.2494180658833
267.53537620150769,37.623223613278185-40.498207297081265,-70.79882284532587-91.7579686963881,-19.193210042999027
357.12219926045171,-17.28154567738535371.25832254568826,75.66646363957787-13.542430807813815,17.403017020353644
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" ] }, - "execution_count": 18, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -234,20 +258,20 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 8, "id": "gamut-random-eqm", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" + "$\\left[\\left[0,1,0\\right],\\left[1,0,0\\right]\\right]$" ], "text/plain": [ - "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" + "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 19, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -274,20 +298,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 16, "id": "gamut-cov-pos", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[1,0,0\\right],\\left[1,0,0\\right]\\right]$" + "$\\left[\\left[0,1,0\\right],\\left[0,0,1\\right]\\right]$" ], "text/plain": [ - "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" + "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 9, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -312,10 +336,10 @@ { "data": { "text/latex": [ - "$\\left[\\left[0,\\frac{16997671701005265}{24287024099647667},\\frac{7289352398642402}{24287024099647667}\\right],\\left[0,\\frac{107697771801459357}{249323213303597507},\\frac{141625441502138150}{249323213303597507}\\right]\\right]$" + "$\\left[\\left[0,\\frac{110388509788362827}{2245945629663800777},\\frac{2135557119875437950}{2245945629663800777}\\right],\\left[\\frac{14081135994751979}{17005061127764935},0,\\frac{2923925133012956}{17005061127764935}\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(16997671701005265, 24287024099647667), Rational(7289352398642402, 24287024099647667)], [Rational(0, 1), Rational(107697771801459357, 249323213303597507), Rational(141625441502138150, 249323213303597507)]]" + "[[Rational(0, 1), Rational(110388509788362827, 2245945629663800777), Rational(2135557119875437950, 2245945629663800777)], [Rational(14081135994751979, 17005061127764935), Rational(0, 1), Rational(2923925133012956, 17005061127764935)]]" ] }, "execution_count": 10, @@ -356,7 +380,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 18, "id": "gamut-majority-gen", "metadata": {}, "outputs": [ @@ -364,13 +388,13 @@ "data": { "text/html": [ "

Majority Voting (3 players, 3 candidates)

\n", - "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.286100644828693
2-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.5733300813682405-83.0567977124171,-13.768973633037334,-12.286100644828693
3-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.28610064482869335.288886375765316,92.71988676487717,-35.71982941826448
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.5733300813682405-83.0567977124171,-13.768973633037334,-12.286100644828693
2-64.01576085834668,-16.419389713268245,-5.5733300813682405-64.01576085834668,-16.419389713268245,-5.5733300813682405-64.01576085834668,-16.419389713268245,-5.5733300813682405
3-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.573330081368240535.288886375765316,92.71988676487717,-35.71982941826448
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-83.0567977124171,-13.768973633037334,-12.286100644828693-83.0567977124171,-13.768973633037334,-12.28610064482869335.288886375765316,92.71988676487717,-35.71982941826448
2-83.0567977124171,-13.768973633037334,-12.286100644828693-64.01576085834668,-16.419389713268245,-5.573330081368240535.288886375765316,92.71988676487717,-35.71982941826448
335.288886375765316,92.71988676487717,-35.7198294182644835.288886375765316,92.71988676487717,-35.7198294182644835.288886375765316,92.71988676487717,-35.71982941826448
\n" + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player1163,-66,-4663,-66,-4663,-66,-46
263,-66,-468,36,063,-66,-46
363,-66,-4663,-66,-4684,-28,10
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player1163,-66,-468,36,063,-66,-46
28,36,08,36,08,36,0
363,-66,-468,36,084,-28,10
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player1163,-66,-4663,-66,-4684,-28,10
263,-66,-468,36,084,-28,10
384,-28,1084,-28,1084,-28,10
\n" ], "text/plain": [ "Game(title='Majority Voting (3 players, 3 candidates)')" ] }, - "execution_count": 11, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -378,7 +402,12 @@ "source": [ "g_mv = gbt.catalog.generate_gamut(\n", " \"MajorityVoting\",\n", - " params={\"players\": 3, \"actions\": 3},\n", + " params={\n", + " \"int_payoffs\": True,\n", + " \"int_mult\": 1,\n", + " \"players\": 3,\n", + " \"actions\": 3\n", + " },\n", " gamut_jar=\"~/Downloads/gamut.jar\",\n", ")\n", "g_mv.title = \"Majority Voting (3 players, 3 candidates)\"\n", @@ -387,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, "id": "gamut-majority-eqm", "metadata": {}, "outputs": [ @@ -400,7 +429,7 @@ "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 12, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -409,68 +438,6 @@ "result_mv = gbt.nash.enumpure_solve(g_mv)\n", "result_mv.equilibria[0]\n" ] - }, - { - "cell_type": "markdown", - "id": "gamut-norm-intro", - "metadata": {}, - "source": [ - "## Payoff normalisation and integer payoffs\n", - "\n", - "By default, GAMUT draws payoffs uniformly from the range [-100, 100]. Four global parameters let you rescale and discretise payoffs:\n", - "\n", - "| Parameter | Type | Effect |\n", - "|---|---|---|\n", - "| `normalize` | boolean flag | Enable rescaling to [`min_payoff`, `max_payoff`] |\n", - "| `min_payoff` | number | Lower bound after normalisation |\n", - "| `max_payoff` | number | Upper bound after normalisation |\n", - "| `int_payoffs` | boolean flag | Round all payoffs to integers |\n", - "\n", - "Boolean flags are passed with value `True`; the flag is then included on the GAMUT command line without a value token (e.g. `{\"normalize\": True}` becomes `-normalize`, `{\"int_payoffs\": True}` becomes `-int_payoffs`).\n", - "\n", - "Integer payoffs are useful when working with Gambit's exact rational arithmetic:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "gamut-int-gen", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Player 1 payoffs:\n", - " [[Rational(6655, 1) Rational(40765, 1) Rational(100000, 1)]\n", - " [Rational(12703, 1) Rational(51749, 1) Rational(27475, 1)]\n", - " [Rational(9075, 1) Rational(56649, 1) Rational(6230, 1)]]\n", - "\n", - "Player 2 payoffs:\n", - " [[Rational(27894, 1) Rational(20576, 1) Rational(56773, 1)]\n", - " [Rational(0, 1) Rational(64373, 1) Rational(72772, 1)]\n", - " [Rational(353, 1) Rational(28614, 1) Rational(18748, 1)]]\n" - ] - } - ], - "source": [ - "g_int = gbt.catalog.generate_gamut(\n", - " \"RandomGame\",\n", - " params={\n", - " \"players\": 2,\n", - " \"actions\": 3,\n", - " \"normalize\": True,\n", - " \"min_payoff\": 0,\n", - " \"max_payoff\": 10,\n", - " \"int_payoffs\": True,\n", - " },\n", - " gamut_jar=\"~/Downloads/gamut.jar\",\n", - ")\n", - "g_int.title = \"Random Game (integer payoffs, 0-10)\"\n", - "p1, p2 = g_int.to_arrays()\n", - "print(\"Player 1 payoffs:\\n\", p1)\n", - "print(\"\\nPlayer 2 payoffs:\\n\", p2)\n" - ] } ], "metadata": { From 4bf702e8c6229788ea59f853d261d5b56c8a7a1d Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 11:36:32 +0100 Subject: [PATCH 13/17] feat: add gamut_games function to list available GAMUT game classes with documentation and tests --- doc/catalog.rst | 7 + doc/pygambit.api.rst | 1 + .../interoperability_tutorials/gamut.ipynb | 408 ++++++++++++++++-- src/pygambit/catalog.py | 130 ++++++ tests/test_catalog.py | 25 ++ 5 files changed, 538 insertions(+), 33 deletions(-) diff --git a/doc/catalog.rst b/doc/catalog.rst index 32754a4b2..b316f9d62 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -61,4 +61,11 @@ GAMUT requires Java (install from `java.com ` argument or the ``GAMUT_JAR`` environment variable. Download GAMUT from http://gamut.stanford.edu/. +To list all available GAMUT game classes and their descriptions:: + + pygambit.catalog.gamut_games() + +The returned DataFrame has columns ``Class``, ``Description``, and ``Players`` +(``"2"`` for two-player only, ``"n"`` for n-player). + See the :doc:`GAMUT interoperability tutorial ` for worked examples. diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 542bbdb96..7a2ef2548 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -352,4 +352,5 @@ Catalog of games load load_openspiel generate_gamut + gamut_games games diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index 00b5d2252..4a3764ac3 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -31,6 +31,348 @@ "import pygambit as gbt" ] }, + { + "cell_type": "markdown", + "id": "a08eb52d", + "metadata": {}, + "source": [ + "## Discovering available game classes\n", + "\n", + "`gbt.catalog.gamut_games()` returns a DataFrame listing all 35 GAMUT game classes, their descriptions, and whether they support more than two players. Use this as a quick reference when choosing a game to generate:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1a5dde4f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ClassDescriptionPlayers
0ArmsRaceArms race with cost and demand functions chose...n
1BattleOfTheSexesCoordination game where players prefer to meet...2
2BertrandOligopolyBertrand oligopoly with arbitrary cost and dem...n
3BidirectionalLEGBidirectional local-effect game on a specified...n
4ChickenClassic 2x2 Chicken game.2
5CollaborationGameGame where all players choosing the same actio...n
6CongestionGameCongestion game where payoffs depend on how ma...n
7CoordinationGamePure coordination game rewarding matching acti...n
8CournotDuopolyCournot duopoly with arbitrary cost and invers...2
9CovariantGameGame with payoffs whose cross-player correlati...n
10DispersionGameGame where dispersed action choices are rewarded.n
11GrabTheDollarSimultaneous competition to claim a prize first.2
12GreedyGamePlayers each choose a subset from a set; payof...2
13GuessThirdsAvePlayers guess a number trying to reach 2/3 of ...n
14HawkAndDoveClassic 2x2 Hawk and Dove game.2
15LocationGameHotelling-style two-player location game on a ...2
16MajorityVotingMajority voting game with arbitrary candidate ...n
17MatchingPenniesClassic 2x2 Matching Pennies game.2
18MinimumEffortGamePayoffs depend on the minimum effort exerted a...n
19NPlayerChickenN-player Chicken game with cooperation costs a...n
20NPlayerPrisonersDilemmaN-player Prisoner's Dilemma with parameterised...n
21PolymatrixGamePolymatrix game formed by two-player edge game...n
22PrisonersDilemmaClassic 2x2 Prisoner's Dilemma.2
23RandomCompoundGameCompound game whose payoffs are sums of random...n
24RandomGameGame with payoffs drawn uniformly at random.n
25RandomGraphicalGameRandom graphical game on a specified graph.n
26RandomLEGRandom local-effect game on a specified graph.n
27RandomZeroSumTwo-player zero-sum game with random payoffs.2
28RockPaperScissorsClassic Rock, Paper, Scissors game.2
29ShapleyGameShapley's original game; no stable equilibrium...2
30SimpleInspectionGameInspection game where players choose from diff...2
31TravelersDilemmaPlayers claim a value; the lowest claim wins a...n
32TwoByTwoGame2x2 game of a specified type per Rapoport's cl...2
33UniformLEGLocal-effect game where all edges share the sa...n
34WarOfAttritionPlayers choose concession times; payoffs depen...2
\n", + "
" + ], + "text/plain": [ + " Class \\\n", + "0 ArmsRace \n", + "1 BattleOfTheSexes \n", + "2 BertrandOligopoly \n", + "3 BidirectionalLEG \n", + "4 Chicken \n", + "5 CollaborationGame \n", + "6 CongestionGame \n", + "7 CoordinationGame \n", + "8 CournotDuopoly \n", + "9 CovariantGame \n", + "10 DispersionGame \n", + "11 GrabTheDollar \n", + "12 GreedyGame \n", + "13 GuessThirdsAve \n", + "14 HawkAndDove \n", + "15 LocationGame \n", + "16 MajorityVoting \n", + "17 MatchingPennies \n", + "18 MinimumEffortGame \n", + "19 NPlayerChicken \n", + "20 NPlayerPrisonersDilemma \n", + "21 PolymatrixGame \n", + "22 PrisonersDilemma \n", + "23 RandomCompoundGame \n", + "24 RandomGame \n", + "25 RandomGraphicalGame \n", + "26 RandomLEG \n", + "27 RandomZeroSum \n", + "28 RockPaperScissors \n", + "29 ShapleyGame \n", + "30 SimpleInspectionGame \n", + "31 TravelersDilemma \n", + "32 TwoByTwoGame \n", + "33 UniformLEG \n", + "34 WarOfAttrition \n", + "\n", + " Description Players \n", + "0 Arms race with cost and demand functions chose... n \n", + "1 Coordination game where players prefer to meet... 2 \n", + "2 Bertrand oligopoly with arbitrary cost and dem... n \n", + "3 Bidirectional local-effect game on a specified... n \n", + "4 Classic 2x2 Chicken game. 2 \n", + "5 Game where all players choosing the same actio... n \n", + "6 Congestion game where payoffs depend on how ma... n \n", + "7 Pure coordination game rewarding matching acti... n \n", + "8 Cournot duopoly with arbitrary cost and invers... 2 \n", + "9 Game with payoffs whose cross-player correlati... n \n", + "10 Game where dispersed action choices are rewarded. n \n", + "11 Simultaneous competition to claim a prize first. 2 \n", + "12 Players each choose a subset from a set; payof... 2 \n", + "13 Players guess a number trying to reach 2/3 of ... n \n", + "14 Classic 2x2 Hawk and Dove game. 2 \n", + "15 Hotelling-style two-player location game on a ... 2 \n", + "16 Majority voting game with arbitrary candidate ... n \n", + "17 Classic 2x2 Matching Pennies game. 2 \n", + "18 Payoffs depend on the minimum effort exerted a... n \n", + "19 N-player Chicken game with cooperation costs a... n \n", + "20 N-player Prisoner's Dilemma with parameterised... n \n", + "21 Polymatrix game formed by two-player edge game... n \n", + "22 Classic 2x2 Prisoner's Dilemma. 2 \n", + "23 Compound game whose payoffs are sums of random... n \n", + "24 Game with payoffs drawn uniformly at random. n \n", + "25 Random graphical game on a specified graph. n \n", + "26 Random local-effect game on a specified graph. n \n", + "27 Two-player zero-sum game with random payoffs. 2 \n", + "28 Classic Rock, Paper, Scissors game. 2 \n", + "29 Shapley's original game; no stable equilibrium... 2 \n", + "30 Inspection game where players choose from diff... 2 \n", + "31 Players claim a value; the lowest claim wins a... n \n", + "32 2x2 game of a specified type per Rapoport's cl... 2 \n", + "33 Local-effect game where all edges share the sa... n \n", + "34 Players choose concession times; payoffs depen... 2 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gbt.catalog.gamut_games()\n" + ] + }, { "cell_type": "markdown", "id": "gamut-bos-intro", @@ -45,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "gamut-bos-gen", "metadata": {}, "outputs": [ @@ -53,13 +395,13 @@ "data": { "text/html": [ "

Battle of the Sexes

\n", - "
Player2
OperaFootball
Player1Opera3,20,0
Football0,02,3
\n" + "
Player2
OperaFootball
Player1Opera3,10,0
Football0,01,3
\n" ], "text/plain": [ "Game(title='Battle of the Sexes')" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -93,7 +435,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "gamut-bos-eqm", "metadata": {}, "outputs": [ @@ -103,7 +445,7 @@ "3" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -123,7 +465,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "37294505", "metadata": {}, "outputs": [ @@ -136,7 +478,7 @@ "[[Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1)]]" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -147,20 +489,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "293a5436", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{3}{5},\\frac{2}{5}\\right],\\left[\\frac{2}{5},\\frac{3}{5}\\right]\\right]$" + "$\\left[\\left[\\frac{3}{4},\\frac{1}{4}\\right],\\left[\\frac{1}{4},\\frac{3}{4}\\right]\\right]$" ], "text/plain": [ - "[[Rational(3, 5), Rational(2, 5)], [Rational(2, 5), Rational(3, 5)]]" + "[[Rational(3, 4), Rational(1, 4)], [Rational(1, 4), Rational(3, 4)]]" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -171,7 +513,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "d2e7a8e8", "metadata": {}, "outputs": [ @@ -184,7 +526,7 @@ "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -227,7 +569,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "gamut-random-gen", "metadata": {}, "outputs": [ @@ -235,13 +577,13 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
123
Player118.449452336790017,50.65136471841282-92.79927704334212,-3.95570812661334571.7458456145082,-80.2494180658833
267.53537620150769,37.623223613278185-40.498207297081265,-70.79882284532587-91.7579686963881,-19.193210042999027
357.12219926045171,-17.28154567738535371.25832254568826,75.66646363957787-13.542430807813815,17.403017020353644
\n" + "
Player2
123
Player1181.52088072134615,-35.17404416445873518.054123974916592,-62.32182562824002-19.130253932577503,24.51389181072075
254.45687444223253,-66.563302169704994.20384047994378,64.4469915321249774.42952512403303,-88.45602937275424
3-67.6732731411297,-52.1562159269053657.992830088253015,20.96793066094353752.877540523599805,55.89329119461476
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -258,20 +600,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "gamut-random-eqm", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[0,1,0\\right],\\left[1,0,0\\right]\\right]$" + "$\\left[\\left[0,1,0\\right],\\left[0,1,0\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" + "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1)]]" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -298,20 +640,20 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 10, "id": "gamut-cov-pos", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[0,1,0\\right],\\left[0,0,1\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 16, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -329,20 +671,20 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "gamut-cov-neg", "metadata": {}, "outputs": [ { "data": { "text/latex": [ - "$\\left[\\left[0,\\frac{110388509788362827}{2245945629663800777},\\frac{2135557119875437950}{2245945629663800777}\\right],\\left[\\frac{14081135994751979}{17005061127764935},0,\\frac{2923925133012956}{17005061127764935}\\right]\\right]$" + "$\\left[\\left[\\frac{32296668026643200}{52576527066053797},0,\\frac{20279859039410597}{52576527066053797}\\right],\\left[\\frac{10504218601489033}{63575383858971418},\\frac{53071165257482385}{63575383858971418},0\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(110388509788362827, 2245945629663800777), Rational(2135557119875437950, 2245945629663800777)], [Rational(14081135994751979, 17005061127764935), Rational(0, 1), Rational(2923925133012956, 17005061127764935)]]" + "[[Rational(32296668026643200, 52576527066053797), Rational(0, 1), Rational(20279859039410597, 52576527066053797)], [Rational(10504218601489033, 63575383858971418), Rational(53071165257482385, 63575383858971418), Rational(0, 1)]]" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -380,7 +722,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "id": "gamut-majority-gen", "metadata": {}, "outputs": [ @@ -388,13 +730,13 @@ "data": { "text/html": [ "

Majority Voting (3 players, 3 candidates)

\n", - "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player1163,-66,-4663,-66,-4663,-66,-46
263,-66,-468,36,063,-66,-46
363,-66,-4663,-66,-4684,-28,10
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player1163,-66,-468,36,063,-66,-46
28,36,08,36,08,36,0
363,-66,-468,36,084,-28,10
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player1163,-66,-4663,-66,-4684,-28,10
263,-66,-468,36,084,-28,10
384,-28,1084,-28,1084,-28,10
\n" + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-56,-38,-19-56,-38,-19-56,-38,-19
2-56,-38,-19-1,-16,-22-56,-38,-19
3-56,-38,-19-56,-38,-19-74,-48,53
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-56,-38,-19-1,-16,-22-56,-38,-19
2-1,-16,-22-1,-16,-22-1,-16,-22
3-56,-38,-19-1,-16,-22-74,-48,53
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-56,-38,-19-56,-38,-19-74,-48,53
2-56,-38,-19-1,-16,-22-74,-48,53
3-74,-48,53-74,-48,53-74,-48,53
\n" ], "text/plain": [ "Game(title='Majority Voting (3 players, 3 candidates)')" ] }, - "execution_count": 18, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -416,7 +758,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 13, "id": "gamut-majority-eqm", "metadata": {}, "outputs": [ @@ -429,7 +771,7 @@ "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, - "execution_count": 19, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index a634c3dc0..4bb7e592b 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -195,6 +195,136 @@ def generate_gamut( out_path.unlink(missing_ok=True) +_GAMUT_GAMES = [ + {"Class": "ArmsRace", + "Description": "Arms race with cost and demand functions chosen by players.", + "Players": "n"}, + {"Class": "BattleOfTheSexes", + "Description": "Coordination game where players prefer to meet but disagree on the venue.", + "Players": "2"}, + {"Class": "BertrandOligopoly", + "Description": "Bertrand oligopoly with arbitrary cost and demand functions.", + "Players": "n"}, + {"Class": "BidirectionalLEG", + "Description": "Bidirectional local-effect game on a specified graph.", + "Players": "n"}, + {"Class": "Chicken", + "Description": "Classic 2x2 Chicken game.", + "Players": "2"}, + {"Class": "CollaborationGame", + "Description": "Game where all players choosing the same action yields the highest payoffs.", + "Players": "n"}, + {"Class": "CongestionGame", + "Description": "Congestion game where payoffs depend on how many players share facilities.", + "Players": "n"}, + {"Class": "CoordinationGame", + "Description": "Pure coordination game rewarding matching action choices.", + "Players": "n"}, + {"Class": "CournotDuopoly", + "Description": "Cournot duopoly with arbitrary cost and inverse demand functions.", + "Players": "2"}, + {"Class": "CovariantGame", + "Description": "Game with payoffs whose cross-player correlation is set by a parameter r.", + "Players": "n"}, + {"Class": "DispersionGame", + "Description": "Game where dispersed action choices are rewarded.", + "Players": "n"}, + {"Class": "GrabTheDollar", + "Description": "Simultaneous competition to claim a prize first.", + "Players": "2"}, + {"Class": "GreedyGame", + "Description": "Players each choose a subset from a set; payoffs depend on overlap.", + "Players": "2"}, + {"Class": "GuessThirdsAve", + "Description": "Players guess a number trying to reach 2/3 of the average.", + "Players": "n"}, + {"Class": "HawkAndDove", + "Description": "Classic 2x2 Hawk and Dove game.", + "Players": "2"}, + {"Class": "LocationGame", + "Description": "Hotelling-style two-player location game on a street.", + "Players": "2"}, + {"Class": "MajorityVoting", + "Description": "Majority voting game with arbitrary candidate utilities.", + "Players": "n"}, + {"Class": "MatchingPennies", + "Description": "Classic 2x2 Matching Pennies game.", + "Players": "2"}, + {"Class": "MinimumEffortGame", + "Description": "Payoffs depend on the minimum effort exerted across all players.", + "Players": "n"}, + {"Class": "NPlayerChicken", + "Description": "N-player Chicken game with cooperation costs and collective rewards.", + "Players": "n"}, + {"Class": "NPlayerPrisonersDilemma", + "Description": "N-player Prisoner's Dilemma with parameterised payoff functions.", + "Players": "n"}, + {"Class": "PolymatrixGame", + "Description": "Polymatrix game formed by two-player edge games on a graph.", + "Players": "n"}, + {"Class": "PrisonersDilemma", + "Description": "Classic 2x2 Prisoner's Dilemma.", + "Players": "2"}, + {"Class": "RandomCompoundGame", + "Description": "Compound game whose payoffs are sums of random 2x2 sub-games.", + "Players": "n"}, + {"Class": "RandomGame", + "Description": "Game with payoffs drawn uniformly at random.", + "Players": "n"}, + {"Class": "RandomGraphicalGame", + "Description": "Random graphical game on a specified graph.", + "Players": "n"}, + {"Class": "RandomLEG", + "Description": "Random local-effect game on a specified graph.", + "Players": "n"}, + {"Class": "RandomZeroSum", + "Description": "Two-player zero-sum game with random payoffs.", + "Players": "2"}, + {"Class": "RockPaperScissors", + "Description": "Classic Rock, Paper, Scissors game.", + "Players": "2"}, + {"Class": "ShapleyGame", + "Description": "Shapley's original game; no stable equilibrium under replicator dynamics.", + "Players": "2"}, + {"Class": "SimpleInspectionGame", + "Description": "Inspection game where players choose from different-sized subsets.", + "Players": "2"}, + {"Class": "TravelersDilemma", + "Description": "Players claim a value; the lowest claim " + "wins a reward, others receive a penalty.", + "Players": "n"}, + {"Class": "TwoByTwoGame", + "Description": "2x2 game of a specified type per Rapoport's classification (type in 1-85).", + "Players": "2"}, + {"Class": "UniformLEG", + "Description": "Local-effect game where all edges share the same local-effect function.", + "Players": "n"}, + {"Class": "WarOfAttrition", + "Description": "Players choose concession times; payoffs " + "depend on valuations and decrements.", + "Players": "2"}, +] + + +def gamut_games() -> pd.DataFrame: + """ + Return a DataFrame listing all 35 GAMUT game classes. + + Each row describes one game class that can be passed to + :func:`generate_gamut` as the ``game_class`` argument. + + Returns + ------- + pd.DataFrame + DataFrame with columns: + + - ``"Class"``: the game class name to pass to :func:`generate_gamut` + - ``"Description"``: a short description of the game family + - ``"Players"``: ``"2"`` for two-player only, ``"n"`` for n-player + """ + return pd.DataFrame.from_records(_GAMUT_GAMES, columns=["Class", "Description", "Players"]) + + def load(slug: str) -> gbt.Game: """ Load a game from the package catalog. diff --git a/tests/test_catalog.py b/tests/test_catalog.py index c32f7a0e2..e824f599c 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -403,3 +403,28 @@ def test_generate_gamut_gamut_fails(monkeypatch, tmp_path): ) with pytest.raises(ValueError, match="Bogus"): gbt.catalog.generate_gamut("Bogus", gamut_jar=fake_jar) + + +def test_gamut_games_returns_dataframe(): + assert isinstance(gbt.catalog.gamut_games(), pd.DataFrame) + + +def test_gamut_games_columns(): + assert list(gbt.catalog.gamut_games().columns) == ["Class", "Description", "Players"] + + +def test_gamut_games_count(): + assert len(gbt.catalog.gamut_games()) == 35 + + +def test_gamut_games_known_classes(): + classes = set(gbt.catalog.gamut_games()["Class"]) + for name in [ + "RandomGame", "BattleOfTheSexes", "CovariantGame", + "MajorityVoting", "PrisonersDilemma", + ]: + assert name in classes + + +def test_gamut_games_players_values(): + assert set(gbt.catalog.gamut_games()["Players"]) == {"2", "n"} From 274ccd7f94f59bc92f593440d072d7ae7a58b7bf Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 15 Jun 2026 11:47:21 +0100 Subject: [PATCH 14/17] docs: replace gamut_games documentation with an interactive dropdown --- doc/catalog.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/catalog.rst b/doc/catalog.rst index b316f9d62..0d81287f3 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -61,11 +61,12 @@ GAMUT requires Java (install from `java.com ` argument or the ``GAMUT_JAR`` environment variable. Download GAMUT from http://gamut.stanford.edu/. -To list all available GAMUT game classes and their descriptions:: +.. dropdown:: All 35 GAMUT game classes - pygambit.catalog.gamut_games() + .. jupyter-execute:: + :hide-code: -The returned DataFrame has columns ``Class``, ``Description``, and ``Players`` -(``"2"`` for two-player only, ``"n"`` for n-player). + import pygambit as gbt + gbt.catalog.gamut_games() See the :doc:`GAMUT interoperability tutorial ` for worked examples. From 70da9560500ea3cbc0119e8ff089dc7282a6821c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 17 Jun 2026 15:32:17 +0100 Subject: [PATCH 15/17] refactor: rename load_openspiel to generate_openspiel --- doc/catalog.rst | 8 ++--- doc/pygambit.api.rst | 2 +- .../openspiel.ipynb | 22 ++---------- src/pygambit/catalog.py | 6 ++-- tests/test_catalog.py | 34 +++++++++---------- 5 files changed, 28 insertions(+), 44 deletions(-) diff --git a/doc/catalog.rst b/doc/catalog.rst index 0d81287f3..118a991f5 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -15,13 +15,13 @@ Games can also be generated on the fly from the GAMUT suite; see :ref:`Generatin .. rubric:: Loading OpenSpiel games Games from the `OpenSpiel `_ library -can be loaded using :func:`pygambit.catalog.load_openspiel`: +can be generated using :func:`pygambit.catalog.generate_openspiel`: .. code-block:: python - pygambit.catalog.load_openspiel("matrix_rps") - pygambit.catalog.load_openspiel("tiny_hanabi") - pygambit.catalog.load_openspiel("blotto", params={"players": 2, "coins": 3, "fields": 2}) + pygambit.catalog.generate_openspiel("matrix_rps") + pygambit.catalog.generate_openspiel("tiny_hanabi") + pygambit.catalog.generate_openspiel("blotto", params={"players": 2, "coins": 3, "fields": 2}) The ``params`` argument is forwarded directly to ``pyspiel.load_game``; see the `OpenSpiel game list `_ for diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 7a2ef2548..a318b34f0 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -350,7 +350,7 @@ Catalog of games :toctree: api/ load - load_openspiel + generate_openspiel generate_gamut gamut_games games diff --git a/doc/tutorials/interoperability_tutorials/openspiel.ipynb b/doc/tutorials/interoperability_tutorials/openspiel.ipynb index 74ec53ae6..adbe54a8a 100644 --- a/doc/tutorials/interoperability_tutorials/openspiel.ipynb +++ b/doc/tutorials/interoperability_tutorials/openspiel.ipynb @@ -146,9 +146,7 @@ "cell_type": "markdown", "id": "045cf8dd", "metadata": {}, - "source": [ - "Gambit's catalog module can load games from the OpenSpiel library using `gbt.catalog.load_openspiel`:\n" - ] + "source": "Gambit's catalog module can generate games from the OpenSpiel library using `gbt.catalog.generate_openspiel`:\n" }, { "cell_type": "code", @@ -156,18 +154,7 @@ "id": "b684325e", "metadata": {}, "outputs": [], - "source": [ - "gbt_matrix_rps_game = gbt.catalog.load_openspiel(\"matrix_rps\")\n", - "\n", - "gbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n", - "\n", - "for player in gbt_matrix_rps_game.players:\n", - " player.strategies[0].label = \"Rock\"\n", - " player.strategies[1].label = \"Paper\"\n", - " player.strategies[2].label = \"Scissors\"\n", - "\n", - "gbt_matrix_rps_game" - ] + "source": "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n\ngbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n\nfor player in gbt_matrix_rps_game.players:\n player.strategies[0].label = \"Rock\"\n player.strategies[1].label = \"Paper\"\n player.strategies[2].label = \"Scissors\"\n\ngbt_matrix_rps_game" }, { "cell_type": "markdown", @@ -457,10 +444,7 @@ "id": "1a534e25", "metadata": {}, "outputs": [], - "source": [ - "gbt_hanabi_game = gbt.catalog.load_openspiel(\"tiny_hanabi\")\n", - "eqm = gbt.nash.lcp_solve(gbt_hanabi_game).equilibria[0]" - ] + "source": "gbt_hanabi_game = gbt.catalog.generate_openspiel(\"tiny_hanabi\")\neqm = gbt.nash.lcp_solve(gbt_hanabi_game).equilibria[0]" }, { "cell_type": "code", diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 4bb7e592b..dfbac9775 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -25,9 +25,9 @@ } -def load_openspiel(game_name: str, params: dict | None = None) -> gbt.Game: +def generate_openspiel(game_name: str, params: dict | None = None) -> gbt.Game: """ - Load a game from the OpenSpiel library. + Generate a game using the OpenSpiel library. Parameters ---------- @@ -44,7 +44,7 @@ def load_openspiel(game_name: str, params: dict | None = None) -> gbt.Game: Returns ------- gbt.Game - The loaded game. + The generated game. Raises ------ diff --git a/tests/test_catalog.py b/tests/test_catalog.py index e824f599c..3d854f0e2 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -208,7 +208,7 @@ def _setup_pyspiel_mock( mock_export_fn = MagicMock() mock_game = MagicMock() - # Wire the dynamics attribute so the == comparison in load_openspiel resolves correctly. + # Wire the dynamics attribute so the == comparison in generate_openspiel resolves correctly. # MagicMock attribute access is idempotent: mock_ps.GameType.Dynamics.SEQUENTIAL always # returns the same object, so the equality check passes. if dynamics == "sequential": @@ -244,35 +244,35 @@ def _setup_pyspiel_mock( return mock_ps, mock_export_fn -def test_openspiel_load_efg_success(monkeypatch): +def test_generate_openspiel_efg_success(monkeypatch): """Sequential (extensive-form) game: EFG export is used and returns a valid Game.""" _setup_pyspiel_mock(monkeypatch, dynamics="sequential", efg_str=_MOCK_EFG) - game = gbt.catalog.load_openspiel("tiny_hanabi") + game = gbt.catalog.generate_openspiel("tiny_hanabi") assert isinstance(game, gbt.Game) -def test_openspiel_load_nfg_success(monkeypatch): +def test_generate_openspiel_nfg_success(monkeypatch): """Simultaneous (normal-form) game: NFG export is used and returns a valid Game.""" _setup_pyspiel_mock(monkeypatch, dynamics="simultaneous", nfg_str=_MOCK_NFG) - game = gbt.catalog.load_openspiel("matrix_rps") + game = gbt.catalog.generate_openspiel("matrix_rps") assert isinstance(game, gbt.Game) -def test_openspiel_load_import_error(monkeypatch): +def test_generate_openspiel_import_error(monkeypatch): """Missing open_spiel raises ImportError with a helpful message.""" monkeypatch.setitem(sys.modules, "pyspiel", None) with pytest.raises(ImportError, match="open_spiel"): - gbt.catalog.load_openspiel("matrix_rps") + gbt.catalog.generate_openspiel("matrix_rps") -def test_openspiel_load_game_not_found(monkeypatch): +def test_generate_openspiel_game_not_found(monkeypatch): """pyspiel.load_game errors propagate directly without wrapping.""" _setup_pyspiel_mock(monkeypatch, load_raises=RuntimeError("Unknown game 'bogus_game'")) with pytest.raises(RuntimeError, match="Unknown game"): - gbt.catalog.load_openspiel("bogus_game") + gbt.catalog.generate_openspiel("bogus_game") -def test_openspiel_load_efg_export_failure(monkeypatch): +def test_generate_openspiel_efg_export_failure(monkeypatch): """EFG export failure on a sequential game raises ValueError with format context.""" _setup_pyspiel_mock( monkeypatch, @@ -280,10 +280,10 @@ def test_openspiel_load_efg_export_failure(monkeypatch): efg_raises=RuntimeError("export error"), ) with pytest.raises(ValueError, match="EFG format"): - gbt.catalog.load_openspiel("tiny_hanabi") + gbt.catalog.generate_openspiel("tiny_hanabi") -def test_openspiel_load_nfg_export_failure(monkeypatch): +def test_generate_openspiel_nfg_export_failure(monkeypatch): """NFG export failure on a simultaneous game raises ValueError with format context.""" _setup_pyspiel_mock( monkeypatch, @@ -291,22 +291,22 @@ def test_openspiel_load_nfg_export_failure(monkeypatch): nfg_raises=RuntimeError("export error"), ) with pytest.raises(ValueError, match="NFG format"): - gbt.catalog.load_openspiel("matrix_rps") + gbt.catalog.generate_openspiel("matrix_rps") -def test_openspiel_load_unsupported_dynamics(monkeypatch): +def test_generate_openspiel_unsupported_dynamics(monkeypatch): """A game with unsupported dynamics (e.g. MEAN_FIELD) raises ValueError.""" _setup_pyspiel_mock(monkeypatch, dynamics="other") with pytest.raises(ValueError, match="unsupported dynamics"): - gbt.catalog.load_openspiel("some_mfg_game") + gbt.catalog.generate_openspiel("some_mfg_game") -def test_openspiel_load_with_params(monkeypatch): +def test_generate_openspiel_with_params(monkeypatch): """params dict is forwarded verbatim to pyspiel.load_game.""" mock_ps, _ = _setup_pyspiel_mock( monkeypatch, dynamics="simultaneous", nfg_str=_MOCK_NFG ) - gbt.catalog.load_openspiel("blotto", params={"players": 2, "coins": 3, "fields": 2}) + gbt.catalog.generate_openspiel("blotto", params={"players": 2, "coins": 3, "fields": 2}) mock_ps.load_game.assert_called_once_with("blotto", {"players": 2, "coins": 3, "fields": 2}) From efc30c1bc2611e45f1068f160c39e13474dc909f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 11:12:06 +0100 Subject: [PATCH 16/17] Save OpenSpiel nb with fix --- .../openspiel.ipynb | 1685 +++++++++-------- 1 file changed, 850 insertions(+), 835 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/openspiel.ipynb b/doc/tutorials/interoperability_tutorials/openspiel.ipynb index d62481288..d11dac55b 100644 --- a/doc/tutorials/interoperability_tutorials/openspiel.ipynb +++ b/doc/tutorials/interoperability_tutorials/openspiel.ipynb @@ -1,838 +1,853 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "fcb19ba2", - "metadata": {}, - "source": [ - "# Using Gambit with OpenSpiel\n", - "\n", - "This tutorial demonstrates the interoperability of the Gambit and OpenSpiel Python packages for game-theoretic analysis.\n", - "\n", - "Gambit provides a range of methods to compute exact and close approximations of equilibria for games. OpenSpiel provides a variety of iterative multi-agent learning algorithms, which may or may not converge to equilibria.\n", - "\n", - "Another key distinction is that the PyGambit API allows the user a simple way to define custom games (see tutorials 1-3). This is also possible in OpenSpiel for normal-form games, and you can load `.efg` files created from Gambit for the extensive-form, however some of the key functionality for iterated learning of strategies is only available for games from the built-in library (see the [OpenSpiel documentation](https://openspiel.readthedocs.io/en/latest/games.html)).\n", - "\n", - "This tutorial demonstrates:\n", - "\n", - "1. Transferring examples of normal (strategic) form and extensive-form games between OpenSpiel and Gambit\n", - "2. Simulating evolutionary dynamics of populations of strategies in OpenSpiel for normal-form games\n", - "3. Training agents using self-play of extensive-form games in OpenSpiel to create strategies\n", - "4. Comparing the strategies from OpenSpiel against equilibrium strategies computed with Gambit\n", - "\n", - "Note: The OpenSpiel code was adapted from the introductory tutorial for the OpenSpiel API on colab [here](https://colab.research.google.com/github/deepmind/open_spiel/blob/master/open_spiel/colabs/OpenSpielTutorial.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebb78322", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import pyspiel\n", - "from open_spiel.python import rl_environment\n", - "from open_spiel.python.algorithms import tabular_qlearner\n", - "from open_spiel.python.egt import dynamics\n", - "from open_spiel.python.egt.utils import game_payoffs_array\n", - "\n", - "import pygambit as gbt" - ] - }, - { - "cell_type": "markdown", - "id": "fd324814", - "metadata": {}, - "source": [ - "## OpenSpiel game library\n", - "\n", - "OpenSpiel has a large selection of games available in its [library](https://openspiel.readthedocs.io/en/latest/games.html). Many of these will not be amenable to equilibrium computation with Gambit, due to their size. For the purposes of this tutorial, we'll pick some of the smallest games from the list below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b3eb3671", - "metadata": {}, - "outputs": [], - "source": [ - "print(pyspiel.registered_names())" - ] - }, - { - "cell_type": "markdown", - "id": "e628a86d", - "metadata": {}, - "source": [ - "## Normal-form games from the OpenSpiel library\n", - "\n", - "Let's start with the simple normal-form game of rock-paper-scissors, in which the payoffs can be represented by a 3x3 matrix.\n", - "\n", - "Load matrix rock-paper-scissors from OpenSpiel:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d51af0a", - "metadata": {}, - "outputs": [], - "source": [ - "ops_matrix_rps_game = pyspiel.load_game(\"matrix_rps\")" - ] - }, - { - "cell_type": "markdown", - "id": "fda1204e", - "metadata": {}, - "source": [ - "In order to simulate a playthrough of the game, you can first initialise a game state:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1bcdb97b", - "metadata": {}, - "outputs": [], - "source": [ - "state = ops_matrix_rps_game.new_initial_state()\n", - "state" - ] - }, - { - "cell_type": "markdown", - "id": "eeee015a", - "metadata": {}, - "source": [ - "The possible actions for both players (player 0 and player 1) are Rock, Paper and Scissors, but these are not labelled and must be accessed via integer indices:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "70575dc7", - "metadata": {}, - "outputs": [], - "source": [ - "print(state.legal_actions(0)) # Player 0 (row) actions\n", - "print(state.legal_actions(1)) # Player 1 (column) actions" - ] - }, - { - "cell_type": "markdown", - "id": "fdea7e5b", - "metadata": {}, - "source": [ - "Since Rock-paper-scissors is a 1-step simultaneous-move normal-form game, we'll apply a list of player actions in one step to reach the terminal state.\n", - "\n", - "Let's simulate player 0 playing Rock (0) and player 1 playing Paper (1):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a532321e", - "metadata": {}, - "outputs": [], - "source": [ - "state.apply_actions([0, 1])\n", - "state" - ] - }, - { - "cell_type": "markdown", - "id": "045cf8dd", - "metadata": {}, - "source": "Gambit's catalog module can generate games from the OpenSpiel library using `gbt.catalog.generate_openspiel`:\n" - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b684325e", - "metadata": {}, - "outputs": [], - "source": "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n\ngbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n\nfor player in gbt_matrix_rps_game.players:\n player.strategies[0].label = \"Rock\"\n player.strategies[1].label = \"Paper\"\n player.strategies[2].label = \"Scissors\"\n\ngbt_matrix_rps_game" - }, - { - "cell_type": "markdown", - "id": "6d7da6f3", - "metadata": {}, - "source": [ - "The unique equilibrium mixed strategy profile for both players is to choose rock, paper, and scissors with equal probability:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "707c6c30", - "metadata": {}, - "outputs": [], - "source": [ - "gbt.nash.lcp_solve(gbt_matrix_rps_game).equilibria[0]" - ] - }, - { - "cell_type": "markdown", - "id": "966e7e3f", - "metadata": {}, - "source": [ - "We can use OpenSpiel's dynamics module to demonstrate evolutionary game theory dynamics, or \"replicator dynamics\", which models how a mixed strategy profile evolves over time based on how the strategies (e.g., choice of actions A, B, C with probabilities X, Y, Z) perform against one another.\n", - "\n", - "Let's start with an initial profile that is not at equilibrium, but weighted towards scissors with proportions: 30% Rock, 30% Paper, 40% Scissors:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf1acdeb", - "metadata": {}, - "outputs": [], - "source": [ - "matrix_rps_payoffs = game_payoffs_array(ops_matrix_rps_game)\n", - "dyn = dynamics.SinglePopulationDynamics(matrix_rps_payoffs, dynamics.replicator)\n", - "x = np.array([0.3, 0.3, 0.4])\n", - "dyn(x)" - ] - }, - { - "cell_type": "markdown", - "id": "fa382753", - "metadata": {}, - "source": [ - "`dyn(x)` calculates the rate of change (derivative) for each strategy in the current profile and returns how fast each strategy's frequency is changing.\n", - "\n", - "In replicator dynamics, a pure strategy that performs well against others will increase in frequency, while strategies performing worse will decrease.\n", - "In our rock-paper-scissors example, the performance of each pure strategy (action) depends on the probability it is assigned in the mixed strategy profile. At the start, whilst there are more players choosing scissors as their action, then rock will perform well and increase in frequency (be more likely to get played in subsequent rounds), while paper will perform poorly and decrease in frequency. We can plot how the frequency of each strategy changes over time:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b9a352c5", - "metadata": {}, - "outputs": [], - "source": [ - "def plot_rps_dynamics(proportions, steps=100, alpha=0.1, plot_average_strategy=False):\n", - " x = np.array(proportions)\n", - " rock_proportions = [x[0]]\n", - " paper_proportions = [x[1]]\n", - " scissors_proportions = [x[2]]\n", - " y = []\n", - " for _ in range(steps):\n", - " x += alpha * dyn(x)\n", - " rock_proportions.append(x[0])\n", - " paper_proportions.append(x[1])\n", - " scissors_proportions.append(x[2])\n", - " if plot_average_strategy:\n", - " y.append([np.mean(rock_proportions),\n", - " np.mean(paper_proportions),\n", - " np.mean(scissors_proportions)\n", - " ])\n", - " else:\n", - " y.append(x.copy())\n", - " y = np.array(y)\n", - "\n", - " plt.plot(y[:, 0], label=\"Rock\")\n", - " plt.plot(y[:, 1], label=\"Paper\")\n", - " plt.plot(y[:, 2], label=\"Scissors\")\n", - " plt.xlabel(\"Time step\")\n", - " if plot_average_strategy:\n", - " plt.ylabel(\"Strategy frequency average up to time step\")\n", - " else:\n", - " plt.ylabel(\"Strategy frequency\")\n", - " plt.legend()\n", - " plt.show()\n", - "\n", - "plot_rps_dynamics([0.3, 0.3, 0.4])" - ] - }, - { - "cell_type": "markdown", - "id": "8569aef4", - "metadata": {}, - "source": [ - "Through the dynamics, we can see that the population proportions oscillate around the equilibrium point (1/3, 1/3, 1/3) without converging to it, because the best strategy depends on the likelihood of the opponents' actions, as defined by the current action probabilities.\n", - "\n", - "However, if we start with the initial population already at the equilibrium mixed strategy profile computed by Gambit (each action is chosen exactly 1/3 of the time), the strategy frequencies will remain constant over time (at the equilibrium point):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "86c6aa52", - "metadata": {}, - "outputs": [], - "source": [ - "plot_rps_dynamics([1/3, 1/3, 1/3])" - ] - }, - { - "cell_type": "markdown", - "id": "a1f6662e", - "metadata": {}, - "source": [ - "When starting from an unbalanced initial mixed strategy profile, the strategy frequencies will oscillate around the equilibrium point without converging to it. However, if we plot the average strategy frequencies over time, we can see that this begins to converge to the equilibrium point:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "189f898f", - "metadata": {}, - "outputs": [], - "source": [ - "plot_rps_dynamics([0.3, 0.3, 0.4], plot_average_strategy=True)" - ] - }, - { - "cell_type": "markdown", - "id": "078a21e0", - "metadata": {}, - "source": [ - "## Normal-form games created with Gambit\n", - "\n", - "You can also set up a normal-form game in Gambit and export it to OpenSpiel. Here we demonstrate this with the simple Prisoner's Dilemma game:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cdd0bfe0", - "metadata": {}, - "outputs": [], - "source": [ - "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", - "player2_payoffs = np.transpose(player1_payoffs)\n", - "\n", - "gbt_prisoners_dilemma_game = gbt.Game.from_arrays(\n", - " player1_payoffs,\n", - " player2_payoffs,\n", - " title=\"Prisoner's Dilemma\"\n", - ")\n", - "gbt_prisoners_dilemma_game" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d42e6545", - "metadata": {}, - "outputs": [], - "source": [ - "gbt.nash.lcp_solve(gbt_prisoners_dilemma_game).equilibria[0]" - ] - }, - { - "cell_type": "markdown", - "id": "15dd432d", - "metadata": {}, - "source": [ - "As expected, Gambit computes the unique equilibrium strategy for both players as choosing cooperate with probability 0 and defect with probability 1.\n", - "\n", - "To re-create the game in OpenSpiel we extract the player payoffs to NumPy arrays, which are then used to create a matrix game in OpenSpiel:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcd42af0", - "metadata": {}, - "outputs": [], - "source": [ - "p1_payoffs, p2_payoffs = gbt_prisoners_dilemma_game.to_arrays(dtype=float)\n", - "p1, p2 = gbt_prisoners_dilemma_game.players\n", - "ops_prisoners_dilemma_game = pyspiel.create_matrix_game(\n", - " gbt_prisoners_dilemma_game.title,\n", - " \"Classic Prisoner's Dilemma\", # description\n", - " [strategy.label for strategy in p1.strategies],\n", - " [strategy.label for strategy in p2.strategies],\n", - " p1_payoffs,\n", - " p2_payoffs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "625a35a4", - "metadata": {}, - "source": [ - "Like rock-paper-scissors, the Prisoner's Dilemma is a 1-step simultaneous-move normal-form game; we'll apply a list of player actions in one step to reach the terminal state. Let's have both player choose to defect (1):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7ce6f2e2", - "metadata": {}, - "outputs": [], - "source": [ - "state = ops_prisoners_dilemma_game.new_initial_state()\n", - "state.apply_actions([1, 1])\n", - "state" - ] - }, - { - "cell_type": "markdown", - "id": "1fea0224", - "metadata": {}, - "source": [ - "Unlike in rock-paper-scissors, the Prisoner's Dilemma has a dominant strategy equilibrium, in which both players defect.\n", - "Using evolutionary dynamics, we can see that a population starting with a mix of cooperators and defectors will evolve towards all defectors over time:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d1495c7c", - "metadata": {}, - "outputs": [], - "source": [ - "matrix_pd_payoffs = game_payoffs_array(ops_prisoners_dilemma_game)\n", - "pd_dyn = dynamics.SinglePopulationDynamics(matrix_pd_payoffs, dynamics.replicator)\n", - "\n", - "def plot_pd_dynamics(proportions, steps=100, alpha=0.1):\n", - " x = np.array(proportions)\n", - " y = []\n", - " for _ in range(steps):\n", - " x += alpha * pd_dyn(x)\n", - " y.append(x.copy())\n", - " y = np.array(y)\n", - " plt.plot(y[:, 0], label=\"Cooperate\")\n", - " plt.plot(y[:, 1], label=\"Defect\")\n", - " plt.xlabel(\"Time step\")\n", - " plt.ylabel(\"Frequency\")\n", - " plt.legend()\n", - " plt.show()\n", - "\n", - "plot_pd_dynamics([0.8, 0.2])" - ] - }, - { - "cell_type": "markdown", - "id": "9926fb07", - "metadata": {}, - "source": [ - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "b12f6330", - "metadata": {}, - "source": [ - "## Extensive-form games from the OpenSpiel library\n", - "\n", - "We can also load extensive-form games via Gambit's catalog module.\n", - "Here we demonstrate this with **Tiny Hanabi**, from the OpenSpiel [game library](https://openspiel.readthedocs.io/en/latest/games.html):\n" - ] - }, - { - "cell_type": "markdown", - "id": "fa354c9f", - "metadata": {}, - "source": [ - "We can then compute equilibria strategies for the players as usual:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a534e25", - "metadata": {}, - "outputs": [], - "source": "gbt_hanabi_game = gbt.catalog.generate_openspiel(\"tiny_hanabi\")\neqm = gbt.nash.lcp_solve(gbt_hanabi_game).equilibria[0]" - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b913fc7a", - "metadata": {}, - "outputs": [], - "source": [ - "from draw_tree import draw_tree\n", - "\n", - "draw_tree(\n", - " gbt_hanabi_game,\n", - " color_scheme=\"gambit\",\n", - " edge_thickness=2,\n", - " action_label_position=0.8,\n", - " shared_terminal_depth=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "cdfe924e", - "metadata": {}, - "source": [ - "We can look at player 0's equilibrium strategy:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ec19b1c", - "metadata": {}, - "outputs": [], - "source": [ - "eqm[\"Pl0\"]" - ] - }, - { - "cell_type": "markdown", - "id": "b54411c0", - "metadata": {}, - "source": [ - "...and use Gambit to explore what those numbers actually mean for player 0:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ae9fc7a7", - "metadata": {}, - "outputs": [], - "source": [ - "for infoset, mixed_action in eqm[\"Pl0\"].mixed_actions():\n", - " print(\n", - " f\"At information set {infoset.number}, \"\n", - " f\"Player 0 plays action 0 with probability: {mixed_action['p0a0']}\"\n", - " f\" and action 1 with probability: {mixed_action['p0a1']}\"\n", - " f\" and action 2 with probability: {mixed_action['p0a2']}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "eac73a24", - "metadata": {}, - "source": [ - "For player 1, we can do the same:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8528e1bd", - "metadata": {}, - "outputs": [], - "source": [ - "eqm[\"Pl1\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2965aed0", - "metadata": {}, - "outputs": [], - "source": [ - "for infoset, mixed_action in eqm[\"Pl1\"].mixed_actions():\n", - " print(\n", - " f\"At information set {infoset.number}, \"\n", - " f\"Player 1 plays action 0 with probability: {mixed_action['p1a0']}\"\n", - " f\" and action 1 with probability: {mixed_action['p1a1']}\"\n", - " f\" and action 2 with probability: {mixed_action['p1a2']}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "d628c0d5", - "metadata": {}, - "source": [ - "Let's now train 2 agents using independent Q-learning on Tiny Hanabi, and play them against eachother.\n", - "\n", - "We can compare the learned strategies played to the equilibrium strategies computed by Gambit.\n", - "\n", - "First let's open the RL environment for Tiny Hanabi and create the agents, one for each player (2 players in this case):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4e72c924", - "metadata": {}, - "outputs": [], - "source": [ - "# Create the environment\n", - "env = rl_environment.Environment(\"tiny_hanabi\")\n", - "num_players = env.num_players\n", - "num_actions = env.action_spec()[\"num_actions\"]\n", - "\n", - "# Create the agents\n", - "agents = [\n", - " tabular_qlearner.QLearner(player_id=idx, num_actions=num_actions)\n", - " for idx in range(num_players)\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "4bf9eea4", - "metadata": {}, - "source": [ - "Now we can train the Q-learning agents in self-play:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53547263", - "metadata": {}, - "outputs": [], - "source": [ - "for cur_episode in range(30000):\n", - " if cur_episode % 10000 == 0:\n", - " print(f\"Episodes: {cur_episode}\")\n", - "\n", - " time_step = env.reset()\n", - " while not time_step.last():\n", - " player_id = time_step.observations[\"current_player\"]\n", - " agent_output = agents[player_id].step(time_step)\n", - " time_step = env.step([agent_output.action])\n", - "\n", - " # Episode is over, step all agents with final info state.\n", - " for agent in agents:\n", - " agent.step(time_step)\n", - "\n", - "print(f\"Episodes: {cur_episode+1}\")" - ] - }, - { - "cell_type": "markdown", - "id": "75cddd36", - "metadata": {}, - "source": [ - "Let's check out the strategies our agents have learned by playing them against eachother again, this time in evaluation mode (setting `is_evaluation=True`):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d71bc733", - "metadata": {}, - "outputs": [], - "source": [ - "time_step = env.reset()\n", - "\n", - "while not time_step.last():\n", - " print(\"\")\n", - " print(env.get_state)\n", - "\n", - " player_id = time_step.observations[\"current_player\"]\n", - " agent_output = agents[player_id].step(time_step, is_evaluation=True)\n", - " print(f\"Agent {player_id} chooses {env.get_state.action_to_string(agent_output.action)}\")\n", - " time_step = env.step([agent_output.action])\n", - "\n", - "print(\"\")\n", - "print(env.get_state)\n", - "print(f\"Rewards: {time_step.rewards}\")" - ] - }, - { - "cell_type": "markdown", - "id": "f1e9b174", - "metadata": {}, - "source": [ - "Are the learned strategies chosen by p0 and p1 consistent with an equilibrium computed by Gambit?\n", - "\n", - "When I ran the above I got the final game state `p0:d0 p1:d0 p0:a2 p1:a0` with payoffs `[10.0, 10.0]`. This is consistent with the equilibrium computed by Gambit:\n", - "- The node `p0:d0 p1:d0` is part of player 0's information set 0.\n", - "- p0 picks a2 which matches the first equilibrium strategy in `eqm['Pl0']` where action `p0a2` is played with probability 1.0.\n", - "- This puts player 1 in their information set 2, and player 1 picks action 0, which is consistent with `eqm['Pl1']` where action `p1a0` is played with probability 1.0." - ] - }, - { - "cell_type": "markdown", - "id": "6f356383", - "metadata": {}, - "source": [ - "## Extensive-form games created with Gambit\n", - "\n", - "It's also possible to create an extensive-form game in Gambit and export it to OpenSpiel. Here we demonstrate this with the one-card poker game introduced in tutorial 3:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77dc34c8", - "metadata": {}, - "outputs": [], - "source": [ - "gbt_one_card_poker = gbt.Game.new_tree(\n", - " players=[\"Alice\", \"Bob\"],\n", - " title=\"Stripped-Down Poker: a simple game of one-card poker from Reiley et al (2008).\"\n", - ")\n", - "\n", - "gbt_one_card_poker.append_move(\n", - " gbt_one_card_poker.root,\n", - " player=gbt_one_card_poker.players.chance,\n", - " actions=[\"King\", \"Queen\"] # By default, chance actions have equal probabilities\n", - ")\n", - "\n", - "for node in gbt_one_card_poker.root.children:\n", - " gbt_one_card_poker.append_move(\n", - " node,\n", - " player=\"Alice\",\n", - " actions=[\"Bet\", \"Fold\"]\n", - " )\n", - "\n", - "gbt_one_card_poker.append_move(\n", - " [\n", - " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"],\n", - " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"]\n", - " ],\n", - " player=\"Bob\",\n", - " actions=[\"Call\", \"Fold\"]\n", - ")\n", - "\n", - "win_big = gbt_one_card_poker.add_outcome([2, -2], label=\"Win Big\")\n", - "win = gbt_one_card_poker.add_outcome([1, -1], label=\"Win\")\n", - "lose_big = gbt_one_card_poker.add_outcome([-2, 2], label=\"Lose Big\")\n", - "lose = gbt_one_card_poker.add_outcome([-1, 1], label=\"Lose\")\n", - "\n", - "# Alice folds, Bob wins small\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"King\"].children[\"Fold\"],\n", - " lose\n", - ")\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"Queen\"].children[\"Fold\"],\n", - " lose\n", - ")\n", - "\n", - "# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"],\n", - " lose_big\n", - ")\n", - "\n", - "# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Call\"],\n", - " win_big\n", - ")\n", - "\n", - "# Bob does not call Alice's Bet, Alice wins small\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"],\n", - " win\n", - ")\n", - "gbt_one_card_poker.set_outcome(\n", - " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"],\n", - " win\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ed920d33-b7c6-4cc1-b055-7244a5bf42d8", - "metadata": {}, - "outputs": [], - "source": [ - "draw_tree(gbt_one_card_poker, color_scheme=\"gambit\")" - ] - }, - { - "cell_type": "markdown", - "id": "4f296f44", - "metadata": {}, - "source": [ - "Create the game in OpenSpiel:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "07340e32", - "metadata": {}, - "outputs": [], - "source": [ - "ops_one_card_poker = pyspiel.load_efg_game(gbt_one_card_poker.to_efg())\n", - "ops_one_card_poker" - ] - }, - { - "cell_type": "markdown", - "id": "ef6939f6", - "metadata": {}, - "source": [ - "Games loaded from EFG in OpenSpiel do not take advantage of the full functionality of the package, for example, it is not possible to carry out training with RL algorithms on these games, as in the example above with Tiny Hanabi. The OpenSpiel documentation explains [how to submit new games to the library](https://openspiel.readthedocs.io/en/latest/developer_guide.html#adding-a-game) if you wish to add your own games.\n", - "\n", - "We can however use the state representation to simulate a playthrough of the game:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "97913fe5", - "metadata": {}, - "outputs": [], - "source": [ - "players = {0: \"Alice\", 1: \"Bob\", -1: \"Chance\"}\n", - "\n", - "# Create an initial game state, then play through to completion\n", - "state = ops_one_card_poker.new_initial_state()\n", - "while not state.is_terminal():\n", - "\n", - " # Store legal actions of current player in a dict\n", - " legal_actions = {}\n", - " for action in state.legal_actions():\n", - " legal_actions[action] = state.action_to_string(state.current_player(), action)\n", - "\n", - " # If player is chance, choose an action according to probability\n", - " if state.is_chance_node():\n", - " outcomes_with_probs = state.chance_outcomes()\n", - " action_list, prob_list = zip(*outcomes_with_probs, strict=True)\n", - " action = np.random.choice(action_list, p=prob_list)\n", - " print(\"Dealt card: \", legal_actions[action])\n", - " state.apply_action(action)\n", - "\n", - " # Regular players pick a random legal action.\n", - " else:\n", - " action = np.random.choice(state.legal_actions())\n", - " print(players[state.current_player()], \" action: \", legal_actions[action])\n", - " state.apply_action(action)\n", - " print()\n", - "\n", - "print(\"Alice receives: \", state.player_return(0), \", Bob receives: \", state.player_return(1))" - ] - }, - { - "cell_type": "markdown", - "id": "1bf09576", - "metadata": {}, - "source": [ - "Run the code cell above to simulate different possible playthroughs of the game." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.13" - } + "cells": [ + { + "cell_type": "markdown", + "id": "fcb19ba2", + "metadata": {}, + "source": [ + "# Using Gambit with OpenSpiel\n", + "\n", + "This tutorial demonstrates the interoperability of the Gambit and OpenSpiel Python packages for game-theoretic analysis.\n", + "\n", + "Gambit provides a range of methods to compute exact and close approximations of equilibria for games. OpenSpiel provides a variety of iterative multi-agent learning algorithms, which may or may not converge to equilibria.\n", + "\n", + "Another key distinction is that the PyGambit API allows the user a simple way to define custom games (see tutorials 1-3). This is also possible in OpenSpiel for normal-form games, and you can load `.efg` files created from Gambit for the extensive-form, however some of the key functionality for iterated learning of strategies is only available for games from the built-in library (see the [OpenSpiel documentation](https://openspiel.readthedocs.io/en/latest/games.html)).\n", + "\n", + "This tutorial demonstrates:\n", + "\n", + "1. Transferring examples of normal (strategic) form and extensive-form games between OpenSpiel and Gambit\n", + "2. Simulating evolutionary dynamics of populations of strategies in OpenSpiel for normal-form games\n", + "3. Training agents using self-play of extensive-form games in OpenSpiel to create strategies\n", + "4. Comparing the strategies from OpenSpiel against equilibrium strategies computed with Gambit\n", + "\n", + "Note: The OpenSpiel code was adapted from the introductory tutorial for the OpenSpiel API on colab [here](https://colab.research.google.com/github/deepmind/open_spiel/blob/master/open_spiel/colabs/OpenSpielTutorial.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebb78322", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pyspiel\n", + "from open_spiel.python import rl_environment\n", + "from open_spiel.python.algorithms import tabular_qlearner\n", + "from open_spiel.python.egt import dynamics\n", + "from open_spiel.python.egt.utils import game_payoffs_array\n", + "\n", + "import pygambit as gbt" + ] + }, + { + "cell_type": "markdown", + "id": "fd324814", + "metadata": {}, + "source": [ + "## OpenSpiel game library\n", + "\n", + "OpenSpiel has a large selection of games available in its [library](https://openspiel.readthedocs.io/en/latest/games.html). Many of these will not be amenable to equilibrium computation with Gambit, due to their size. For the purposes of this tutorial, we'll pick some of the smallest games from the list below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3eb3671", + "metadata": {}, + "outputs": [], + "source": [ + "print(pyspiel.registered_names())" + ] + }, + { + "cell_type": "markdown", + "id": "e628a86d", + "metadata": {}, + "source": [ + "## Normal-form games from the OpenSpiel library\n", + "\n", + "Let's start with the simple normal-form game of rock-paper-scissors, in which the payoffs can be represented by a 3x3 matrix.\n", + "\n", + "Load matrix rock-paper-scissors from OpenSpiel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d51af0a", + "metadata": {}, + "outputs": [], + "source": [ + "ops_matrix_rps_game = pyspiel.load_game(\"matrix_rps\")" + ] + }, + { + "cell_type": "markdown", + "id": "fda1204e", + "metadata": {}, + "source": [ + "In order to simulate a playthrough of the game, you can first initialise a game state:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1bcdb97b", + "metadata": {}, + "outputs": [], + "source": [ + "state = ops_matrix_rps_game.new_initial_state()\n", + "state" + ] + }, + { + "cell_type": "markdown", + "id": "eeee015a", + "metadata": {}, + "source": [ + "The possible actions for both players (player 0 and player 1) are Rock, Paper and Scissors, but these are not labelled and must be accessed via integer indices:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70575dc7", + "metadata": {}, + "outputs": [], + "source": [ + "print(state.legal_actions(0)) # Player 0 (row) actions\n", + "print(state.legal_actions(1)) # Player 1 (column) actions" + ] + }, + { + "cell_type": "markdown", + "id": "fdea7e5b", + "metadata": {}, + "source": [ + "Since Rock-paper-scissors is a 1-step simultaneous-move normal-form game, we'll apply a list of player actions in one step to reach the terminal state.\n", + "\n", + "Let's simulate player 0 playing Rock (0) and player 1 playing Paper (1):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a532321e", + "metadata": {}, + "outputs": [], + "source": [ + "state.apply_actions([0, 1])\n", + "state" + ] + }, + { + "cell_type": "markdown", + "id": "045cf8dd", + "metadata": {}, + "source": [ + "Gambit's catalog module can generate games from the OpenSpiel library using `gbt.catalog.generate_openspiel`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b684325e", + "metadata": {}, + "outputs": [], + "source": [ + "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n", + "\n", + "gbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n", + "\n", + "for player in gbt_matrix_rps_game.players:\n", + " for strategy, name in zip(player.strategies, [\"Rock\", \"Paper\", \"Scissors\"], strict=True):\n", + " strategy.label = name\n", + "\n", + "gbt_matrix_rps_game" + ] + }, + { + "cell_type": "markdown", + "id": "6d7da6f3", + "metadata": {}, + "source": [ + "The unique equilibrium mixed strategy profile for both players is to choose rock, paper, and scissors with equal probability:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "707c6c30", + "metadata": {}, + "outputs": [], + "source": [ + "gbt.nash.lcp_solve(gbt_matrix_rps_game).equilibria[0]" + ] + }, + { + "cell_type": "markdown", + "id": "966e7e3f", + "metadata": {}, + "source": [ + "We can use OpenSpiel's dynamics module to demonstrate evolutionary game theory dynamics, or \"replicator dynamics\", which models how a mixed strategy profile evolves over time based on how the strategies (e.g., choice of actions A, B, C with probabilities X, Y, Z) perform against one another.\n", + "\n", + "Let's start with an initial profile that is not at equilibrium, but weighted towards scissors with proportions: 30% Rock, 30% Paper, 40% Scissors:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf1acdeb", + "metadata": {}, + "outputs": [], + "source": [ + "matrix_rps_payoffs = game_payoffs_array(ops_matrix_rps_game)\n", + "dyn = dynamics.SinglePopulationDynamics(matrix_rps_payoffs, dynamics.replicator)\n", + "x = np.array([0.3, 0.3, 0.4])\n", + "dyn(x)" + ] + }, + { + "cell_type": "markdown", + "id": "fa382753", + "metadata": {}, + "source": [ + "`dyn(x)` calculates the rate of change (derivative) for each strategy in the current profile and returns how fast each strategy's frequency is changing.\n", + "\n", + "In replicator dynamics, a pure strategy that performs well against others will increase in frequency, while strategies performing worse will decrease.\n", + "In our rock-paper-scissors example, the performance of each pure strategy (action) depends on the probability it is assigned in the mixed strategy profile. At the start, whilst there are more players choosing scissors as their action, then rock will perform well and increase in frequency (be more likely to get played in subsequent rounds), while paper will perform poorly and decrease in frequency. We can plot how the frequency of each strategy changes over time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9a352c5", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_rps_dynamics(proportions, steps=100, alpha=0.1, plot_average_strategy=False):\n", + " x = np.array(proportions)\n", + " rock_proportions = [x[0]]\n", + " paper_proportions = [x[1]]\n", + " scissors_proportions = [x[2]]\n", + " y = []\n", + " for _ in range(steps):\n", + " x += alpha * dyn(x)\n", + " rock_proportions.append(x[0])\n", + " paper_proportions.append(x[1])\n", + " scissors_proportions.append(x[2])\n", + " if plot_average_strategy:\n", + " y.append([np.mean(rock_proportions),\n", + " np.mean(paper_proportions),\n", + " np.mean(scissors_proportions)\n", + " ])\n", + " else:\n", + " y.append(x.copy())\n", + " y = np.array(y)\n", + "\n", + " plt.plot(y[:, 0], label=\"Rock\")\n", + " plt.plot(y[:, 1], label=\"Paper\")\n", + " plt.plot(y[:, 2], label=\"Scissors\")\n", + " plt.xlabel(\"Time step\")\n", + " if plot_average_strategy:\n", + " plt.ylabel(\"Strategy frequency average up to time step\")\n", + " else:\n", + " plt.ylabel(\"Strategy frequency\")\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "plot_rps_dynamics([0.3, 0.3, 0.4])" + ] + }, + { + "cell_type": "markdown", + "id": "8569aef4", + "metadata": {}, + "source": [ + "Through the dynamics, we can see that the population proportions oscillate around the equilibrium point (1/3, 1/3, 1/3) without converging to it, because the best strategy depends on the likelihood of the opponents' actions, as defined by the current action probabilities.\n", + "\n", + "However, if we start with the initial population already at the equilibrium mixed strategy profile computed by Gambit (each action is chosen exactly 1/3 of the time), the strategy frequencies will remain constant over time (at the equilibrium point):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86c6aa52", + "metadata": {}, + "outputs": [], + "source": [ + "plot_rps_dynamics([1/3, 1/3, 1/3])" + ] + }, + { + "cell_type": "markdown", + "id": "a1f6662e", + "metadata": {}, + "source": [ + "When starting from an unbalanced initial mixed strategy profile, the strategy frequencies will oscillate around the equilibrium point without converging to it. However, if we plot the average strategy frequencies over time, we can see that this begins to converge to the equilibrium point:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "189f898f", + "metadata": {}, + "outputs": [], + "source": [ + "plot_rps_dynamics([0.3, 0.3, 0.4], plot_average_strategy=True)" + ] + }, + { + "cell_type": "markdown", + "id": "078a21e0", + "metadata": {}, + "source": [ + "## Normal-form games created with Gambit\n", + "\n", + "You can also set up a normal-form game in Gambit and export it to OpenSpiel. Here we demonstrate this with the simple Prisoner's Dilemma game:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdd0bfe0", + "metadata": {}, + "outputs": [], + "source": [ + "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", + "player2_payoffs = np.transpose(player1_payoffs)\n", + "\n", + "gbt_prisoners_dilemma_game = gbt.Game.from_arrays(\n", + " player1_payoffs,\n", + " player2_payoffs,\n", + " title=\"Prisoner's Dilemma\"\n", + ")\n", + "gbt_prisoners_dilemma_game" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d42e6545", + "metadata": {}, + "outputs": [], + "source": [ + "gbt.nash.lcp_solve(gbt_prisoners_dilemma_game).equilibria[0]" + ] + }, + { + "cell_type": "markdown", + "id": "15dd432d", + "metadata": {}, + "source": [ + "As expected, Gambit computes the unique equilibrium strategy for both players as choosing cooperate with probability 0 and defect with probability 1.\n", + "\n", + "To re-create the game in OpenSpiel we extract the player payoffs to NumPy arrays, which are then used to create a matrix game in OpenSpiel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcd42af0", + "metadata": {}, + "outputs": [], + "source": [ + "p1_payoffs, p2_payoffs = gbt_prisoners_dilemma_game.to_arrays(dtype=float)\n", + "p1, p2 = gbt_prisoners_dilemma_game.players\n", + "ops_prisoners_dilemma_game = pyspiel.create_matrix_game(\n", + " gbt_prisoners_dilemma_game.title,\n", + " \"Classic Prisoner's Dilemma\", # description\n", + " [strategy.label for strategy in p1.strategies],\n", + " [strategy.label for strategy in p2.strategies],\n", + " p1_payoffs,\n", + " p2_payoffs\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "625a35a4", + "metadata": {}, + "source": [ + "Like rock-paper-scissors, the Prisoner's Dilemma is a 1-step simultaneous-move normal-form game; we'll apply a list of player actions in one step to reach the terminal state. Let's have both player choose to defect (1):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ce6f2e2", + "metadata": {}, + "outputs": [], + "source": [ + "state = ops_prisoners_dilemma_game.new_initial_state()\n", + "state.apply_actions([1, 1])\n", + "state" + ] + }, + { + "cell_type": "markdown", + "id": "1fea0224", + "metadata": {}, + "source": [ + "Unlike in rock-paper-scissors, the Prisoner's Dilemma has a dominant strategy equilibrium, in which both players defect.\n", + "Using evolutionary dynamics, we can see that a population starting with a mix of cooperators and defectors will evolve towards all defectors over time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1495c7c", + "metadata": {}, + "outputs": [], + "source": [ + "matrix_pd_payoffs = game_payoffs_array(ops_prisoners_dilemma_game)\n", + "pd_dyn = dynamics.SinglePopulationDynamics(matrix_pd_payoffs, dynamics.replicator)\n", + "\n", + "def plot_pd_dynamics(proportions, steps=100, alpha=0.1):\n", + " x = np.array(proportions)\n", + " y = []\n", + " for _ in range(steps):\n", + " x += alpha * pd_dyn(x)\n", + " y.append(x.copy())\n", + " y = np.array(y)\n", + " plt.plot(y[:, 0], label=\"Cooperate\")\n", + " plt.plot(y[:, 1], label=\"Defect\")\n", + " plt.xlabel(\"Time step\")\n", + " plt.ylabel(\"Frequency\")\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "plot_pd_dynamics([0.8, 0.2])" + ] + }, + { + "cell_type": "markdown", + "id": "9926fb07", + "metadata": {}, + "source": [ + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "b12f6330", + "metadata": {}, + "source": [ + "## Extensive-form games from the OpenSpiel library\n", + "\n", + "We can also load extensive-form games via Gambit's catalog module.\n", + "Here we demonstrate this with **Tiny Hanabi**, from the OpenSpiel [game library](https://openspiel.readthedocs.io/en/latest/games.html):\n" + ] + }, + { + "cell_type": "markdown", + "id": "fa354c9f", + "metadata": {}, + "source": [ + "We can then compute equilibria strategies for the players as usual:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a534e25", + "metadata": {}, + "outputs": [], + "source": [ + "gbt_hanabi_game = gbt.catalog.generate_openspiel(\"tiny_hanabi\")\n", + "eqm = gbt.nash.lcp_solve(gbt_hanabi_game).equilibria[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b913fc7a", + "metadata": {}, + "outputs": [], + "source": [ + "from draw_tree import draw_tree\n", + "\n", + "draw_tree(\n", + " gbt_hanabi_game,\n", + " color_scheme=\"gambit\",\n", + " edge_thickness=2,\n", + " action_label_position=0.8,\n", + " shared_terminal_depth=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cdfe924e", + "metadata": {}, + "source": [ + "We can look at player 0's equilibrium strategy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ec19b1c", + "metadata": {}, + "outputs": [], + "source": [ + "eqm[\"Pl0\"]" + ] + }, + { + "cell_type": "markdown", + "id": "b54411c0", + "metadata": {}, + "source": [ + "...and use Gambit to explore what those numbers actually mean for player 0:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae9fc7a7", + "metadata": {}, + "outputs": [], + "source": [ + "for infoset, mixed_action in eqm[\"Pl0\"].mixed_actions():\n", + " print(\n", + " f\"At information set {infoset.number}, \"\n", + " f\"Player 0 plays action 0 with probability: {mixed_action['p0a0']}\"\n", + " f\" and action 1 with probability: {mixed_action['p0a1']}\"\n", + " f\" and action 2 with probability: {mixed_action['p0a2']}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "eac73a24", + "metadata": {}, + "source": [ + "For player 1, we can do the same:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8528e1bd", + "metadata": {}, + "outputs": [], + "source": [ + "eqm[\"Pl1\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2965aed0", + "metadata": {}, + "outputs": [], + "source": [ + "for infoset, mixed_action in eqm[\"Pl1\"].mixed_actions():\n", + " print(\n", + " f\"At information set {infoset.number}, \"\n", + " f\"Player 1 plays action 0 with probability: {mixed_action['p1a0']}\"\n", + " f\" and action 1 with probability: {mixed_action['p1a1']}\"\n", + " f\" and action 2 with probability: {mixed_action['p1a2']}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "d628c0d5", + "metadata": {}, + "source": [ + "Let's now train 2 agents using independent Q-learning on Tiny Hanabi, and play them against eachother.\n", + "\n", + "We can compare the learned strategies played to the equilibrium strategies computed by Gambit.\n", + "\n", + "First let's open the RL environment for Tiny Hanabi and create the agents, one for each player (2 players in this case):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e72c924", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the environment\n", + "env = rl_environment.Environment(\"tiny_hanabi\")\n", + "num_players = env.num_players\n", + "num_actions = env.action_spec()[\"num_actions\"]\n", + "\n", + "# Create the agents\n", + "agents = [\n", + " tabular_qlearner.QLearner(player_id=idx, num_actions=num_actions)\n", + " for idx in range(num_players)\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "4bf9eea4", + "metadata": {}, + "source": [ + "Now we can train the Q-learning agents in self-play:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53547263", + "metadata": {}, + "outputs": [], + "source": [ + "for cur_episode in range(30000):\n", + " if cur_episode % 10000 == 0:\n", + " print(f\"Episodes: {cur_episode}\")\n", + "\n", + " time_step = env.reset()\n", + " while not time_step.last():\n", + " player_id = time_step.observations[\"current_player\"]\n", + " agent_output = agents[player_id].step(time_step)\n", + " time_step = env.step([agent_output.action])\n", + "\n", + " # Episode is over, step all agents with final info state.\n", + " for agent in agents:\n", + " agent.step(time_step)\n", + "\n", + "print(f\"Episodes: {cur_episode+1}\")" + ] + }, + { + "cell_type": "markdown", + "id": "75cddd36", + "metadata": {}, + "source": [ + "Let's check out the strategies our agents have learned by playing them against eachother again, this time in evaluation mode (setting `is_evaluation=True`):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d71bc733", + "metadata": {}, + "outputs": [], + "source": [ + "time_step = env.reset()\n", + "\n", + "while not time_step.last():\n", + " print(\"\")\n", + " print(env.get_state)\n", + "\n", + " player_id = time_step.observations[\"current_player\"]\n", + " agent_output = agents[player_id].step(time_step, is_evaluation=True)\n", + " print(f\"Agent {player_id} chooses {env.get_state.action_to_string(agent_output.action)}\")\n", + " time_step = env.step([agent_output.action])\n", + "\n", + "print(\"\")\n", + "print(env.get_state)\n", + "print(f\"Rewards: {time_step.rewards}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f1e9b174", + "metadata": {}, + "source": [ + "Are the learned strategies chosen by p0 and p1 consistent with an equilibrium computed by Gambit?\n", + "\n", + "When I ran the above I got the final game state `p0:d0 p1:d0 p0:a2 p1:a0` with payoffs `[10.0, 10.0]`. This is consistent with the equilibrium computed by Gambit:\n", + "- The node `p0:d0 p1:d0` is part of player 0's information set 0.\n", + "- p0 picks a2 which matches the first equilibrium strategy in `eqm['Pl0']` where action `p0a2` is played with probability 1.0.\n", + "- This puts player 1 in their information set 2, and player 1 picks action 0, which is consistent with `eqm['Pl1']` where action `p1a0` is played with probability 1.0." + ] + }, + { + "cell_type": "markdown", + "id": "6f356383", + "metadata": {}, + "source": [ + "## Extensive-form games created with Gambit\n", + "\n", + "It's also possible to create an extensive-form game in Gambit and export it to OpenSpiel. Here we demonstrate this with the one-card poker game introduced in tutorial 3:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77dc34c8", + "metadata": {}, + "outputs": [], + "source": [ + "gbt_one_card_poker = gbt.Game.new_tree(\n", + " players=[\"Alice\", \"Bob\"],\n", + " title=\"Stripped-Down Poker: a simple game of one-card poker from Reiley et al (2008).\"\n", + ")\n", + "\n", + "gbt_one_card_poker.append_move(\n", + " gbt_one_card_poker.root,\n", + " player=gbt_one_card_poker.players.chance,\n", + " actions=[\"King\", \"Queen\"] # By default, chance actions have equal probabilities\n", + ")\n", + "\n", + "for node in gbt_one_card_poker.root.children:\n", + " gbt_one_card_poker.append_move(\n", + " node,\n", + " player=\"Alice\",\n", + " actions=[\"Bet\", \"Fold\"]\n", + " )\n", + "\n", + "gbt_one_card_poker.append_move(\n", + " [\n", + " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"],\n", + " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"]\n", + " ],\n", + " player=\"Bob\",\n", + " actions=[\"Call\", \"Fold\"]\n", + ")\n", + "\n", + "win_big = gbt_one_card_poker.add_outcome([2, -2], label=\"Win Big\")\n", + "win = gbt_one_card_poker.add_outcome([1, -1], label=\"Win\")\n", + "lose_big = gbt_one_card_poker.add_outcome([-2, 2], label=\"Lose Big\")\n", + "lose = gbt_one_card_poker.add_outcome([-1, 1], label=\"Lose\")\n", + "\n", + "# Alice folds, Bob wins small\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"King\"].children[\"Fold\"],\n", + " lose\n", + ")\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"Queen\"].children[\"Fold\"],\n", + " lose\n", + ")\n", + "\n", + "# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"],\n", + " lose_big\n", + ")\n", + "\n", + "# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Call\"],\n", + " win_big\n", + ")\n", + "\n", + "# Bob does not call Alice's Bet, Alice wins small\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"],\n", + " win\n", + ")\n", + "gbt_one_card_poker.set_outcome(\n", + " gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"],\n", + " win\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed920d33-b7c6-4cc1-b055-7244a5bf42d8", + "metadata": {}, + "outputs": [], + "source": [ + "draw_tree(gbt_one_card_poker, color_scheme=\"gambit\")" + ] + }, + { + "cell_type": "markdown", + "id": "4f296f44", + "metadata": {}, + "source": [ + "Create the game in OpenSpiel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07340e32", + "metadata": {}, + "outputs": [], + "source": [ + "ops_one_card_poker = pyspiel.load_efg_game(gbt_one_card_poker.to_efg())\n", + "ops_one_card_poker" + ] + }, + { + "cell_type": "markdown", + "id": "ef6939f6", + "metadata": {}, + "source": [ + "Games loaded from EFG in OpenSpiel do not take advantage of the full functionality of the package, for example, it is not possible to carry out training with RL algorithms on these games, as in the example above with Tiny Hanabi. The OpenSpiel documentation explains [how to submit new games to the library](https://openspiel.readthedocs.io/en/latest/developer_guide.html#adding-a-game) if you wish to add your own games.\n", + "\n", + "We can however use the state representation to simulate a playthrough of the game:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97913fe5", + "metadata": {}, + "outputs": [], + "source": [ + "players = {0: \"Alice\", 1: \"Bob\", -1: \"Chance\"}\n", + "\n", + "# Create an initial game state, then play through to completion\n", + "state = ops_one_card_poker.new_initial_state()\n", + "while not state.is_terminal():\n", + "\n", + " # Store legal actions of current player in a dict\n", + " legal_actions = {}\n", + " for action in state.legal_actions():\n", + " legal_actions[action] = state.action_to_string(state.current_player(), action)\n", + "\n", + " # If player is chance, choose an action according to probability\n", + " if state.is_chance_node():\n", + " outcomes_with_probs = state.chance_outcomes()\n", + " action_list, prob_list = zip(*outcomes_with_probs, strict=True)\n", + " action = np.random.choice(action_list, p=prob_list)\n", + " print(\"Dealt card: \", legal_actions[action])\n", + " state.apply_action(action)\n", + "\n", + " # Regular players pick a random legal action.\n", + " else:\n", + " action = np.random.choice(state.legal_actions())\n", + " print(players[state.current_player()], \" action: \", legal_actions[action])\n", + " state.apply_action(action)\n", + " print()\n", + "\n", + "print(\"Alice receives: \", state.player_return(0), \", Bob receives: \", state.player_return(1))" + ] + }, + { + "cell_type": "markdown", + "id": "1bf09576", + "metadata": {}, + "source": [ + "Run the code cell above to simulate different possible playthroughs of the game." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 }, - "nbformat": 4, - "nbformat_minor": 5 + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } From d270b047fffec55bdae9210ae57c22c53771e93f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 11:13:13 +0100 Subject: [PATCH 17/17] save gamut nb with outputs --- .../interoperability_tutorials/gamut.ipynb | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index 4a3764ac3..31fcb60e3 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -395,7 +395,7 @@ "data": { "text/html": [ "

Battle of the Sexes

\n", - "
Player2
OperaFootball
Player1Opera3,10,0
Football0,01,3
\n" + "
Player2
OperaFootball
Player1Opera3,30,0
Football0,03,3
\n" ], "text/plain": [ "Game(title='Battle of the Sexes')" @@ -419,9 +419,9 @@ " gamut_jar=\"~/Downloads/gamut.jar\",\n", ")\n", "g_bos.title = \"Battle of the Sexes\"\n", - "for i, label in enumerate([\"Opera\", \"Football\"]):\n", - " g_bos.players[0].strategies[i].label = label\n", - " g_bos.players[1].strategies[i].label = label\n", + "for player in g_bos.players:\n", + " for strategy, label in zip(player.strategies, [\"Opera\", \"Football\"], strict=True):\n", + " strategy.label = label\n", "g_bos\n" ] }, @@ -496,10 +496,10 @@ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{3}{4},\\frac{1}{4}\\right],\\left[\\frac{1}{4},\\frac{3}{4}\\right]\\right]$" + "$\\left[\\left[\\frac{1}{2},\\frac{1}{2}\\right],\\left[\\frac{1}{2},\\frac{1}{2}\\right]\\right]$" ], "text/plain": [ - "[[Rational(3, 4), Rational(1, 4)], [Rational(1, 4), Rational(3, 4)]]" + "[[Rational(1, 2), Rational(1, 2)], [Rational(1, 2), Rational(1, 2)]]" ] }, "execution_count": 6, @@ -577,7 +577,7 @@ "data": { "text/html": [ "

Random Game (2 players, 3x4)

\n", - "
Player2
123
Player1181.52088072134615,-35.17404416445873518.054123974916592,-62.32182562824002-19.130253932577503,24.51389181072075
254.45687444223253,-66.563302169704994.20384047994378,64.4469915321249774.42952512403303,-88.45602937275424
3-67.6732731411297,-52.1562159269053657.992830088253015,20.96793066094353752.877540523599805,55.89329119461476
\n" + "
Player2
123
Player11-24.90896560296312,-41.46093893377729-97.04833183504611,-23.56380990697073250.0831929786506,26.051862305230316
2-65.96410417734546,44.09018185830413530.77026176277343,-17.52125395472777-10.707266835174579,26.28680487237078
3-65.52630679123462,-87.74228077603547-3.9604024034780565,-61.10491491476051-94.29034279305093,-45.34942611324284
\n" ], "text/plain": [ "Game(title='Random Game (2 players, 3x4)')" @@ -607,10 +607,10 @@ { "data": { "text/latex": [ - "$\\left[\\left[0,1,0\\right],\\left[0,1,0\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" ], "text/plain": [ - "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1), Rational(0, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" ] }, "execution_count": 9, @@ -647,10 +647,10 @@ { "data": { "text/latex": [ - "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[1,0,0\\right]\\right]$" ], "text/plain": [ - "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(1, 1), Rational(0, 1), Rational(0, 1)]]" ] }, "execution_count": 10, @@ -678,10 +678,10 @@ { "data": { "text/latex": [ - "$\\left[\\left[\\frac{32296668026643200}{52576527066053797},0,\\frac{20279859039410597}{52576527066053797}\\right],\\left[\\frac{10504218601489033}{63575383858971418},\\frac{53071165257482385}{63575383858971418},0\\right]\\right]$" + "$\\left[\\left[1,0,0\\right],\\left[0,0,1\\right]\\right]$" ], "text/plain": [ - "[[Rational(32296668026643200, 52576527066053797), Rational(0, 1), Rational(20279859039410597, 52576527066053797)], [Rational(10504218601489033, 63575383858971418), Rational(53071165257482385, 63575383858971418), Rational(0, 1)]]" + "[[Rational(1, 1), Rational(0, 1), Rational(0, 1)], [Rational(0, 1), Rational(0, 1), Rational(1, 1)]]" ] }, "execution_count": 11, @@ -730,7 +730,7 @@ "data": { "text/html": [ "

Majority Voting (3 players, 3 candidates)

\n", - "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player11-56,-38,-19-56,-38,-19-56,-38,-19
2-56,-38,-19-1,-16,-22-56,-38,-19
3-56,-38,-19-56,-38,-19-74,-48,53
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player11-56,-38,-19-1,-16,-22-56,-38,-19
2-1,-16,-22-1,-16,-22-1,-16,-22
3-56,-38,-19-1,-16,-22-74,-48,53
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player11-56,-38,-19-56,-38,-19-74,-48,53
2-56,-38,-19-1,-16,-22-74,-48,53
3-74,-48,53-74,-48,53-74,-48,53
\n" + "
Subtable with strategies:
Player 3 Strategy 1
Player2
123
Player1139,54,9739,54,9739,54,97
239,54,97-42,-25,8739,54,97
339,54,9739,54,9732,-98,-29
Subtable with strategies:
Player 3 Strategy 2
Player2
123
Player1139,54,97-42,-25,8739,54,97
2-42,-25,87-42,-25,87-42,-25,87
339,54,97-42,-25,8732,-98,-29
Subtable with strategies:
Player 3 Strategy 3
Player2
123
Player1139,54,9739,54,9732,-98,-29
239,54,97-42,-25,8732,-98,-29
332,-98,-2932,-98,-2932,-98,-29
\n" ], "text/plain": [ "Game(title='Majority Voting (3 players, 3 candidates)')"