draftomen 0.3.0__py3-none-any.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.
- draftomen/__init__.py +17 -0
- draftomen/assets/draftomen.icns +0 -0
- draftomen/assets/draftomen.ico +0 -0
- draftomen/assets/draftomen_logo.png +0 -0
- draftomen/audit.py +606 -0
- draftomen/backtest.py +449 -0
- draftomen/benchmark.py +834 -0
- draftomen/carddb.py +1869 -0
- draftomen/cardimages.py +313 -0
- draftomen/cli.py +891 -0
- draftomen/config.py +133 -0
- draftomen/deckbuilder.py +2723 -0
- draftomen/events.py +772 -0
- draftomen/logfollow.py +451 -0
- draftomen/mock_session.py +745 -0
- draftomen/paths.py +120 -0
- draftomen/pickengine.py +1117 -0
- draftomen/pool.py +1259 -0
- draftomen/preferences.py +289 -0
- draftomen/qml/AboutDialog.qml +156 -0
- draftomen/qml/AppBar.qml +120 -0
- draftomen/qml/BacktestView.qml +316 -0
- draftomen/qml/BuildView.qml +1151 -0
- draftomen/qml/CardPreview.qml +434 -0
- draftomen/qml/DimensionalButton.qml +42 -0
- draftomen/qml/DimensionalComboBox.qml +182 -0
- draftomen/qml/DimensionalSurface.qml +118 -0
- draftomen/qml/DimensionalTabButton.qml +41 -0
- draftomen/qml/LiveDraftView.qml +468 -0
- draftomen/qml/Main.qml +175 -0
- draftomen/qml/NavigationRail.qml +226 -0
- draftomen/qml/PoolSummaryPanel.qml +322 -0
- draftomen/qml/PrivacyDialog.qml +96 -0
- draftomen/qml/RecentPickThumbnail.qml +83 -0
- draftomen/qml/RecentPicksGallery.qml +333 -0
- draftomen/qml/RecommendationRow.qml +404 -0
- draftomen/qml/SettingsSwitch.qml +141 -0
- draftomen/qml/SettingsView.qml +531 -0
- draftomen/qml/StateBanner.qml +189 -0
- draftomen/qml/StatusStrip.qml +84 -0
- draftomen/qml/Theme.qml +71 -0
- draftomen/qml/qmldir +23 -0
- draftomen/qt_adapter.py +884 -0
- draftomen/qt_gui.py +338 -0
- draftomen/qt_mock.py +63 -0
- draftomen/ranking.py +126 -0
- draftomen/replay.py +480 -0
- draftomen/session.py +3668 -0
- draftomen/setinfo.py +26 -0
- draftomen/seventeen.py +2766 -0
- draftomen/splash.py +617 -0
- draftomen/tui.py +4007 -0
- draftomen/watch.py +336 -0
- draftomen-0.3.0.dist-info/METADATA +156 -0
- draftomen-0.3.0.dist-info/RECORD +59 -0
- draftomen-0.3.0.dist-info/WHEEL +5 -0
- draftomen-0.3.0.dist-info/entry_points.txt +6 -0
- draftomen-0.3.0.dist-info/licenses/LICENSE +21 -0
- draftomen-0.3.0.dist-info/top_level.txt +1 -0
draftomen/watch.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Adapt shared live-session events to plain-text watch output.
|
|
2
|
+
The adapter preserves the established command and output contracts.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Iterable
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
from os import PathLike
|
|
12
|
+
from typing import TextIO, TypeAlias
|
|
13
|
+
|
|
14
|
+
from draftomen.carddb import CardDatabase
|
|
15
|
+
from draftomen.config import POLL_INTERVAL_SECONDS
|
|
16
|
+
from draftomen.deckbuilder import DeckBuilderError, format_build_result
|
|
17
|
+
from draftomen.events import (
|
|
18
|
+
AccountEvent,
|
|
19
|
+
DraftCompletedEvent,
|
|
20
|
+
DraftStartedEvent,
|
|
21
|
+
PackOfferedEvent,
|
|
22
|
+
PickMadeEvent,
|
|
23
|
+
QuickDraftDetectedEvent,
|
|
24
|
+
)
|
|
25
|
+
from draftomen.replay import (
|
|
26
|
+
format_draft_completed_event,
|
|
27
|
+
format_pack_offered_event,
|
|
28
|
+
format_pick_made_event,
|
|
29
|
+
)
|
|
30
|
+
from draftomen.session import (
|
|
31
|
+
LiveSession,
|
|
32
|
+
LiveSessionEvent,
|
|
33
|
+
OperationKind,
|
|
34
|
+
RequestBuild,
|
|
35
|
+
)
|
|
36
|
+
from draftomen.seventeen import SeventeenLandsData
|
|
37
|
+
|
|
38
|
+
PathInput: TypeAlias = str | PathLike[str]
|
|
39
|
+
RatingsLoader: TypeAlias = Callable[[str], SeventeenLandsData]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PlainLogWatcher:
|
|
43
|
+
"""Render one shared live session as incremental plain text.
|
|
44
|
+
Tests can call poll_once to simulate one live polling cycle exactly.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
log_path: PathInput,
|
|
51
|
+
card_database: CardDatabase,
|
|
52
|
+
app_dir: PathInput | None = None,
|
|
53
|
+
poll_interval: float = POLL_INTERVAL_SECONDS,
|
|
54
|
+
previous_log_path: PathInput | None = None,
|
|
55
|
+
ratings_loader: RatingsLoader | None = None,
|
|
56
|
+
splash_enabled: bool = True,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.card_database = card_database
|
|
59
|
+
self.splash_enabled = splash_enabled
|
|
60
|
+
self._events: list[LiveSessionEvent] = []
|
|
61
|
+
self.session = LiveSession(
|
|
62
|
+
log_path=log_path,
|
|
63
|
+
card_database=card_database,
|
|
64
|
+
app_dir=app_dir,
|
|
65
|
+
poll_interval=poll_interval,
|
|
66
|
+
previous_log_path=previous_log_path,
|
|
67
|
+
event_publisher=self._capture_event,
|
|
68
|
+
ratings_loader=ratings_loader,
|
|
69
|
+
lazy_pair_card_ratings=True,
|
|
70
|
+
splash_enabled=splash_enabled,
|
|
71
|
+
)
|
|
72
|
+
self.log_path = self.session.log_path
|
|
73
|
+
|
|
74
|
+
def _capture_event(self, published: LiveSessionEvent) -> None:
|
|
75
|
+
if (
|
|
76
|
+
isinstance(published.event, DraftCompletedEvent)
|
|
77
|
+
and published.snapshot.draft is not None
|
|
78
|
+
):
|
|
79
|
+
snapshot = self.session.dispatch(
|
|
80
|
+
command=RequestBuild(allow_splash=self.splash_enabled),
|
|
81
|
+
)
|
|
82
|
+
published = replace(published, snapshot=snapshot)
|
|
83
|
+
|
|
84
|
+
self._events.append(published)
|
|
85
|
+
|
|
86
|
+
def poll_once(self) -> str:
|
|
87
|
+
"""Process one follower poll cycle and return rendered output.
|
|
88
|
+
Empty strings mean no complete draft events were available this cycle.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
self._events.clear()
|
|
92
|
+
self.session.poll_once()
|
|
93
|
+
return self._render_published_events()
|
|
94
|
+
|
|
95
|
+
def scan_startup_files(self, *, include_previous: bool = True) -> str:
|
|
96
|
+
"""Process Player-prev.log and Player.log from the beginning.
|
|
97
|
+
This is opt-in recovery for a watch session started mid-draft.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
self._events.clear()
|
|
101
|
+
self.session.scan_startup_files(include_previous=include_previous)
|
|
102
|
+
return self._render_published_events()
|
|
103
|
+
|
|
104
|
+
def process_lines(self, *, lines: Iterable[str]) -> str:
|
|
105
|
+
"""Pass complete log lines through the shared live session.
|
|
106
|
+
Formatting consumes only published session events and commands.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
self._events.clear()
|
|
110
|
+
self.session.process_lines(lines=lines)
|
|
111
|
+
return self._render_published_events()
|
|
112
|
+
|
|
113
|
+
def _render_published_events(self) -> str:
|
|
114
|
+
output_lines: list[str] = []
|
|
115
|
+
for published in self._events:
|
|
116
|
+
output_lines.extend(self._format_event(published=published))
|
|
117
|
+
|
|
118
|
+
self._events.clear()
|
|
119
|
+
return _join_output_lines(lines=output_lines)
|
|
120
|
+
|
|
121
|
+
def _format_event(self, *, published: LiveSessionEvent) -> list[str]:
|
|
122
|
+
event = published.event
|
|
123
|
+
if isinstance(event, AccountEvent):
|
|
124
|
+
label = _account_label(published=published, account_id=event.client_id)
|
|
125
|
+
if event.previous_client_id is None:
|
|
126
|
+
headline = f"Active account: {label}"
|
|
127
|
+
else:
|
|
128
|
+
previous_label = _account_label(
|
|
129
|
+
published=published,
|
|
130
|
+
account_id=event.previous_client_id,
|
|
131
|
+
)
|
|
132
|
+
headline = f"Account switched: {previous_label} -> {label}"
|
|
133
|
+
|
|
134
|
+
return [headline, f"Status: active account {label}", ""]
|
|
135
|
+
|
|
136
|
+
if isinstance(event, QuickDraftDetectedEvent):
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
if isinstance(event, DraftStartedEvent):
|
|
140
|
+
account_label = _account_label(
|
|
141
|
+
published=published,
|
|
142
|
+
account_id=event.account_id,
|
|
143
|
+
)
|
|
144
|
+
return [
|
|
145
|
+
"Draft started: "
|
|
146
|
+
f"{event.event_name} (set {event.set_code}, draft {event.course_id})",
|
|
147
|
+
f"Status: active account {account_label}, draft {event.course_id}",
|
|
148
|
+
"",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if isinstance(event, PackOfferedEvent):
|
|
152
|
+
if published.scored_pack is None:
|
|
153
|
+
raise RuntimeError("Shared live session did not score the offered pack.")
|
|
154
|
+
|
|
155
|
+
pack_lines = format_pack_offered_event(
|
|
156
|
+
event=event,
|
|
157
|
+
card_database=self.card_database,
|
|
158
|
+
scored_pack=published.scored_pack,
|
|
159
|
+
)
|
|
160
|
+
account_label = _account_label(
|
|
161
|
+
published=published,
|
|
162
|
+
account_id=event.account_id,
|
|
163
|
+
)
|
|
164
|
+
lines = [
|
|
165
|
+
"Status: "
|
|
166
|
+
f"active account {account_label}, "
|
|
167
|
+
f"pick P{event.pack_number + 1}P{event.pick_number + 1}, "
|
|
168
|
+
f"{_color_status_from_pack_lines(lines=pack_lines)}, "
|
|
169
|
+
f"data {_data_source_from_pack_lines(lines=pack_lines)}"
|
|
170
|
+
]
|
|
171
|
+
lines.extend(_pack_lines_without_color_status(lines=pack_lines))
|
|
172
|
+
return lines
|
|
173
|
+
|
|
174
|
+
if isinstance(event, PickMadeEvent):
|
|
175
|
+
lines = format_pick_made_event(
|
|
176
|
+
event=event,
|
|
177
|
+
card_database=self.card_database,
|
|
178
|
+
)
|
|
179
|
+
lines.append("")
|
|
180
|
+
return lines
|
|
181
|
+
|
|
182
|
+
if isinstance(event, DraftCompletedEvent):
|
|
183
|
+
lines = format_draft_completed_event(event=event)
|
|
184
|
+
lines.append("")
|
|
185
|
+
if published.snapshot.draft is not None:
|
|
186
|
+
lines.extend(self._format_build_sheet(published=published))
|
|
187
|
+
return lines
|
|
188
|
+
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
def _format_build_sheet(self, *, published: LiveSessionEvent) -> list[str]:
|
|
192
|
+
result = published.snapshot.build
|
|
193
|
+
if result is None:
|
|
194
|
+
error = next(
|
|
195
|
+
(
|
|
196
|
+
error
|
|
197
|
+
for error in published.snapshot.errors
|
|
198
|
+
if error.operation == OperationKind.BUILD
|
|
199
|
+
),
|
|
200
|
+
None,
|
|
201
|
+
)
|
|
202
|
+
if error is None:
|
|
203
|
+
raise DeckBuilderError("Deck build did not return a result.")
|
|
204
|
+
|
|
205
|
+
message = error.message.removeprefix("Deck build failed: ")
|
|
206
|
+
raise DeckBuilderError(message)
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
result.domain_pool is None
|
|
210
|
+
or result.domain_selection is None
|
|
211
|
+
or result.domain_spell_selection is None
|
|
212
|
+
or result.domain_mana_base is None
|
|
213
|
+
):
|
|
214
|
+
raise DeckBuilderError("Deck build did not include domain details.")
|
|
215
|
+
|
|
216
|
+
pool = replace(
|
|
217
|
+
result.domain_pool,
|
|
218
|
+
source_label=(
|
|
219
|
+
f"watch {result.domain_pool.account_id}/"
|
|
220
|
+
f"{result.domain_pool.draft_id}"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
return format_build_result(
|
|
224
|
+
pool=pool,
|
|
225
|
+
selection=result.domain_selection,
|
|
226
|
+
spell_selection=result.domain_spell_selection,
|
|
227
|
+
mana_base=result.domain_mana_base,
|
|
228
|
+
).rstrip("\n").splitlines()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def run_plain_watch(
|
|
232
|
+
*,
|
|
233
|
+
log_path: PathInput,
|
|
234
|
+
card_database: CardDatabase,
|
|
235
|
+
app_dir: PathInput | None = None,
|
|
236
|
+
output: TextIO | None = None,
|
|
237
|
+
poll_interval: float = POLL_INTERVAL_SECONDS,
|
|
238
|
+
once: bool = False,
|
|
239
|
+
startup_scan: bool = False,
|
|
240
|
+
stop_after_empty_polls: int | None = None,
|
|
241
|
+
ratings_loader: RatingsLoader | None = None,
|
|
242
|
+
splash_enabled: bool = True,
|
|
243
|
+
) -> int:
|
|
244
|
+
"""Run watch --plain until interrupted or a test stop condition fires.
|
|
245
|
+
The process returns zero unless a caller catches and maps an exception.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
if output is None:
|
|
249
|
+
output = sys.stdout
|
|
250
|
+
|
|
251
|
+
watcher = PlainLogWatcher(
|
|
252
|
+
log_path=log_path,
|
|
253
|
+
card_database=card_database,
|
|
254
|
+
app_dir=app_dir,
|
|
255
|
+
poll_interval=poll_interval,
|
|
256
|
+
ratings_loader=ratings_loader,
|
|
257
|
+
splash_enabled=splash_enabled,
|
|
258
|
+
)
|
|
259
|
+
output.write("Draft Omen watch\n")
|
|
260
|
+
output.write(f"Watching: {watcher.log_path}\n")
|
|
261
|
+
output.write("Mode: plain-text\n\n")
|
|
262
|
+
output.flush()
|
|
263
|
+
|
|
264
|
+
if startup_scan:
|
|
265
|
+
_write_if_present(output=output, text=watcher.scan_startup_files())
|
|
266
|
+
|
|
267
|
+
if once:
|
|
268
|
+
_write_if_present(output=output, text=watcher.poll_once())
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
empty_polls = 0
|
|
272
|
+
while True:
|
|
273
|
+
text = watcher.poll_once()
|
|
274
|
+
if text:
|
|
275
|
+
_write_if_present(output=output, text=text)
|
|
276
|
+
empty_polls = 0
|
|
277
|
+
else:
|
|
278
|
+
empty_polls += 1
|
|
279
|
+
|
|
280
|
+
if stop_after_empty_polls is not None and empty_polls >= stop_after_empty_polls:
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
time.sleep(poll_interval)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _write_if_present(*, output: TextIO, text: str) -> None:
|
|
287
|
+
if not text:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
output.write(text)
|
|
291
|
+
output.flush()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _account_label(*, published: LiveSessionEvent, account_id: str | None) -> str:
|
|
295
|
+
if account_id is None:
|
|
296
|
+
return "unknown"
|
|
297
|
+
|
|
298
|
+
identity = next(
|
|
299
|
+
(
|
|
300
|
+
account
|
|
301
|
+
for account in published.snapshot.accounts
|
|
302
|
+
if account.account_id == account_id
|
|
303
|
+
),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
if identity is None or identity.screen_name is None:
|
|
307
|
+
return account_id
|
|
308
|
+
|
|
309
|
+
return f"{identity.screen_name} ({account_id})"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _data_source_from_pack_lines(*, lines: list[str]) -> str:
|
|
313
|
+
for line in lines:
|
|
314
|
+
if line.startswith("Data source: "):
|
|
315
|
+
return line.removeprefix("Data source: ")
|
|
316
|
+
|
|
317
|
+
return "unknown"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _color_status_from_pack_lines(*, lines: list[str]) -> str:
|
|
321
|
+
for line in lines:
|
|
322
|
+
if line.startswith("Status: inferred pair "):
|
|
323
|
+
return line.removeprefix("Status: ")
|
|
324
|
+
|
|
325
|
+
return "inferred pair open, commitment 0% (open), pool unknown"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _pack_lines_without_color_status(*, lines: list[str]) -> list[str]:
|
|
329
|
+
return [line for line in lines if not line.startswith("Status: inferred pair ")]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _join_output_lines(*, lines: list[str]) -> str:
|
|
333
|
+
if not lines:
|
|
334
|
+
return ""
|
|
335
|
+
|
|
336
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: draftomen
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: An unofficial Quick Draft assistant for MTG Arena
|
|
5
|
+
Author: Andrea Grandi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/andreagrandi/draftomen
|
|
8
|
+
Project-URL: Repository, https://github.com/andreagrandi/draftomen
|
|
9
|
+
Project-URL: Issues, https://github.com/andreagrandi/draftomen/issues
|
|
10
|
+
Keywords: draft,magic-the-gathering,mtg-arena,terminal,tui
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Games/Entertainment
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyside6<7,>=6.9
|
|
24
|
+
Requires-Dist: textual>=8.2.8
|
|
25
|
+
Requires-Dist: textual-image>=0.13.2
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="https://raw.githubusercontent.com/andreagrandi/draftomen/master/docs/assets/draftomen_banner.png" alt="Draft Omen banner" width="100%">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
# Draft Omen
|
|
33
|
+
|
|
34
|
+
Draft Omen is an unofficial desktop draft assistant for MTG Arena Quick Drafts. Its PySide6/QML application reads Arena's local `Player.log`, recognizes each pack and pick, and ranks the available cards while you draft. When the draft is complete, it also suggests a 40-card deck from your pool. A terminal interface with the same draft behavior is available as `draftomen-tui`.
|
|
35
|
+
|
|
36
|
+
Draft Omen is read-only: it does not write to, inject into, or automate MTG Arena. Card details come from [Scryfall](https://scryfall.com/) and draft statistics come from [17Lands](https://www.17lands.com/).
|
|
37
|
+
|
|
38
|
+
## Screenshots
|
|
39
|
+
|
|
40
|
+
### Live Draft GUI — pick recommendations
|
|
41
|
+
|
|
42
|
+
The Live Draft view shows Pack 1, Pick 3 with 12 cards available and Glóin the Mighty selected. Its DO Score, 17Lands win rate, grade, and color fit appear alongside focused-card details and a pool summary.
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
### Suggested deck GUI
|
|
47
|
+
|
|
48
|
+
After the draft, the Suggested deck view shows an automatic UR pair and a 40-card build with 23 spells, 17 lands, a mana base and curve, grouped main-deck spells, and selected-card details.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
## How recommendations work
|
|
53
|
+
|
|
54
|
+
Draft Omen keeps the data behind every recommendation visible instead of presenting a black-box pick order:
|
|
55
|
+
|
|
56
|
+
- **17Lands ratings:** each card shows its Games-in-Hand win rate (`17L WR`) and a 17Lands-style grade. Quick Draft data is preferred; Premier Draft data is used as a fallback when Quick Draft samples are missing or too small.
|
|
57
|
+
- **DO Score:** the default 0-100 ranking normalizes 17Lands win rates across the set. A card without a reliable sample starts from a neutral score of 50, adjusted by its Average Last Seen At (`ALSA`) when available. The five freely available basic lands instead score 0 and rank after draftable cards.
|
|
58
|
+
- **Color fit:** early picks stay open. From pick 6 onward, scores gradually favor the colors supported by the drafted pool; from pick 16, on-color cards receive the full bonus and off-color cards the full penalty. Strong picks influence the inferred color pair more than filler.
|
|
59
|
+
- **Close early picks:** when two cards have similar scores, set- and format-specific 17Lands color-pair win rates can break the tie without forcing an early commitment.
|
|
60
|
+
- **Conservative splashing:** an `A`-range, single-pip card can be marked as a visible third-color splash when it is materially better than the on-color choices and the pool can support its mana. Draft Omen limits this to one extra color and two cards.
|
|
61
|
+
- **Transparent alternatives:** press `s` to compare rankings by DO Score, raw 17Lands win rate, ALSA, or mana value. The interface also identifies neutral-prior and Premier-fallback rows.
|
|
62
|
+
- **Deck suggestion:** the builder evaluates two-color pairs using card quality and 17Lands pair performance, then chooses spells and lands while considering creatures, curve, colored mana requirements, and cached 17Lands deck-structure targets when available.
|
|
63
|
+
|
|
64
|
+
DO Score is the default because it performed better than raw 17Lands win rate in Draft Omen's offline pick benchmarks. It is still guidance rather than a perfect pick order: public statistics reflect the decks, players, and contexts in which cards were played.
|
|
65
|
+
|
|
66
|
+
For the complete methodology, see [pick scoring](docs/pick-scoring.md), [benchmarking](docs/benchmarking.md), and the [deck builder](docs/deck-builder.md).
|
|
67
|
+
|
|
68
|
+
## How to use it
|
|
69
|
+
|
|
70
|
+
### 1. Install
|
|
71
|
+
|
|
72
|
+
On macOS, install Draft Omen with Homebrew:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
brew install andreagrandi/tap/draftomen
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
On other platforms, [install uv](https://docs.astral.sh/uv/getting-started/installation/) and run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv tool install draftomen
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Enable Arena logs
|
|
85
|
+
|
|
86
|
+
In MTG Arena, open **Settings → Account**, enable **Detailed Logs (Plugin Support)**, and restart Arena.
|
|
87
|
+
|
|
88
|
+
### 3. Start Draft Omen
|
|
89
|
+
|
|
90
|
+
Start Draft Omen before entering a Quick Draft:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
draftomen
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The PySide6/QML desktop application loads card metadata when needed, watches
|
|
97
|
+
Arena's standard log location, detects the set, and follows the draft
|
|
98
|
+
automatically. If 17Lands ratings are not cached for that set, it offers to
|
|
99
|
+
download them.
|
|
100
|
+
|
|
101
|
+
### Terminal interface
|
|
102
|
+
|
|
103
|
+
For a terminal workflow, use the stable `draftomen-tui` command. It preserves
|
|
104
|
+
the watch, replay, build, backtest, benchmark, and data-refresh subcommands,
|
|
105
|
+
including plain-text mode:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
draftomen-tui
|
|
109
|
+
draftomen-tui watch --plain
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use the arrow keys or `j`/`k` to browse cards, `s` to change the ranking,
|
|
113
|
+
`b` to open the current build, `c` to configure the view and optional splash
|
|
114
|
+
recommendations, and `q` to quit.
|
|
115
|
+
|
|
116
|
+
### Visual development
|
|
117
|
+
|
|
118
|
+
For deterministic visual development, launch the explicit forced-mock entry
|
|
119
|
+
point:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
draftomen-gui-mockup
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For an automated, non-interactive GUI smoke check, use the default command
|
|
126
|
+
with its mock provider and bounded smoke flag:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
QT_QPA_PLATFORM=offscreen draftomen --provider mock --smoke-test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
See the [desktop GUI guide](docs/gui-mockup.md) for live and mock launch
|
|
133
|
+
options, selectable states, and responsive review targets. For reproducible
|
|
134
|
+
unsigned development bundles, see the [desktop bundle guide](docs/desktop-bundles.md).
|
|
135
|
+
|
|
136
|
+
Live recommendations currently support Quick Draft. Windows support is best-effort.
|
|
137
|
+
|
|
138
|
+
## Local draft audit data
|
|
139
|
+
|
|
140
|
+
Live sessions started with `draftomen-tui`, including `watch --plain`, keep an
|
|
141
|
+
independent, append-only JSONL audit log for every draft under
|
|
142
|
+
`~/.draftomen/audit/drafts/<account-id>/<draft-id>.jsonl`. Each record includes
|
|
143
|
+
the offered pack, pool snapshot, ratings source, scoring configuration, complete
|
|
144
|
+
candidate calculations, splash eligibility and mana-source reasoning, all
|
|
145
|
+
supported rankings, the recommendation visible when the pick was made, and the
|
|
146
|
+
card actually chosen. Stable record IDs prevent
|
|
147
|
+
startup scans and log rotation from rewriting prior evidence.
|
|
148
|
+
|
|
149
|
+
Offline `replay` and backtest commands do not write audit records. For the schema
|
|
150
|
+
and retention details, see [draft audit logging](docs/draft-audit-log.md).
|
|
151
|
+
|
|
152
|
+
## Branding and compliance
|
|
153
|
+
|
|
154
|
+
Draft Omen is not affiliated with, sponsored by, approved by, or endorsed by Wizards of the Coast, Scryfall, or 17Lands.
|
|
155
|
+
|
|
156
|
+
Draft Omen is unofficial Fan Content permitted under the Fan Content Policy. Not approved/endorsed by Wizards. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC. Card data from Scryfall and 17Lands; neither service endorses this tool.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
draftomen/__init__.py,sha256=USYipFyFaVtpkSZILaGYIqL88qSKyZ0PJxzj44YZpdg,608
|
|
2
|
+
draftomen/audit.py,sha256=IXqU_OoxKbENVG0UvwP2iM24QrJjcNb6tNZpDqaq4Dg,19803
|
|
3
|
+
draftomen/backtest.py,sha256=1A8jm4nnFOGEX0lPdFtphzXLNNGoTmgBKYpeeU-WNo8,13177
|
|
4
|
+
draftomen/benchmark.py,sha256=j45GaKwrwA4wAINGPTTw6I1UWsSFcBJUvrwTLJSa37Q,24449
|
|
5
|
+
draftomen/carddb.py,sha256=ZMswc9fYAWT5lzpIHeDwUNPCUjcm4K_whHWJBV-YqdM,57641
|
|
6
|
+
draftomen/cardimages.py,sha256=CF0OjYiMmtp_vFFxlNP05gckS99wXKswJMYR1Il-XXE,11101
|
|
7
|
+
draftomen/cli.py,sha256=jJW0rCM4E2ubuU9_ztvdX9YuTJJ6cknPRvLWD693Lcw,28136
|
|
8
|
+
draftomen/config.py,sha256=1Y9eNkUVFPryORdpwV88zD62sEBrpMAC6Oxy2V7Ud4g,4321
|
|
9
|
+
draftomen/deckbuilder.py,sha256=4BKKpdY0j3hV2z5FRlKXtcNo70v_8RJ3vCfF_LD-QcE,81979
|
|
10
|
+
draftomen/events.py,sha256=-KJRAUGnzUDaEsIlTKmJTUJq2WzRZuw8cN2Egbjkz8I,22223
|
|
11
|
+
draftomen/logfollow.py,sha256=uNIojOTWVAYzRY_NSG3CxhiVU7wQFjb7mCreoJUR8Rk,14631
|
|
12
|
+
draftomen/mock_session.py,sha256=6vMiYSbDQQOwkCGkgjxtKQku4T_j0kbWtrtq6lX2lts,22737
|
|
13
|
+
draftomen/paths.py,sha256=O0OlBU3FLKortvtpzK0T-TZ9FdMMQrLpmfqk5U1lKdQ,3322
|
|
14
|
+
draftomen/pickengine.py,sha256=7B_RUX4rwS5KayI6NkWYpP782nNtsW8_qHYBSG981SU,32891
|
|
15
|
+
draftomen/pool.py,sha256=oypWhQvWz3OigtPE1j3WoCnxodoU5JDvXzzp7r1Na_A,41046
|
|
16
|
+
draftomen/preferences.py,sha256=AvyGrrVweTgrilNnEwQt8Dq9Dj7VWrJJ7ZE8z8yzF_s,9427
|
|
17
|
+
draftomen/qt_adapter.py,sha256=W4quF7-qw4QLWlrlqGou3CWgB4YjG5gBWWjsWqyO7Dg,29754
|
|
18
|
+
draftomen/qt_gui.py,sha256=gu6ViXFumsxJrLdfJ23ktwC9EVihoADq8TA5ek_GrWg,10936
|
|
19
|
+
draftomen/qt_mock.py,sha256=3OkUfSbQYu6cuBweWvh5b2JZvLTNuSoGoaKAVBs0zws,1825
|
|
20
|
+
draftomen/ranking.py,sha256=AD4egfWNMXVFK4-LVZxMHoYZtQyflD24KPuswRupqdI,3371
|
|
21
|
+
draftomen/replay.py,sha256=dun5F_1Em58uZ_MArkJcwd1PHIxH7ZRbiP3HhffDn_4,14324
|
|
22
|
+
draftomen/session.py,sha256=lfAtpi1yPU3_1W9Wl6_lhboJ-OOltgNizimAO4lFo_I,130801
|
|
23
|
+
draftomen/setinfo.py,sha256=7xoOfYpBPK-fo0BV0-IkvCVx4mMsXALwFAJY-Qd8020,654
|
|
24
|
+
draftomen/seventeen.py,sha256=xXIkYWCujD9QXIw8r5cdFHqtP2U3WzbmgkH9b3cTQso,87441
|
|
25
|
+
draftomen/splash.py,sha256=Z0Ft8HmPC_xXtGs9yc-tCC6WrcwBn0i8QxZXMmfd9xw,18736
|
|
26
|
+
draftomen/tui.py,sha256=RlkZOrT1qCVuc1V7GJXi_oJhyVJG519SnKR11wvcImE,134053
|
|
27
|
+
draftomen/watch.py,sha256=EVFpEK2dpy7EPjaAO65BfTpIqOeehwRrk5Ght8OQxUQ,10942
|
|
28
|
+
draftomen/assets/draftomen.icns,sha256=EmMNL-ZKAV5f43F_ABtcrrAw21Y4wd7j0FsXRgS5qVY,994218
|
|
29
|
+
draftomen/assets/draftomen.ico,sha256=8f-sLKQFP0AufFAv_Cchft_kzbg8w-CTqy6D-CY_RZ8,130414
|
|
30
|
+
draftomen/assets/draftomen_logo.png,sha256=FMv_XxOg1Jst0htu1yy3j_AnJgNFsCEgOgE74_P81Io,1613066
|
|
31
|
+
draftomen/qml/AboutDialog.qml,sha256=swZBXKYnUPlwHxfWMOLpP-_WBipTatVT54RRLIiNltc,4692
|
|
32
|
+
draftomen/qml/AppBar.qml,sha256=XgjiXtMi2c4JyIjGL5Ar9S6bYq7RXNpJS1Y525WnWzQ,3940
|
|
33
|
+
draftomen/qml/BacktestView.qml,sha256=42VaEtgUgPCGgOyzrFhrr_DRKsmv9zVAJVjovqVrRRs,17209
|
|
34
|
+
draftomen/qml/BuildView.qml,sha256=i4JS6Su1qJ9xCdwkEEZ4bdHk8YFIXEZfCXKxrE0O2zs,56161
|
|
35
|
+
draftomen/qml/CardPreview.qml,sha256=Krz68J1Cyj8vop-Edh_x6aivajpz_91FQNkP2Mf9q7M,16830
|
|
36
|
+
draftomen/qml/DimensionalButton.qml,sha256=b4mcHZA--2Y2-4tISZ05XqAeKWnRSYvZdCY90paAgCk,1258
|
|
37
|
+
draftomen/qml/DimensionalComboBox.qml,sha256=QYsvCnfVv-QSSvLu27vxrJb2xGvlNDmiTdMw5cR-GFY,6022
|
|
38
|
+
draftomen/qml/DimensionalSurface.qml,sha256=RZMy3J-5-9SPymjETK1i13iP8O1F6Z6gzcu9LLJTEto,3989
|
|
39
|
+
draftomen/qml/DimensionalTabButton.qml,sha256=I7Fk8bYrZhXpOEBe8OlOGZ3mS2ryYUWu243vRkGPnCA,1205
|
|
40
|
+
draftomen/qml/LiveDraftView.qml,sha256=83BP3JRk4SG7XFs16LOuZe-IkGTqbXYVAUN5Xegs3jk,18908
|
|
41
|
+
draftomen/qml/Main.qml,sha256=c2dTfNoneRPSwbZNfuWRlLRBJDKvvse0CxwFgX9E3Rw,5894
|
|
42
|
+
draftomen/qml/NavigationRail.qml,sha256=hNXPsxj7VdG1zY8YiuCCejoFdVR_2zN1Q6zaqViD-dA,8749
|
|
43
|
+
draftomen/qml/PoolSummaryPanel.qml,sha256=dbCWwxD-xVAne9zbahb7p2x2m6BLDK7OGopSeanqvi0,11571
|
|
44
|
+
draftomen/qml/PrivacyDialog.qml,sha256=pDIO5HsFF7B6uHi8Z6n-PzpDs59p92PEvuwuT0smmZA,2610
|
|
45
|
+
draftomen/qml/RecentPickThumbnail.qml,sha256=zdJTKAkNcWdU-SNyGkd1WS9EMYh5kdhzbHS3IaefOGI,2719
|
|
46
|
+
draftomen/qml/RecentPicksGallery.qml,sha256=zlPfxBWsB1tnTM0GS1q_C53oiydMmDnJYy4hPSMeM_A,10705
|
|
47
|
+
draftomen/qml/RecommendationRow.qml,sha256=3HsvplhIqxhsc-hbSsq7Ur6K67C-hHPxYzMAtoiuJlo,15093
|
|
48
|
+
draftomen/qml/SettingsSwitch.qml,sha256=y6caW0kq_xxtKC5IsQH67aoAMwG_uR5NWOlHvKHF_4o,5320
|
|
49
|
+
draftomen/qml/SettingsView.qml,sha256=OD_XYAsfTANNJmyJmjmIMhsnIYo4tV-1KfoAwsOVa4M,25987
|
|
50
|
+
draftomen/qml/StateBanner.qml,sha256=GHJjMXpxVu6L5SRWFB5a_-ndIHZbWL2GYmKq6fnZjjA,6443
|
|
51
|
+
draftomen/qml/StatusStrip.qml,sha256=AsqyWcCTuEs3sZlbJvdw5raSsYdP46yw0Wr-B4jGVM8,2446
|
|
52
|
+
draftomen/qml/Theme.qml,sha256=uq8ZOO_dg6ycPYSySk0Thp8a5YJVoSoJwZ0GYqGXoTg,2783
|
|
53
|
+
draftomen/qml/qmldir,sha256=xcXbi_ZXRxDGstJ8tAyeRvEAArkXoODXch-MrBZWVxI,810
|
|
54
|
+
draftomen-0.3.0.dist-info/licenses/LICENSE,sha256=CAbXVt-kglXwH27kdm-0fJMnAjrvdPBxFjC_ZC-1TBY,1070
|
|
55
|
+
draftomen-0.3.0.dist-info/METADATA,sha256=WTNdkSAwhNYcqnFRbbIFX0ivbl35R6BJenZFjiSAsh8,8182
|
|
56
|
+
draftomen-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
57
|
+
draftomen-0.3.0.dist-info/entry_points.txt,sha256=FqGt9cbwb7-C2LC0D8-TA41NJrWnlNSGEOYk02KBOmc,148
|
|
58
|
+
draftomen-0.3.0.dist-info/top_level.txt,sha256=2itiKkfjeHhMwCsocHw6TEuFXfICgYIseDZs7pQK7kw,10
|
|
59
|
+
draftomen-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrea Grandi
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
draftomen
|