phase-python 0.77.0__cp314-cp314-macosx_11_0_arm64.whl
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.
- phase/__init__.py +19 -0
- phase/_phase.cpython-314-darwin.so +0 -0
- phase/_phase.pyi +207 -0
- phase/gen.py +210 -0
- phase/py.typed +0 -0
- phase_python-0.77.0.dist-info/METADATA +101 -0
- phase_python-0.77.0.dist-info/RECORD +11 -0
- phase_python-0.77.0.dist-info/WHEEL +4 -0
- phase_python-0.77.0.dist-info/entry_points.txt +2 -0
- phase_python-0.77.0.dist-info/licenses/LICENSE +21 -0
- phase_python-0.77.0.dist-info/sboms/phase.cyclonedx.json +2459 -0
phase/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Python bindings for the phase.rs Magic: The Gathering rules engine."""
|
|
2
|
+
|
|
3
|
+
from phase._phase import (
|
|
4
|
+
ActionResult,
|
|
5
|
+
Engine,
|
|
6
|
+
Game,
|
|
7
|
+
GameAction,
|
|
8
|
+
build_oracle_face,
|
|
9
|
+
build_oracle_face_multi,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ActionResult",
|
|
14
|
+
"Engine",
|
|
15
|
+
"Game",
|
|
16
|
+
"GameAction",
|
|
17
|
+
"build_oracle_face",
|
|
18
|
+
"build_oracle_face_multi",
|
|
19
|
+
]
|
|
Binary file
|
phase/_phase.pyi
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python bindings for the phase.rs Magic: The Gathering rules engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from typing import Any, final
|
|
8
|
+
|
|
9
|
+
@final
|
|
10
|
+
class ActionResult:
|
|
11
|
+
"""
|
|
12
|
+
Native result of applying one or more actions.
|
|
13
|
+
|
|
14
|
+
Event and prompt values are converted only when their getters are read.
|
|
15
|
+
"""
|
|
16
|
+
def __repr__(self, /) -> str: ...
|
|
17
|
+
@property
|
|
18
|
+
def events(self, /) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Events emitted by the requested action and any fast-forwarded passes.
|
|
21
|
+
"""
|
|
22
|
+
@property
|
|
23
|
+
def fast_forwarded(self, /) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Number of automatic `PassPriority` actions applied after the requested action.
|
|
26
|
+
"""
|
|
27
|
+
@property
|
|
28
|
+
def log_entries(self, /) -> Any:
|
|
29
|
+
"""
|
|
30
|
+
Game log entries emitted by the represented actions.
|
|
31
|
+
"""
|
|
32
|
+
def to_dict(self, /) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Convert to the engine's JSON-compatible result shape.
|
|
35
|
+
"""
|
|
36
|
+
@property
|
|
37
|
+
def waiting_for(self, /) -> Any:
|
|
38
|
+
"""
|
|
39
|
+
Prompt active after all actions represented by this result.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
class Engine:
|
|
44
|
+
"""
|
|
45
|
+
Loaded card database used to create games.
|
|
46
|
+
"""
|
|
47
|
+
def card_count(self, /) -> int:
|
|
48
|
+
"""
|
|
49
|
+
Number of card faces in the loaded database.
|
|
50
|
+
"""
|
|
51
|
+
@staticmethod
|
|
52
|
+
def from_json(json: str) -> Engine:
|
|
53
|
+
"""
|
|
54
|
+
Load a `card-data.json` export from a JSON string.
|
|
55
|
+
"""
|
|
56
|
+
@staticmethod
|
|
57
|
+
def from_path(path: str |PathLike[str]) -> Engine:
|
|
58
|
+
"""
|
|
59
|
+
Load a `card-data.json` export from disk.
|
|
60
|
+
"""
|
|
61
|
+
def load_game(self, /, state: Any) -> Game:
|
|
62
|
+
"""
|
|
63
|
+
Resume a game from a previously exported state.
|
|
64
|
+
|
|
65
|
+
`state` may be the dict returned by [`Game::state`], a JSON string of
|
|
66
|
+
that dict, or a WASM `TrustedGameStateEnvelope` (`{"state": ...}`).
|
|
67
|
+
"""
|
|
68
|
+
def new_game(self, /, player: Any, opponent: Any, *, extra_players: Sequence[Any] |None = None, seed: int = 42, format: str = "Standard", format_config: Any |None = None, match_config: Any |None = None, first_player: int |None = None) -> Game:
|
|
69
|
+
"""
|
|
70
|
+
Start a match and return a live [`Game`].
|
|
71
|
+
|
|
72
|
+
`player` / `opponent` / `extra_players` may be a list of card names
|
|
73
|
+
(treated as the main deck) or a dict matching the engine `PlayerDeckList`
|
|
74
|
+
(`main_deck`, `sideboard`, `commander`, ...).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@final
|
|
78
|
+
class Game:
|
|
79
|
+
"""
|
|
80
|
+
A started game: inspect legal actions, apply one, and read state.
|
|
81
|
+
"""
|
|
82
|
+
def __repr__(self, /) -> str: ...
|
|
83
|
+
def actions(self, /) -> list[GameAction]:
|
|
84
|
+
"""
|
|
85
|
+
Legal actions for the player currently expected to act.
|
|
86
|
+
|
|
87
|
+
Returns [`GameAction`] objects. Pass one back to [`Game::apply`] with no
|
|
88
|
+
JSON conversion. Dicts in the tagged engine shape are still accepted.
|
|
89
|
+
"""
|
|
90
|
+
def apply(self, /, actor: int, action: Any, *, fast_forward: bool = False) -> ActionResult:
|
|
91
|
+
"""
|
|
92
|
+
Apply `action` as `actor` (seat index) and return the `ActionResult`.
|
|
93
|
+
|
|
94
|
+
`action` should be a [`GameAction`] from [`Game::actions`]. A tagged
|
|
95
|
+
JSON dict is still accepted.
|
|
96
|
+
"""
|
|
97
|
+
def battlefield(self, /) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Objects currently on the battlefield.
|
|
100
|
+
"""
|
|
101
|
+
def exile(self, /) -> Any:
|
|
102
|
+
"""
|
|
103
|
+
Objects in exile.
|
|
104
|
+
"""
|
|
105
|
+
def graveyard(self, /, seat: int) -> Any:
|
|
106
|
+
"""
|
|
107
|
+
Objects in `seat`'s graveyard.
|
|
108
|
+
"""
|
|
109
|
+
def hand(self, /, seat: int) -> Any:
|
|
110
|
+
"""
|
|
111
|
+
Objects in `seat`'s hand.
|
|
112
|
+
"""
|
|
113
|
+
def object(self, /, object_id: int) -> Any:
|
|
114
|
+
"""
|
|
115
|
+
Look up one game object by its numeric object ID.
|
|
116
|
+
"""
|
|
117
|
+
@property
|
|
118
|
+
def phase(self, /) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Current phase name.
|
|
121
|
+
"""
|
|
122
|
+
def player(self, /, seat: int) -> Any:
|
|
123
|
+
"""
|
|
124
|
+
Public player state for `seat`.
|
|
125
|
+
"""
|
|
126
|
+
def priority_player(self, /) -> int:
|
|
127
|
+
"""
|
|
128
|
+
Seat that currently holds priority, if any.
|
|
129
|
+
"""
|
|
130
|
+
def stack(self, /) -> Any:
|
|
131
|
+
"""
|
|
132
|
+
Current stack entries.
|
|
133
|
+
"""
|
|
134
|
+
def state(self, /) -> Any:
|
|
135
|
+
"""
|
|
136
|
+
Full persisted `GameState` as a Python dict (same serde shape as WASM export).
|
|
137
|
+
|
|
138
|
+
Pass the result to [`Engine::load_game`] to resume from this snapshot.
|
|
139
|
+
"""
|
|
140
|
+
@property
|
|
141
|
+
def turn(self, /) -> int:
|
|
142
|
+
"""
|
|
143
|
+
Current turn number.
|
|
144
|
+
"""
|
|
145
|
+
def waiting_for(self, /) -> Any:
|
|
146
|
+
"""
|
|
147
|
+
Current `waiting_for` prompt.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
@final
|
|
151
|
+
class GameAction:
|
|
152
|
+
"""
|
|
153
|
+
An engine `GameAction`. Returned by [`Game::actions`] and accepted by [`Game::apply`].
|
|
154
|
+
|
|
155
|
+
The Rust value is held natively — listing and applying actions does not
|
|
156
|
+
serialize through JSON. Use [`GameAction::to_dict`] only when you need the
|
|
157
|
+
tagged JSON shape.
|
|
158
|
+
"""
|
|
159
|
+
@property
|
|
160
|
+
def __dict__(self, /) -> dict:
|
|
161
|
+
"""
|
|
162
|
+
Flattened view of `kind` plus payload fields. Debuggers that inspect
|
|
163
|
+
`__dict__` use this instead of the native `EngineAction` layout.
|
|
164
|
+
"""
|
|
165
|
+
def __dir__(self, /) -> list[str]: ...
|
|
166
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
167
|
+
def __getattr__(self, name: str, /) -> Any: ...
|
|
168
|
+
def __repr__(self, /) -> str: ...
|
|
169
|
+
@property
|
|
170
|
+
def data(self, /) -> Any:
|
|
171
|
+
"""
|
|
172
|
+
Variant payload as a dict (or `None` for unit variants).
|
|
173
|
+
|
|
174
|
+
Field names match the engine `GameAction` serde shape, so a `PlayLand`
|
|
175
|
+
action exposes `{"object_id": ..., "card_id": ...}`. The same keys are
|
|
176
|
+
also available as attributes (`action.object_id`) for debugger inspection.
|
|
177
|
+
"""
|
|
178
|
+
@staticmethod
|
|
179
|
+
def from_dict(value: Any) -> GameAction:
|
|
180
|
+
"""
|
|
181
|
+
Build from the tagged JSON dict (`{"type": "...", "data": ...}`).
|
|
182
|
+
"""
|
|
183
|
+
@property
|
|
184
|
+
def kind(self, /) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Variant name, e.g. `"MulliganDecision"` or `"PassPriority"`.
|
|
187
|
+
"""
|
|
188
|
+
def to_dict(self, /) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Tagged JSON dict. Prefer passing this object to [`Game::apply`] instead.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def build_oracle_face(mtgjson: Any, oracle_id: str |None) -> Any:
|
|
194
|
+
"""
|
|
195
|
+
Build a `CardFace` from MTGJSON atomic card data.
|
|
196
|
+
|
|
197
|
+
`mtgjson` should be a dict matching the engine `AtomicCard` shape
|
|
198
|
+
(`name`, `mana_cost`, `types`, `text`, `layout`, etc.).
|
|
199
|
+
`oracle_id` is an optional Scryfall oracle ID.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def build_oracle_face_multi(mtgjson: Any, oracle_id: str |None) -> Any:
|
|
203
|
+
"""
|
|
204
|
+
Build a `CardFace` for a multi-face card, skipping MTGJSON keywords.
|
|
205
|
+
|
|
206
|
+
See [`build_oracle_face`] for parameter details.
|
|
207
|
+
"""
|
phase/gen.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# simplified version of crates/engine/src/bin/oracle_gen.rs
|
|
2
|
+
import argparse
|
|
3
|
+
import gzip
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import urllib.request
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from phase import build_oracle_face, build_oracle_face_multi
|
|
10
|
+
|
|
11
|
+
INPUT_PATH = "AtomicCards.json.gz"
|
|
12
|
+
OUTPUT_PATH = "card-data.json"
|
|
13
|
+
SOURCE_URL = "https://mtgjson.com/api/v5/AtomicCards.json.gz"
|
|
14
|
+
SET_SOURCE_URL = "https://mtgjson.com/api/v5/{set_name}.json.gz"
|
|
15
|
+
|
|
16
|
+
MULTI_LAYOUTS = {
|
|
17
|
+
"split",
|
|
18
|
+
"flip",
|
|
19
|
+
"transform",
|
|
20
|
+
"meld",
|
|
21
|
+
"adventure",
|
|
22
|
+
"modal_dfc",
|
|
23
|
+
"prepare",
|
|
24
|
+
"aftermath",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def legality_score(card: dict) -> int:
|
|
29
|
+
return sum(1 for status in (card.get("legalities") or {}).values() if status.lower() == "legal")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_entry(face: dict, layout=None, face_index=None) -> dict:
|
|
33
|
+
entry = dict(face)
|
|
34
|
+
if layout:
|
|
35
|
+
entry["layout"] = layout
|
|
36
|
+
if face_index is not None:
|
|
37
|
+
entry["face_index"] = face_index
|
|
38
|
+
return entry
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def insert_face(result: dict, entry: dict) -> None:
|
|
42
|
+
key = entry['name'].lower()
|
|
43
|
+
existing = result.get(key)
|
|
44
|
+
if existing is None:
|
|
45
|
+
result[key] = entry
|
|
46
|
+
return
|
|
47
|
+
# Standalone paper cards win over a multi-face back face of the same name.
|
|
48
|
+
if existing.get("layout") and not entry.get("layout"):
|
|
49
|
+
result[key] = entry
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def oracle_id(card: dict) -> str | None:
|
|
53
|
+
return (card.get("identifiers") or {}).get("scryfallOracleId")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_json(path: str):
|
|
57
|
+
if path.endswith(".gz"):
|
|
58
|
+
with gzip.open(path, "rt", encoding="utf-8") as f:
|
|
59
|
+
return json.load(f)
|
|
60
|
+
with open(path, encoding="utf-8") as f:
|
|
61
|
+
return json.load(f)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def dedupe_set_faces(cards: list) -> list:
|
|
65
|
+
unique = []
|
|
66
|
+
seen = set()
|
|
67
|
+
for card in cards:
|
|
68
|
+
key = (card.get("faceName") or card.get("name"), card.get("side"), oracle_id(card))
|
|
69
|
+
if key in seen:
|
|
70
|
+
continue
|
|
71
|
+
seen.add(key)
|
|
72
|
+
unique.append(card)
|
|
73
|
+
unique.sort(key=lambda c: c.get("side") or "a")
|
|
74
|
+
return unique
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def groups_from_set_cards(cards: list):
|
|
78
|
+
by_name = defaultdict(list)
|
|
79
|
+
for card in cards:
|
|
80
|
+
if isinstance(card, dict) and card.get("name"):
|
|
81
|
+
by_name[card["name"]].append(card)
|
|
82
|
+
for name, group in by_name.items():
|
|
83
|
+
yield name, dedupe_set_faces(group)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_card_groups(file_path: str):
|
|
87
|
+
root = load_json(file_path)
|
|
88
|
+
data = root.get("data", {})
|
|
89
|
+
if "cards" in data:
|
|
90
|
+
yield from groups_from_set_cards(data["cards"])
|
|
91
|
+
else:
|
|
92
|
+
for name, cards in data.items():
|
|
93
|
+
if isinstance(cards, list):
|
|
94
|
+
yield name, cards
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def process_group(cards: list, result: dict) -> None:
|
|
98
|
+
faces = [c for c in cards if isinstance(c, dict) and not c.get("isFunny")]
|
|
99
|
+
if not faces or len(faces) > 2:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if len(faces) == 1:
|
|
103
|
+
source = faces[0]
|
|
104
|
+
face = build_oracle_face(source, oracle_id(source))
|
|
105
|
+
insert_face(result, face)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if len(faces) == 2 and faces[1]['subtypes'] == ['Omen']:
|
|
109
|
+
# fix Omen -> adventure layout. in mtgjson data they have layout 'reversible_card'
|
|
110
|
+
layouts = {'adventure', }
|
|
111
|
+
else:
|
|
112
|
+
layouts = {f.get("layout", "normal") for f in faces}
|
|
113
|
+
if layouts <= MULTI_LAYOUTS:
|
|
114
|
+
oid = oracle_id(faces[0])
|
|
115
|
+
layout_str = 'adventure' if layouts == {'adventure', } else faces[0].get("layout")
|
|
116
|
+
face_a = build_oracle_face_multi(faces[0], oid)
|
|
117
|
+
face_b = build_oracle_face_multi(faces[1], oid)
|
|
118
|
+
|
|
119
|
+
for idx, face in enumerate([face_a, face_b]):
|
|
120
|
+
insert_face(
|
|
121
|
+
result,
|
|
122
|
+
make_entry(
|
|
123
|
+
face,
|
|
124
|
+
layout=layout_str,
|
|
125
|
+
face_index=idx,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if all(f.get("layout", "normal") not in MULTI_LAYOUTS for f in faces):
|
|
131
|
+
# name collisions
|
|
132
|
+
source = max(faces, key=legality_score)
|
|
133
|
+
face = build_oracle_face(source, oracle_id(source))
|
|
134
|
+
insert_face(result, face)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def resolve_set_input(set_name: str) -> str:
|
|
138
|
+
for candidate in (f"{set_name}.json.gz", f"{set_name}.json"):
|
|
139
|
+
if os.path.exists(candidate):
|
|
140
|
+
return candidate
|
|
141
|
+
return f"{set_name}.json.gz"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def ensure_source(file_path: str, source_url: str) -> None:
|
|
145
|
+
if os.path.exists(file_path):
|
|
146
|
+
return
|
|
147
|
+
print(f"Downloading {source_url} -> {file_path}")
|
|
148
|
+
tmp_path = file_path + ".tmp"
|
|
149
|
+
try:
|
|
150
|
+
urllib.request.urlretrieve(source_url, tmp_path)
|
|
151
|
+
os.replace(tmp_path, file_path)
|
|
152
|
+
except Exception:
|
|
153
|
+
if os.path.exists(tmp_path):
|
|
154
|
+
os.remove(tmp_path)
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def process_cards(file_path: str, output_path: str) -> None:
|
|
159
|
+
print(f"Processing cards from: {file_path}")
|
|
160
|
+
result = {}
|
|
161
|
+
for name, cards in load_card_groups(file_path):
|
|
162
|
+
try:
|
|
163
|
+
process_group(cards, result)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
print(f"Skipping {name}: {exc}")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
169
|
+
json.dump(result, f, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
170
|
+
print(f"Wrote {len(result)} faces to {output_path}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main(argv: list[str] | None = None) -> None:
|
|
174
|
+
parser = argparse.ArgumentParser(
|
|
175
|
+
prog="phase-gen",
|
|
176
|
+
description="Generate card-data.json from MTGJSON AtomicCards or a single set.",
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"-i",
|
|
180
|
+
"--input",
|
|
181
|
+
default=None,
|
|
182
|
+
help="Path to AtomicCards.json.gz or a set JSON (downloaded from MTGJSON if missing)",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"-o",
|
|
186
|
+
"--output",
|
|
187
|
+
default=OUTPUT_PATH,
|
|
188
|
+
help="Path to write card-data.json",
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--set",
|
|
192
|
+
dest="set_name",
|
|
193
|
+
help="MTGJSON set code (e.g. HOB). Downloads https://mtgjson.com/api/v5/{SET}.json.gz "
|
|
194
|
+
"and reads cards from data.cards",
|
|
195
|
+
)
|
|
196
|
+
args = parser.parse_args(argv)
|
|
197
|
+
if args.set_name:
|
|
198
|
+
set_name = args.set_name.upper()
|
|
199
|
+
source_url = SET_SOURCE_URL.format(set_name=set_name)
|
|
200
|
+
input_path = args.input if args.input is not None else resolve_set_input(set_name)
|
|
201
|
+
else:
|
|
202
|
+
source_url = SOURCE_URL
|
|
203
|
+
input_path = args.input if args.input is not None else INPUT_PATH
|
|
204
|
+
|
|
205
|
+
ensure_source(input_path, source_url)
|
|
206
|
+
process_cards(input_path, args.output)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
phase/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phase-python
|
|
3
|
+
Version: 0.77.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: Rust
|
|
15
|
+
Classifier: Topic :: Games/Entertainment
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Summary: Python bindings for the phase.rs Magic: The Gathering rules engine
|
|
18
|
+
Keywords: mtg,magic-the-gathering,rules-engine
|
|
19
|
+
Author-email: Boris Klyus <klyusba@gmail.com>
|
|
20
|
+
License: MIT
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
23
|
+
Project-URL: Homepage, https://github.com/klyusba/phase-python
|
|
24
|
+
Project-URL: Repository, https://github.com/klyusba/phase-python
|
|
25
|
+
|
|
26
|
+
# phase-rs
|
|
27
|
+
|
|
28
|
+
Unofficial Python bindings for the [phase.rs](https://github.com/phase-rs/phase) Magic: The Gathering rules engine.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from phase import Engine, Game, GameAction
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install from PyPI (prebuilt wheels; no Rust toolchain required):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install phase-python
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The import name is `phase`. The distribution is `phase-rs` because `phase` is already taken on PyPI.
|
|
41
|
+
|
|
42
|
+
## Card data
|
|
43
|
+
|
|
44
|
+
The engine needs a `card-data.json` oracle export. After installing the package, generate one with:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
phase-gen -o card-data.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or generate `card-data.json` for one set only:
|
|
51
|
+
```bash
|
|
52
|
+
phase-gen --set SET -o card-data.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If `AtomicCards.json.gz` is missing, `phase-gen` downloads it from [MTGJSON](https://mtgjson.com). You can also pass an existing dump:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
phase-gen -i AtomicCards.json.gz -o card-data.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You can generate a `card-data.json` for single set with
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
phase-gen -i HOB.json -o card-data.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A hosted snapshot is available at https://data.phase-rs.dev/card-data.json.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from phase import Engine
|
|
73
|
+
|
|
74
|
+
engine = Engine.from_path("card-data.json")
|
|
75
|
+
game = engine.new_game(
|
|
76
|
+
player=["Forest"] * 60,
|
|
77
|
+
opponent=["Forest"] * 60,
|
|
78
|
+
seed=42,
|
|
79
|
+
first_player=0,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
actions = game.actions()
|
|
83
|
+
result = game.apply(0, actions[0])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
See [API.md](API.md) for the full Python API.
|
|
87
|
+
|
|
88
|
+
## Develop from source
|
|
89
|
+
|
|
90
|
+
Requires Rust (see `rust-toolchain.toml`) and Python 3.10+.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv venv && source .venv/bin/activate
|
|
94
|
+
uv pip install maturin
|
|
95
|
+
maturin develop --generate-stubs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
101
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
phase/__init__.py,sha256=_Aur4XAH3a6L_RbTuKtkcX44P1t9msdeA1RtOVUEkMQ,347
|
|
2
|
+
phase/_phase.cpython-314-darwin.so,sha256=YHDuAbEE-vOylLAseU6GK0BaqTFoqicM63M9lfYbLV8,62184064
|
|
3
|
+
phase/_phase.pyi,sha256=tISdGY1kHfMJMFn5-c1qTseqS69Jhiy9VXm1xWyUcbE,6627
|
|
4
|
+
phase/gen.py,sha256=EsBsge7WpVw96dI9Ax3Tvz2SOG8ZfD5uCuPPuK65hqk,6357
|
|
5
|
+
phase/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
phase_python-0.77.0.dist-info/METADATA,sha256=RMacQU3JwVgLmbeBmRPxaQES-wojb-PbixwkFBymYwQ,2617
|
|
7
|
+
phase_python-0.77.0.dist-info/WHEEL,sha256=wCw_9gE4rxFYW3BEzeT3cTNR34OBbxo66jwr_m338Mg,105
|
|
8
|
+
phase_python-0.77.0.dist-info/entry_points.txt,sha256=cGDxKEmcSjzKFUeQBBgH2ZHsX1qvtZOYUfunWpOORdQ,43
|
|
9
|
+
phase_python-0.77.0.dist-info/licenses/LICENSE,sha256=2AfuGGvse92w30YagK582bRaR63VGWsvqEJEgjZIM2U,1083
|
|
10
|
+
phase_python-0.77.0.dist-info/sboms/phase.cyclonedx.json,sha256=8kGtcsPRguwEkTV4x35C2OXF4APTOxgmZIYAIEv_-YE,76367
|
|
11
|
+
phase_python-0.77.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 phase.rs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|