game-resolver-cpp 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/CMakeLists.txt +1 -1
  2. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/PKG-INFO +26 -4
  3. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/README.md +2 -0
  4. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/docs/README_for_pypi.md +25 -3
  5. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/pyproject.toml +1 -1
  6. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/bayesian_nash_equilibrium.py +16 -1
  7. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/src/binding.cpp +51 -3
  8. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/tests/test_compat.py +32 -0
  9. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/version.cpp +1 -1
  10. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/smoke_test.cpp +1 -1
  11. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/.clang-format +0 -0
  12. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/.clang-tidy +0 -0
  13. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/.gitignore +0 -0
  14. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/LICENSE +0 -0
  15. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/Makefile +0 -0
  16. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/benchmarks/CMakeLists.txt +0 -0
  17. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/benchmarks/benchmark.cpp +0 -0
  18. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/cmake/game_resolverConfig.cmake.in +0 -0
  19. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/CMakeLists.txt +0 -0
  20. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/battle_of_sex.cpp +0 -0
  21. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/bayesian_battle_of_sex.cpp +0 -0
  22. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/cournot.cpp +0 -0
  23. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/hawk_dove.cpp +0 -0
  24. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/examples/prisoners_dilemma.cpp +0 -0
  25. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/action.hpp +0 -0
  26. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/bayesian/bayesian_game.hpp +0 -0
  27. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/bayesian/bayesian_nash_equilibrium.hpp +0 -0
  28. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/bayesian/bayesian_player.hpp +0 -0
  29. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/error.hpp +0 -0
  30. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/game.hpp +0 -0
  31. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/game_resolver.hpp +0 -0
  32. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/nash_equilibrium.hpp +0 -0
  33. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/ndarray.hpp +0 -0
  34. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/payoff.hpp +0 -0
  35. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/player.hpp +0 -0
  36. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/solver/nash_solver.hpp +0 -0
  37. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/include/game_resolver/version.hpp +0 -0
  38. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/CMakeLists.txt +0 -0
  39. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/benchmarks/compare_with_python.py +0 -0
  40. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/__init__.py +0 -0
  41. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/bayesian_game.py +0 -0
  42. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/bayesian_player.py +0 -0
  43. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/game.py +0 -0
  44. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/nash_equilibrium.py +0 -0
  45. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/payoff.py +0 -0
  46. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/python/game_resolver_cpp/player.py +0 -0
  47. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/action.cpp +0 -0
  48. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/bayesian/bayesian_nash_equilibrium.cpp +0 -0
  49. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/bayesian/bayesian_player.cpp +0 -0
  50. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/nash_equilibrium.cpp +0 -0
  51. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/payoff.cpp +0 -0
  52. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/src/solver/nash_solver.cpp +0 -0
  53. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/CMakeLists.txt +0 -0
  54. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/action_test.cpp +0 -0
  55. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/bayesian_test.cpp +0 -0
  56. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/example_games.hpp +0 -0
  57. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/battle_of_sex.txt +0 -0
  58. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/bayesian_battle_of_sex.txt +0 -0
  59. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/cournot.txt +0 -0
  60. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/generate.py +0 -0
  61. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/hawk_dove.txt +0 -0
  62. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/prisoners_dilemma.txt +0 -0
  63. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden/verify_exact.py +0 -0
  64. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/golden_test.cpp +0 -0
  65. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/nash_equilibrium_test.cpp +0 -0
  66. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/nash_solver_test.cpp +0 -0
  67. {game_resolver_cpp-0.1.0 → game_resolver_cpp-0.1.1}/tests/ndarray_test.cpp +0 -0
@@ -1,5 +1,5 @@
1
1
  cmake_minimum_required(VERSION 3.21)
2
- project(game_resolver_cpp VERSION 0.1.0 LANGUAGES CXX)
2
+ project(game_resolver_cpp VERSION 0.1.1 LANGUAGES CXX)
3
3
 
4
4
  option(GAME_RESOLVER_BUILD_TESTS "Build the test suite" ${PROJECT_IS_TOP_LEVEL})
5
5
  option(GAME_RESOLVER_BUILD_EXAMPLES "Build the example programs" ${PROJECT_IS_TOP_LEVEL})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: game_resolver_cpp
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: C++ backed game equilibrium solver with the game_resolver interface
5
5
  Keywords: game-theory,nash-equilibrium,bayesian-game
6
6
  Author: nishino-lab
@@ -78,7 +78,11 @@ All four ways of defining payoffs work, exactly as in `game_resolver`:
78
78
 
79
79
  Subclass `BayesianGame` and implement `posterior()`.
80
80
 
81
+ A Bayesian battle of the sexes. Each player has a type `A` / `B`, observes their own type,
82
+ and computes the expected payoff with the posterior over the other player's type.
83
+
81
84
  ```python
85
+ import numpy as np
82
86
  from game_resolver_cpp.bayesian_game import BayesianGame
83
87
  from game_resolver_cpp.bayesian_nash_equilibrium import BayesianNashEquilibrium
84
88
  from game_resolver_cpp.bayesian_player import BayesianPlayer
@@ -97,14 +101,32 @@ class SampleGame(BayesianGame):
97
101
  other = type_tuple[(player_id + 1) % 2]
98
102
  return 1 / 4 if own == other else 3 / 4
99
103
 
100
- # payoff functions take (own type, each player's action)
101
- def male(self, type, male_action, female_action): ...
102
- def female(self, type, male_action, female_action): ...
104
+ # payoff functions take (own type, each player's action), and must return a number
105
+ def male(self, type, male_action, female_action):
106
+ if male_action != female_action:
107
+ return 0.0
108
+ if type == "A":
109
+ return 2.0 if male_action == "Boxing" else 1.0
110
+ return 1.0 if male_action == "Boxing" else 2.0
111
+
112
+ def female(self, type, male_action, female_action):
113
+ if male_action != female_action:
114
+ return 0.0
115
+ if type == "A":
116
+ return 2.0 if female_action == "Boxing" else 1.0
117
+ return 1.0 if female_action == "Boxing" else 2.0
103
118
 
104
119
 
105
120
  nash = BayesianNashEquilibrium(SampleGame()).get_nash_equilibrium()
121
+ print(nash["index"]) # [(0, 0), (1, 2), (2, 1), (3, 3)]
122
+ print(nash["profile"][0]) # [{'A': 'Boxing', 'B': 'Boxing'}, {'A': 'Boxing', 'B': 'Boxing'}]
123
+ print(nash["payoff"][0]) # [{'A': 2.0, 'B': 1.0}, {'A': 2.0, 'B': 1.0}]
106
124
  ```
107
125
 
126
+ `profile` and `payoff` are indexed by `[equilibrium][player][type]`. The types and actions
127
+ are handed back exactly as you passed them in, so with `np.array` they print as
128
+ `np.str_('A')`; pass plain lists if you want plain strings.
129
+
108
130
  ## What gets faster
109
131
 
110
132
  Against the pure Python `game_resolver`, on an 11-core Apple M series machine.
@@ -235,6 +235,8 @@ class SampleGame : public BayesianGame {
235
235
  };
236
236
  ```
237
237
 
238
+ The bodies left out here are in [examples/bayesian_battle_of_sex.cpp](examples/bayesian_battle_of_sex.cpp).
239
+
238
240
  ```cpp
239
241
  SampleGame game;
240
242
  BayesianNashEquilibrium equilibrium(game);
@@ -48,7 +48,11 @@ All four ways of defining payoffs work, exactly as in `game_resolver`:
48
48
 
49
49
  Subclass `BayesianGame` and implement `posterior()`.
50
50
 
51
+ A Bayesian battle of the sexes. Each player has a type `A` / `B`, observes their own type,
52
+ and computes the expected payoff with the posterior over the other player's type.
53
+
51
54
  ```python
55
+ import numpy as np
52
56
  from game_resolver_cpp.bayesian_game import BayesianGame
53
57
  from game_resolver_cpp.bayesian_nash_equilibrium import BayesianNashEquilibrium
54
58
  from game_resolver_cpp.bayesian_player import BayesianPlayer
@@ -67,14 +71,32 @@ class SampleGame(BayesianGame):
67
71
  other = type_tuple[(player_id + 1) % 2]
68
72
  return 1 / 4 if own == other else 3 / 4
69
73
 
70
- # payoff functions take (own type, each player's action)
71
- def male(self, type, male_action, female_action): ...
72
- def female(self, type, male_action, female_action): ...
74
+ # payoff functions take (own type, each player's action), and must return a number
75
+ def male(self, type, male_action, female_action):
76
+ if male_action != female_action:
77
+ return 0.0
78
+ if type == "A":
79
+ return 2.0 if male_action == "Boxing" else 1.0
80
+ return 1.0 if male_action == "Boxing" else 2.0
81
+
82
+ def female(self, type, male_action, female_action):
83
+ if male_action != female_action:
84
+ return 0.0
85
+ if type == "A":
86
+ return 2.0 if female_action == "Boxing" else 1.0
87
+ return 1.0 if female_action == "Boxing" else 2.0
73
88
 
74
89
 
75
90
  nash = BayesianNashEquilibrium(SampleGame()).get_nash_equilibrium()
91
+ print(nash["index"]) # [(0, 0), (1, 2), (2, 1), (3, 3)]
92
+ print(nash["profile"][0]) # [{'A': 'Boxing', 'B': 'Boxing'}, {'A': 'Boxing', 'B': 'Boxing'}]
93
+ print(nash["payoff"][0]) # [{'A': 2.0, 'B': 1.0}, {'A': 2.0, 'B': 1.0}]
76
94
  ```
77
95
 
96
+ `profile` and `payoff` are indexed by `[equilibrium][player][type]`. The types and actions
97
+ are handed back exactly as you passed them in, so with `np.array` they print as
98
+ `np.str_('A')`; pass plain lists if you want plain strings.
99
+
78
100
  ## What gets faster
79
101
 
80
102
  Against the pure Python `game_resolver`, on an 11-core Apple M series machine.
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "game_resolver_cpp"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "C++ backed game equilibrium solver with the game_resolver interface"
9
9
  readme = "docs/README_for_pypi.md"
10
10
  requires-python = ">=3.9"
@@ -24,7 +24,22 @@ class _CoreBayesianGame(_core.BayesianGame):
24
24
  self._game = game
25
25
 
26
26
  def posterior(self, player_id: int, type_profile) -> float:
27
- return float(self._game.posterior(player_id, tuple(type_profile)))
27
+ types = tuple(type_profile)
28
+ value = self._game.posterior(player_id, types)
29
+ try:
30
+ return float(value)
31
+ except (TypeError, ValueError):
32
+ # 素の float() のエラーはどの呼び出しで起きたか言わない。return の書き忘れが多い。
33
+ what = (
34
+ "何も返しませんでした (None)"
35
+ if value is None
36
+ else f"数値でない値 {value!r} を返しました"
37
+ )
38
+ raise TypeError(
39
+ f"{type(self._game).__name__}.posterior() が{what}。"
40
+ f"引数は (player_id={player_id}, type_tuple={types}) でした。"
41
+ "事後確率は実数を返してください"
42
+ ) from None
28
43
 
29
44
 
30
45
  class BayesianNashEquilibrium:
@@ -94,6 +94,42 @@ PayoffTable to_payoff_table(const py::dict& mapping) {
94
94
  return table;
95
95
  }
96
96
 
97
+ /// 呼び出し可能オブジェクトを、エラーメッセージ用の名前にする。
98
+ std::string describe_callable(const py::handle& function) {
99
+ if (py::hasattr(function, "__qualname__")) {
100
+ return py::str(function.attr("__qualname__")).cast<std::string>() + "()";
101
+ }
102
+ return py::repr(function).cast<std::string>();
103
+ }
104
+
105
+ std::string describe_arguments(const py::tuple& arguments) {
106
+ std::string text;
107
+ for (std::size_t i = 0; i < arguments.size(); ++i) {
108
+ if (i != 0) {
109
+ text += ", ";
110
+ }
111
+ text += py::repr(arguments[i]).cast<std::string>();
112
+ }
113
+ return text;
114
+ }
115
+
116
+ /// Python が返した値を利得の実数にする。
117
+ ///
118
+ /// pybind11 の素の cast エラーは「NoneType を C++ の型 '?' に変換できません」としか言わず、
119
+ /// どの関数がどの引数で失敗したのか分からない。戻り値の書き忘れが一番多いので、ここで拾う。
120
+ double to_payoff_value(const py::object& result, const py::handle& function,
121
+ const py::tuple& arguments) {
122
+ try {
123
+ return result.cast<double>();
124
+ } catch (const py::cast_error&) {
125
+ const std::string what =
126
+ result.is_none() ? "何も返しませんでした (None)"
127
+ : "数値でない値 " + py::repr(result).cast<std::string>() + " を返しました";
128
+ throw py::type_error(describe_callable(function) + " が" + what + "。引数は (" +
129
+ describe_arguments(arguments) + ") でした。利得関数は実数を返してください");
130
+ }
131
+ }
132
+
97
133
  /// Python の関数を 1 セルごとに呼ぶ利得関数。
98
134
  ///
99
135
  /// 1 回の呼び出しにつき Python の呼び出しと引数変換が発生するので、セル数が多いゲームでは
@@ -105,7 +141,7 @@ PayoffFunction to_payoff_function(py::function function) {
105
141
  for (std::size_t i = 0; i < profile.size(); ++i) {
106
142
  arguments[i] = from_action(profile[i]);
107
143
  }
108
- return function(*arguments).cast<double>();
144
+ return to_payoff_value(function(*arguments), function, arguments);
109
145
  };
110
146
  }
111
147
 
@@ -130,7 +166,7 @@ BayesianPayoffFunction to_bayesian_payoff_function(py::function function) {
130
166
  for (std::size_t i = 0; i < profile.size(); ++i) {
131
167
  arguments[i + 1] = from_action(profile[i]);
132
168
  }
133
- return function(*arguments).cast<double>();
169
+ return to_payoff_value(function(*arguments), function, arguments);
134
170
  };
135
171
  }
136
172
 
@@ -149,7 +185,19 @@ class PyBayesianGame : public BayesianGame {
149
185
  if (!override) {
150
186
  throw py::value_error("posterior() が実装されていません");
151
187
  }
152
- return override(player_id, types).cast<double>();
188
+ const py::object result = override(player_id, types);
189
+ try {
190
+ return result.cast<double>();
191
+ } catch (const py::cast_error&) {
192
+ const std::string what = result.is_none()
193
+ ? "何も返しませんでした (None)"
194
+ : "数値でない値 " + py::repr(result).cast<std::string>() +
195
+ " を返しました";
196
+ throw py::type_error("posterior() が" + what + "。引数は (player_id=" +
197
+ std::to_string(player_id) + ", type_profile=" +
198
+ py::repr(types).cast<std::string>() +
199
+ ") でした。事後確率は実数を返してください");
200
+ }
153
201
  }
154
202
  };
155
203
 
@@ -237,6 +237,38 @@ def test_initialize_payoff_matrices_is_idempotent():
237
237
  assert len(equilibrium.get_nash_equilibrium()["index"]) == 4
238
238
 
239
239
 
240
+ def test_reports_payoff_function_returning_none():
241
+ """return を書き忘れた利得関数は、どの関数のどの呼び出しかを言って落ちる。"""
242
+
243
+ class ForgotToReturn(SampleGame):
244
+ def male(self, type, male_action, female_action):
245
+ pass
246
+
247
+ with pytest.raises(TypeError, match="ForgotToReturn.male"):
248
+ BayesianNashEquilibrium(ForgotToReturn()).get_nash_equilibrium()
249
+
250
+
251
+ def test_reports_posterior_returning_none():
252
+ class ForgotToReturn(SampleGame):
253
+ def posterior(self, player_id, type_tuple):
254
+ pass
255
+
256
+ with pytest.raises(TypeError, match="posterior"):
257
+ BayesianNashEquilibrium(ForgotToReturn()).get_nash_equilibrium()
258
+
259
+
260
+ def test_core_reports_payoff_callable_returning_none():
261
+ """C++ から呼び戻す経路(_core.Player に関数を渡す)でも同じように言う。"""
262
+ from game_resolver_cpp import _core
263
+
264
+ game = _core.Game()
265
+ actions = ["C", "D"]
266
+ game.add(_core.Player(0, actions, lambda a, b: None))
267
+ game.add(_core.Player(1, actions, np.array([[3.0, 5.0], [0.0, 1.0]])))
268
+ with pytest.raises(TypeError, match="None"):
269
+ _core.NashEquilibrium(game)
270
+
271
+
240
272
  # ---------------------------------------------------------------- その他
241
273
 
242
274
 
@@ -3,7 +3,7 @@
3
3
  namespace game_resolver {
4
4
 
5
5
  std::string_view version() noexcept {
6
- return "0.1.0";
6
+ return "0.1.1";
7
7
  }
8
8
 
9
9
  } // namespace game_resolver
@@ -3,5 +3,5 @@
3
3
  #include "game_resolver/version.hpp"
4
4
 
5
5
  TEST(Smoke, VersionIsReported) {
6
- EXPECT_EQ(game_resolver::version(), "0.1.0");
6
+ EXPECT_EQ(game_resolver::version(), "0.1.1");
7
7
  }