@imperialbower/bowery 0.0.0 → 0.1.0

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.
@@ -0,0 +1,8 @@
1
+ 775ebac50d3042c8c90b52418414588ea1b7b433fbc854899e908b6f8ec92158 LICENSE-APACHE
2
+ a5fb933e5d0221688f909047fbe29b70b11cb25d5cb9a08e59a269df7ee15266 LICENSE-MIT
3
+ d15cfb86c381c17f40bcae4977098e77aa74caf1cad1f49cf409c1c89e3702cd README.md
4
+ a9224c7609958d7222e9dcf3718020873e05c56535c24e09c21a71587f2170ae bowery.css
5
+ 69b0d329338f15d473a4fb3920863948dc8f92fba01857882dade8752ebc5eec bowery.js
6
+ 47cd091c2d631f544ba4c9c6ae902b338773dceec3deb42571f40b19b32acf80 bowery_wasm.js
7
+ b8412512d47cb9fdbbeb8c434d0e93d5b69b65f851eb6c91318c2dca90f68c03 bowery_wasm_bg.wasm
8
+ 39b49a99faf77e9dde1703dfad704b28c90f88587dd0e6d7ae6197676759cbb5 package.json
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![Crates.io](https://img.shields.io/crates/v/bowery.svg)](https://crates.io/crates/bowery)
2
+ [![npm](https://img.shields.io/npm/v/@imperialbower/bowery.svg)](https://www.npmjs.com/package/@imperialbower/bowery)
2
3
  [![Build and Test](https://github.com/ImperialBower/bowery/actions/workflows/ci.yaml/badge.svg)](https://github.com/ImperialBower/bowery/actions/workflows/ci.yaml)
3
4
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md)
4
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE-MIT)
@@ -20,18 +21,61 @@ crate builds two ways: an rlib kernel (config, check, game, frame, SVG
20
21
  drawing) and, behind a `wasm` feature, the browser element. The design is
21
22
  bower's [EPIC-20](https://github.com/ImperialBower/bower/blob/main/docs/EPIC-20_Bowery.md).
22
23
 
23
- This repository is freshly scaffolded: the crate holds no table code yet.
24
+ ## In a web page
24
25
 
25
- ## How to use
26
+ ```html
27
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@imperialbower/bowery@0.1.0/bowery.js"></script>
28
+
29
+ <bowery-table>
30
+ <script type="application/yaml">
31
+ mode: play
32
+ seat: 0
33
+ table: { stakes: { small_blind: 50.0, big_blind: 100.0 } }
34
+ players:
35
+ - { seat: 0, name: You, stack: 10000 }
36
+ - { seat: 1, stack: 10000, bot: tight_aggressive }
37
+ - { seat: 2, stack: 10000, bot: maniac }
38
+ </script>
39
+ <p>A poker table: it needs JavaScript.</p> <!-- the fallback, kept until the table is ready -->
40
+ </bowery-table>
41
+ ```
42
+
43
+ The element loads its wasm from beside `bowery.js` (or from its `base`
44
+ attribute) and asks nothing of any other origin. To serve it yourself, take
45
+ `bowery-vX.Y.Z.tar.gz` from a [release](https://github.com/ImperialBower/bowery/releases):
46
+ one flat directory whose files `MANIFEST.sha256` lists. A config can also
47
+ come from a file (`src="table.yaml"`), and `mode: replay` or
48
+ `mode: interactive` take a recorded pkcore hand history as `hand:`. The
49
+ config's fields are documented on [`Config`](https://docs.rs/bowery/latest/bowery/config/struct.Config.html).
50
+
51
+ ## In Rust
52
+
53
+ ```rust
54
+ use bowery::check::check;
55
+ use bowery::config::{Config, View};
56
+ use bowery::draw::{Theme, svg};
57
+ use bowery::game::Game;
58
+
59
+ let config = Config::from_yaml(yaml)?;
60
+ let game = Game::new(check(&config).map_err(|problems| problems[0].clone())?)?;
61
+ let picture = svg(&game.frame(View::All), config.layout, 680, Theme::Light);
62
+ ```
63
+
64
+ `cargo run --example bowery-still -- fixtures/the_hand.yaml --layout spotlight --at river`
65
+ prints The Hand on the river as one SVG.
66
+
67
+ ## Develop
26
68
 
27
69
  ```sh
28
70
  git clone https://github.com/ImperialBower/bowery.git
29
71
  cd bowery
30
- make install-tools # cargo-deny, cargo-audit, cargo-watch
31
- make ayce # the full gate: clean, fmt, build, test, lint, security-scan, docs
72
+ make install-tools # the wasm target, cargo-deny, cargo-audit, cargo-watch
73
+ make ayce # the full gate, the wasm size gate and the Playwright proof included
74
+ make serve # the demo at http://localhost:8000
32
75
  ```
33
76
 
34
- `make help` lists every target.
77
+ `make ayce` also needs `wasm-pack` and Node (see `.tool-versions`). `make help`
78
+ lists every target.
35
79
 
36
80
  ## What's in the box
37
81
 
package/bowery.css ADDED
@@ -0,0 +1,73 @@
1
+ /* <bowery-table>: loaded into the element's shadow root, so nothing here
2
+ reaches the page. Black on white, 2 px strokes, the system monospace;
3
+ :host([data-dark]) when the page is dark. No fonts, no url(). */
4
+ :host {
5
+ --bowery-bg: #ffffff;
6
+ --bowery-fg: #000000;
7
+ --bowery-to-act: #ffe94d;
8
+ display: block;
9
+ max-width: 100%;
10
+ border: 2px solid var(--bowery-fg);
11
+ background: var(--bowery-bg);
12
+ color: var(--bowery-fg);
13
+ font-family: ui-monospace, Menlo, Consolas, monospace;
14
+ font-size: 13px;
15
+ }
16
+ :host([data-dark]) {
17
+ --bowery-bg: #111111;
18
+ --bowery-fg: #f2f2f2;
19
+ --bowery-to-act: #5e5200;
20
+ }
21
+ :host(:focus-visible) {
22
+ outline: 3px solid var(--bowery-to-act);
23
+ outline-offset: 2px;
24
+ }
25
+ .stage svg {
26
+ display: block;
27
+ max-width: 100%;
28
+ height: auto;
29
+ }
30
+ .bar {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: 6px;
34
+ align-items: center;
35
+ border-top: 2px solid var(--bowery-fg);
36
+ padding: 6px 8px;
37
+ }
38
+ .bar:empty {
39
+ display: none;
40
+ }
41
+ button,
42
+ select,
43
+ input {
44
+ font: inherit;
45
+ color: var(--bowery-fg);
46
+ background: var(--bowery-bg);
47
+ border: 2px solid var(--bowery-fg);
48
+ border-radius: 0;
49
+ padding: 2px 8px;
50
+ }
51
+ button {
52
+ font-weight: 700;
53
+ cursor: pointer;
54
+ }
55
+ button:hover,
56
+ button:focus-visible {
57
+ background: var(--bowery-fg);
58
+ color: var(--bowery-bg);
59
+ }
60
+ input[type='number'] {
61
+ width: 9ch;
62
+ }
63
+ .readout {
64
+ flex: 1;
65
+ min-width: 8ch;
66
+ }
67
+ .status {
68
+ position: absolute;
69
+ width: 1px;
70
+ height: 1px;
71
+ overflow: hidden;
72
+ clip-path: inset(50%);
73
+ }
package/bowery.js ADDED
@@ -0,0 +1,413 @@
1
+ // <bowery-table>: one poker table control, configured not compiled.
2
+ //
3
+ // Usage:
4
+ // <script type="module" src="bowery/bowery.js"></script>
5
+ // <bowery-table base="bowery/">
6
+ // <script type="application/yaml">mode: replay …</script>
7
+ // <svg …>the fallback picture</svg>
8
+ // </bowery-table>
9
+ //
10
+ // Nothing loads until the element is within a screen of the viewport. The
11
+ // first element instantiates the wasm module; every later one reuses it and
12
+ // builds its own BoweryTable, so tables on a page are independent. The
13
+ // fallback picture stays if the config, the import or the init fails, and a
14
+ // config's problems are printed under it. Every request goes to `base`
15
+ // (default: this module's directory) or to `src`, both on the page's origin.
16
+ // Contract: bower docs/EPIC-20_Bowery.md § <bowery-table>.
17
+
18
+ const HERE = new URL('.', import.meta.url);
19
+ const GLUE = 'bowery_wasm.js';
20
+ const WASM = 'bowery_wasm_bg.wasm';
21
+ const STYLE = 'bowery.css';
22
+
23
+ /** One import and one init per base, shared by every element. */
24
+ const modules = new Map();
25
+
26
+ function loadModule(base) {
27
+ const key = base.href;
28
+ if (!modules.has(key)) {
29
+ modules.set(
30
+ key,
31
+ import(new URL(GLUE, base).href).then(async (glue) => {
32
+ await glue.default({ module_or_path: new URL(WASM, base) });
33
+ return glue;
34
+ }),
35
+ );
36
+ }
37
+ return modules.get(key);
38
+ }
39
+
40
+ /** mdBook's dark themes, or the system's dark preference. */
41
+ function prefersDark() {
42
+ const html = document.documentElement.classList;
43
+ if (html.contains('coal') || html.contains('navy') || html.contains('ayu')) return true;
44
+ if (html.contains('light') || html.contains('rust')) return false;
45
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
46
+ }
47
+
48
+ /** A button; `name` is its accessible name when the label is a symbol. */
49
+ function button(label, onClick, name) {
50
+ const element = document.createElement('button');
51
+ element.type = 'button';
52
+ element.textContent = label;
53
+ if (name) {
54
+ element.setAttribute('aria-label', name);
55
+ element.title = name;
56
+ }
57
+ element.addEventListener('click', onClick);
58
+ return element;
59
+ }
60
+
61
+ class BoweryTableElement extends HTMLElement {
62
+ static get observedAttributes() {
63
+ return ['src'];
64
+ }
65
+
66
+ connectedCallback() {
67
+ if (this._wired) return;
68
+ this._wired = true;
69
+ this._base = new URL(this.getAttribute('base') || HERE.href, document.baseURI);
70
+ if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
71
+ const observer = new IntersectionObserver(
72
+ (entries) => {
73
+ if (entries.some((entry) => entry.isIntersecting)) {
74
+ observer.disconnect();
75
+ this._start();
76
+ }
77
+ },
78
+ { rootMargin: '100% 0px' },
79
+ );
80
+ observer.observe(this);
81
+ }
82
+
83
+ disconnectedCallback() {
84
+ clearTimeout(this._timer);
85
+ this._resize?.disconnect();
86
+ }
87
+
88
+ attributeChangedCallback(name, before, after) {
89
+ if (name === 'src' && this._table && before !== after) this._reload();
90
+ }
91
+
92
+ async _reload() {
93
+ clearTimeout(this._timer);
94
+ this._table?.free?.();
95
+ this._table = null;
96
+ await this._start();
97
+ }
98
+
99
+ async _start() {
100
+ if (location.protocol === 'file:') {
101
+ this._fail('this table needs a served page');
102
+ return;
103
+ }
104
+ let yaml;
105
+ try {
106
+ yaml = await this._config();
107
+ } catch (error) {
108
+ this._fail(`the config did not load: ${error.message}`);
109
+ return;
110
+ }
111
+ let glue;
112
+ try {
113
+ glue = await loadModule(this._base);
114
+ } catch (error) {
115
+ this._fail('the table did not load');
116
+ this._emit('bowery:error', { message: String(error) });
117
+ return;
118
+ }
119
+ try {
120
+ this._table = new glue.BoweryTable(yaml);
121
+ } catch (error) {
122
+ this._problems(String(error));
123
+ return;
124
+ }
125
+ this._version = glue.BoweryTable.version();
126
+ this._mount();
127
+ this._render();
128
+ this._emit('bowery:ready', { version: this._version });
129
+ this._pace();
130
+ }
131
+
132
+ /** The inline YAML, or the file `src` names (same origin only). */
133
+ async _config() {
134
+ const src = this.getAttribute('src');
135
+ if (src) {
136
+ const url = new URL(src, document.baseURI);
137
+ if (url.origin !== location.origin) throw new Error('src must be on this page\'s origin');
138
+ const response = await fetch(url);
139
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
140
+ return response.text();
141
+ }
142
+ const script = this.querySelector('script[type="application/yaml"]');
143
+ if (!script) throw new Error('no <script type="application/yaml"> and no src');
144
+ return script.textContent;
145
+ }
146
+
147
+ /** The fallback stays; a note goes under it. */
148
+ _fail(message) {
149
+ const note = document.createElement('p');
150
+ note.className = 'bowery-note';
151
+ note.textContent = message;
152
+ this.append(note);
153
+ }
154
+
155
+ /** The fallback stays; the config's problems go under it. */
156
+ _problems(json) {
157
+ let lines;
158
+ try {
159
+ lines = JSON.parse(json).map((problem) => (problem.path ? `${problem.path}: ${problem.message}` : problem.message));
160
+ } catch {
161
+ lines = [json];
162
+ }
163
+ const pre = document.createElement('pre');
164
+ pre.className = 'bowery-problems';
165
+ pre.textContent = lines.join('\n');
166
+ this.append(pre);
167
+ this._emit('bowery:error', { problems: lines });
168
+ }
169
+
170
+ _mount() {
171
+ const root = this.shadowRoot || this.attachShadow({ mode: 'open' });
172
+ root.replaceChildren();
173
+ // A custom element is inline until bowery.css arrives; block at once, so
174
+ // the first frame is measured and drawn at the element's real width.
175
+ const block = document.createElement('style');
176
+ block.textContent = ':host { display: block; }';
177
+ const style = document.createElement('link');
178
+ style.rel = 'stylesheet';
179
+ style.href = new URL(STYLE, this._base).href;
180
+ this._stage = document.createElement('div');
181
+ this._stage.className = 'stage';
182
+ this._bar = document.createElement('div');
183
+ this._bar.className = 'bar';
184
+ this._status = document.createElement('div');
185
+ this._status.className = 'status';
186
+ this._status.setAttribute('aria-live', 'polite');
187
+ root.append(block, style, this._stage, this._bar, this._status);
188
+
189
+ this._width = this.clientWidth || 680;
190
+ this._resize = new ResizeObserver((entries) => {
191
+ const width = Math.round(entries[0].contentRect.width);
192
+ if (width > 0 && width !== this._width) {
193
+ this._width = width;
194
+ this._draw();
195
+ }
196
+ });
197
+ this._resize.observe(this);
198
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this._render());
199
+ new MutationObserver(() => this._render()).observe(document.documentElement, {
200
+ attributes: true,
201
+ attributeFilter: ['class'],
202
+ });
203
+ this.addEventListener('keydown', (event) => this._key(event));
204
+ }
205
+
206
+ _state() {
207
+ return JSON.parse(this._table.state());
208
+ }
209
+
210
+ _draw() {
211
+ const dark = prefersDark();
212
+ this.toggleAttribute('data-dark', dark);
213
+ // The SVG is bowery's own renderer's: every name and caption from the
214
+ // config is XML-escaped there (tests/draw.rs draw__names_are_escaped),
215
+ // and it holds no script.
216
+ this._stage.innerHTML = this._table.svg(this._width, dark);
217
+ }
218
+
219
+ _render() {
220
+ if (!this._table) return;
221
+ const before = this._last;
222
+ const state = this._state();
223
+ this._last = state;
224
+ this._draw();
225
+ this._controls(state);
226
+ this._status.textContent = state.caption;
227
+ this._emit('bowery:moment', { moment: state.moment - 1, moments: state.moments });
228
+ if (state.record === 'off' && before?.record !== 'off') this._emit('bowery:off-record', { since: state.since });
229
+ if (state.hand_over && !before?.hand_over) this._emit('bowery:hand-over', { result: state.result });
230
+ }
231
+
232
+ /** The bar under the table, rebuilt for the state; focus stays put. */
233
+ _controls(state) {
234
+ const focused = this.shadowRoot.activeElement?.dataset?.control;
235
+ const bar = this._bar;
236
+ bar.replaceChildren();
237
+ const add = (element, control) => {
238
+ element.dataset.control = control;
239
+ bar.append(element);
240
+ return element;
241
+ };
242
+ if (state.controls.seek) {
243
+ add(button('⏮', () => this._seek(0), 'first'), 'first');
244
+ add(button('◀', () => this._seek(state.moment - 2), 'back'), 'back');
245
+ add(button('▶', () => this._step(), 'next'), 'next');
246
+ add(button('⏭', () => this._seek(state.moments - 1), 'last'), 'last');
247
+ const readout = document.createElement('span');
248
+ readout.className = 'readout';
249
+ readout.textContent = `${state.moment} / ${state.moments}`;
250
+ add(readout, 'readout');
251
+ }
252
+ if (state.controls.take_seat && !state.hand_over) {
253
+ const select = document.createElement('select');
254
+ select.setAttribute('aria-label', 'seat');
255
+ for (const seat of state.seats.filter((seat) => seat.live)) {
256
+ const option = document.createElement('option');
257
+ option.value = seat.seat;
258
+ option.textContent = seat.name;
259
+ option.selected = seat.seat === state.reader;
260
+ select.append(option);
261
+ }
262
+ add(select, 'seat');
263
+ add(button('Take this seat', () => this._take(Number(select.value))), 'take');
264
+ }
265
+ if (state.controls.back_to_record) add(button('Back to the record', () => this._back()), 'back-to-record');
266
+ // Empty unless the reader can act at this moment.
267
+ if (state.legal.length > 0) this._actions(state, add);
268
+ if (state.controls.next_hand) add(button('Next hand', () => this._next()), 'next-hand');
269
+ if (state.controls.shuffle) add(button('Shuffle', () => this._shuffle()), 'shuffle');
270
+ if (state.controls.view) {
271
+ add(button(`👁 ${state.view}`, () => this._toggleView(state.view)), 'view');
272
+ }
273
+ if (state.controls.layout) {
274
+ add(button(`▦ ${state.layout}`, () => this._toggleLayout(state.layout)), 'layout');
275
+ }
276
+ const again = focused && bar.querySelector(`[data-control="${focused}"]`);
277
+ if (again) again.focus();
278
+ }
279
+
280
+ /** The reader's legal actions as buttons; a size box for bet or raise. */
281
+ _actions(state, add) {
282
+ for (const legal of state.legal) {
283
+ if (legal.action === 'bet' || legal.action === 'raise') {
284
+ const size = document.createElement('input');
285
+ size.type = 'number';
286
+ size.min = legal.min;
287
+ size.max = legal.max;
288
+ size.value = legal.min;
289
+ size.setAttribute('aria-label', `${legal.action} to`);
290
+ add(size, `${legal.action}-size`);
291
+ add(
292
+ button(legal.action === 'bet' ? 'Bet' : 'Raise to', () =>
293
+ this._act({ action: legal.action, amount: Number(size.value) }),
294
+ ),
295
+ legal.action,
296
+ );
297
+ } else {
298
+ const label = {
299
+ fold: 'Fold',
300
+ check: 'Check',
301
+ call: `Call ${legal.amount?.toLocaleString('en-US') ?? ''}`.trim(),
302
+ all_in: 'All in',
303
+ }[legal.action];
304
+ add(button(label, () => this._act({ action: legal.action })), legal.action);
305
+ }
306
+ }
307
+ }
308
+
309
+ _key(event) {
310
+ const state = this._last;
311
+ if (!state?.controls.seek || event.target !== this) return;
312
+ const moves = {
313
+ ArrowLeft: state.moment - 2,
314
+ ArrowRight: state.moment,
315
+ Home: 0,
316
+ End: state.moments - 1,
317
+ };
318
+ if (event.key in moves) {
319
+ event.preventDefault();
320
+ this._seek(moves[event.key]);
321
+ }
322
+ }
323
+
324
+ _seek(index) {
325
+ clearTimeout(this._timer);
326
+ this._table.seek(Math.max(0, index));
327
+ this._render();
328
+ }
329
+
330
+ /** ▶: one moment on; on the record it stops at the reader's turn. */
331
+ _step() {
332
+ this._try(() => this._table.advance());
333
+ this._pace();
334
+ }
335
+
336
+ _take(seat) {
337
+ this._try(() => this._table.take_seat(seat));
338
+ }
339
+
340
+ _back() {
341
+ clearTimeout(this._timer);
342
+ this._table.back_to_record();
343
+ this._render();
344
+ }
345
+
346
+ _act(action) {
347
+ this._try(() => this._table.act(JSON.stringify(action)));
348
+ this._pace();
349
+ }
350
+
351
+ _next() {
352
+ this._try(() => this._table.next_hand());
353
+ this._pace();
354
+ }
355
+
356
+ _shuffle() {
357
+ clearTimeout(this._timer);
358
+ this._try(() => this._table.shuffle((Math.random() * 0xffffffff) >>> 0));
359
+ this._pace();
360
+ }
361
+
362
+ _toggleView(view) {
363
+ this._table.set_view(view === 'all' ? 'seat' : 'all');
364
+ this._render();
365
+ }
366
+
367
+ _toggleLayout(layout) {
368
+ const order = ['ring', 'spotlight', 'ledger'];
369
+ this._table.set_layout(order[(order.indexOf(layout) + 1) % order.length]);
370
+ this._render();
371
+ }
372
+
373
+ _try(move) {
374
+ try {
375
+ move();
376
+ } catch (error) {
377
+ this._status.textContent = String(error);
378
+ this._emit('bowery:error', { message: String(error) });
379
+ }
380
+ this._render();
381
+ }
382
+
383
+ /**
384
+ * Off the record and in play, bots and deals go on by themselves, one
385
+ * moment per `delay_ms`, while the latest moment is on screen and it is
386
+ * not the reader's turn.
387
+ */
388
+ _pace() {
389
+ clearTimeout(this._timer);
390
+ const state = this._state();
391
+ const live = state.mode === 'play' || state.record === 'off';
392
+ if (!live || state.hand_over || state.moment !== state.moments) return;
393
+ this._timer = setTimeout(() => {
394
+ let answer;
395
+ try {
396
+ answer = this._table.advance();
397
+ } catch (error) {
398
+ this._emit('bowery:error', { message: String(error) });
399
+ return;
400
+ }
401
+ this._render();
402
+ if (answer === 'acted' || answer === 'dealt') this._pace();
403
+ }, state.delay_ms);
404
+ }
405
+
406
+ _emit(name, detail) {
407
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
408
+ }
409
+ }
410
+
411
+ if (!customElements.get('bowery-table')) customElements.define('bowery-table', BoweryTableElement);
412
+
413
+ export { BoweryTableElement };
package/bowery_wasm.js ADDED
@@ -0,0 +1,609 @@
1
+ /**
2
+ * One element's table, for JavaScript.
3
+ *
4
+ * # Examples
5
+ *
6
+ * ```
7
+ * use bowery::wasm::BoweryTable;
8
+ *
9
+ * let table = BoweryTable::new("
10
+ * mode: play
11
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
12
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
13
+ * ").unwrap();
14
+ * assert!(table.state().contains(r#""mode":"play""#));
15
+ * ```
16
+ */
17
+ export class BoweryTable {
18
+ __destroy_into_raw() {
19
+ const ptr = this.__wbg_ptr;
20
+ this.__wbg_ptr = 0;
21
+ BoweryTableFinalization.unregister(this);
22
+ return ptr;
23
+ }
24
+ free() {
25
+ const ptr = this.__destroy_into_raw();
26
+ wasm.__wbg_bowerytable_free(ptr, 0);
27
+ }
28
+ /**
29
+ * The reader acts: `{"action":"raise","amount":600}`.
30
+ *
31
+ * # Errors
32
+ *
33
+ * The JSON does not read, or the game refuses the action.
34
+ *
35
+ * # Examples
36
+ *
37
+ * ```
38
+ * use bowery::wasm::BoweryTable;
39
+ *
40
+ * let mut table = BoweryTable::new("
41
+ * mode: play
42
+ * table: { button: 0, stakes: { small_blind: 1.0, big_blind: 2.0 } }
43
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
44
+ * ").unwrap();
45
+ * assert!(table.act(r#"{"action":"call"}"#).is_ok());
46
+ * ```
47
+ * @param {string} action_json
48
+ */
49
+ act(action_json) {
50
+ const ptr0 = passStringToWasm0(action_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
51
+ const len0 = WASM_VECTOR_LEN;
52
+ const ret = wasm.bowerytable_act(this.__wbg_ptr, ptr0, len0);
53
+ if (ret[1]) {
54
+ throw takeFromExternrefTable0(ret[0]);
55
+ }
56
+ }
57
+ /**
58
+ * One step: `"acted"`, `"dealt"`, `"waiting"` or `"over"`.
59
+ *
60
+ * # Errors
61
+ *
62
+ * The engine failed.
63
+ *
64
+ * # Examples
65
+ *
66
+ * ```
67
+ * use bowery::wasm::BoweryTable;
68
+ *
69
+ * let mut table = BoweryTable::new("
70
+ * mode: play
71
+ * table: { button: 0, stakes: { small_blind: 1.0, big_blind: 2.0 } }
72
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
73
+ * ").unwrap();
74
+ * assert_eq!("waiting", table.advance().unwrap());
75
+ * ```
76
+ * @returns {string}
77
+ */
78
+ advance() {
79
+ let deferred2_0;
80
+ let deferred2_1;
81
+ try {
82
+ const ret = wasm.bowerytable_advance(this.__wbg_ptr);
83
+ var ptr1 = ret[0];
84
+ var len1 = ret[1];
85
+ if (ret[3]) {
86
+ ptr1 = 0; len1 = 0;
87
+ throw takeFromExternrefTable0(ret[2]);
88
+ }
89
+ deferred2_0 = ptr1;
90
+ deferred2_1 = len1;
91
+ return getStringFromWasm0(ptr1, len1);
92
+ } finally {
93
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
94
+ }
95
+ }
96
+ /**
97
+ * Back to the record at the takeover moment.
98
+ *
99
+ * # Examples
100
+ *
101
+ * ```
102
+ * use bowery::wasm::BoweryTable;
103
+ *
104
+ * let mut table = BoweryTable::new("
105
+ * mode: play
106
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
107
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
108
+ * ").unwrap();
109
+ * table.back_to_record();
110
+ * assert!(table.state().contains(r#""record":"none""#));
111
+ * ```
112
+ */
113
+ back_to_record() {
114
+ wasm.bowerytable_back_to_record(this.__wbg_ptr);
115
+ }
116
+ /**
117
+ * Reads, checks and builds a table from a YAML config.
118
+ *
119
+ * # Errors
120
+ *
121
+ * The config's problems as a JSON string (see
122
+ * [`crate::element::problems_json`]).
123
+ *
124
+ * # Examples
125
+ *
126
+ * ```
127
+ * use bowery::wasm::BoweryTable;
128
+ *
129
+ * let table = BoweryTable::new("
130
+ * mode: play
131
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
132
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
133
+ * ");
134
+ * assert!(table.is_ok());
135
+ * ```
136
+ * @param {string} config_yaml
137
+ */
138
+ constructor(config_yaml) {
139
+ const ptr0 = passStringToWasm0(config_yaml, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
140
+ const len0 = WASM_VECTOR_LEN;
141
+ const ret = wasm.bowerytable_new(ptr0, len0);
142
+ if (ret[2]) {
143
+ throw takeFromExternrefTable0(ret[1]);
144
+ }
145
+ this.__wbg_ptr = ret[0];
146
+ BoweryTableFinalization.register(this, this.__wbg_ptr, this);
147
+ return this;
148
+ }
149
+ /**
150
+ * Play: deals the next hand.
151
+ *
152
+ * # Errors
153
+ *
154
+ * Not play mode, the hand is not over, or the table is broken.
155
+ *
156
+ * # Examples
157
+ *
158
+ * ```
159
+ * use bowery::wasm::BoweryTable;
160
+ *
161
+ * let mut table = BoweryTable::new("
162
+ * mode: play
163
+ * table: { button: 0, stakes: { small_blind: 1.0, big_blind: 2.0 } }
164
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
165
+ * ").unwrap();
166
+ * table.act(r#"{"action":"fold"}"#).unwrap();
167
+ * while table.advance().unwrap() != "over" {}
168
+ * assert!(table.next_hand().is_ok());
169
+ * ```
170
+ */
171
+ next_hand() {
172
+ const ret = wasm.bowerytable_next_hand(this.__wbg_ptr);
173
+ if (ret[1]) {
174
+ throw takeFromExternrefTable0(ret[0]);
175
+ }
176
+ }
177
+ /**
178
+ * Moves to moment `index`, from 0; past the end, to the last.
179
+ *
180
+ * # Examples
181
+ *
182
+ * ```
183
+ * use bowery::wasm::BoweryTable;
184
+ *
185
+ * let mut table = BoweryTable::new("
186
+ * mode: play
187
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
188
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
189
+ * ").unwrap();
190
+ * table.seek(0);
191
+ * assert!(table.state().contains(r#""moment":1"#));
192
+ * ```
193
+ * @param {number} index
194
+ */
195
+ seek(index) {
196
+ wasm.bowerytable_seek(this.__wbg_ptr, index);
197
+ }
198
+ /**
199
+ * Draws in `layout` from now on: `ring`, `spotlight` or `ledger`.
200
+ *
201
+ * # Errors
202
+ *
203
+ * Any other name.
204
+ *
205
+ * # Examples
206
+ *
207
+ * ```
208
+ * use bowery::wasm::BoweryTable;
209
+ *
210
+ * let mut table = BoweryTable::new("
211
+ * mode: play
212
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
213
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
214
+ * ").unwrap();
215
+ * assert!(table.set_layout("ledger").is_ok());
216
+ * ```
217
+ * @param {string} layout
218
+ */
219
+ set_layout(layout) {
220
+ const ptr0 = passStringToWasm0(layout, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
221
+ const len0 = WASM_VECTOR_LEN;
222
+ const ret = wasm.bowerytable_set_layout(this.__wbg_ptr, ptr0, len0);
223
+ if (ret[1]) {
224
+ throw takeFromExternrefTable0(ret[0]);
225
+ }
226
+ }
227
+ /**
228
+ * Draws in `view` from now on: `all` or `seat`.
229
+ *
230
+ * # Errors
231
+ *
232
+ * Any other name.
233
+ *
234
+ * # Examples
235
+ *
236
+ * ```
237
+ * use bowery::wasm::BoweryTable;
238
+ *
239
+ * let mut table = BoweryTable::new("
240
+ * mode: play
241
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
242
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
243
+ * ").unwrap();
244
+ * assert!(table.set_view("all").is_ok());
245
+ * ```
246
+ * @param {string} view
247
+ */
248
+ set_view(view) {
249
+ const ptr0 = passStringToWasm0(view, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
250
+ const len0 = WASM_VECTOR_LEN;
251
+ const ret = wasm.bowerytable_set_view(this.__wbg_ptr, ptr0, len0);
252
+ if (ret[1]) {
253
+ throw takeFromExternrefTable0(ret[0]);
254
+ }
255
+ }
256
+ /**
257
+ * Play: a new game seeded with `seed`.
258
+ *
259
+ * # Errors
260
+ *
261
+ * Not play mode.
262
+ *
263
+ * # Examples
264
+ *
265
+ * ```
266
+ * use bowery::wasm::BoweryTable;
267
+ *
268
+ * let mut table = BoweryTable::new("
269
+ * mode: play
270
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
271
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
272
+ * ").unwrap();
273
+ * assert!(table.shuffle(9).is_ok());
274
+ * ```
275
+ * @param {number} seed
276
+ */
277
+ shuffle(seed) {
278
+ const ret = wasm.bowerytable_shuffle(this.__wbg_ptr, seed);
279
+ if (ret[1]) {
280
+ throw takeFromExternrefTable0(ret[0]);
281
+ }
282
+ }
283
+ /**
284
+ * The controls' state as JSON (see [`crate::element::State`]).
285
+ *
286
+ * # Examples
287
+ *
288
+ * ```
289
+ * use bowery::wasm::BoweryTable;
290
+ *
291
+ * let table = BoweryTable::new("
292
+ * mode: play
293
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
294
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
295
+ * ").unwrap();
296
+ * assert!(table.state().contains(r#""moment":1"#));
297
+ * ```
298
+ * @returns {string}
299
+ */
300
+ state() {
301
+ let deferred1_0;
302
+ let deferred1_1;
303
+ try {
304
+ const ret = wasm.bowerytable_state(this.__wbg_ptr);
305
+ deferred1_0 = ret[0];
306
+ deferred1_1 = ret[1];
307
+ return getStringFromWasm0(ret[0], ret[1]);
308
+ } finally {
309
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
310
+ }
311
+ }
312
+ /**
313
+ * The table on screen as SVG, `width` pixels wide, dark or light.
314
+ *
315
+ * # Examples
316
+ *
317
+ * ```
318
+ * use bowery::wasm::BoweryTable;
319
+ *
320
+ * let table = BoweryTable::new("
321
+ * mode: play
322
+ * table: { stakes: { small_blind: 1.0, big_blind: 2.0 } }
323
+ * players: [ { seat: 0, stack: 200 }, { seat: 1, stack: 200, bot: abc } ]
324
+ * ").unwrap();
325
+ * assert!(table.svg(400, true).contains(r#"width="400""#));
326
+ * ```
327
+ * @param {number} width
328
+ * @param {boolean} dark
329
+ * @returns {string}
330
+ */
331
+ svg(width, dark) {
332
+ let deferred1_0;
333
+ let deferred1_1;
334
+ try {
335
+ const ret = wasm.bowerytable_svg(this.__wbg_ptr, width, dark);
336
+ deferred1_0 = ret[0];
337
+ deferred1_1 = ret[1];
338
+ return getStringFromWasm0(ret[0], ret[1]);
339
+ } finally {
340
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
341
+ }
342
+ }
343
+ /**
344
+ * The reader takes `seat` (interactive).
345
+ *
346
+ * # Errors
347
+ *
348
+ * The game's reason, as a string.
349
+ *
350
+ * # Examples
351
+ *
352
+ * ```
353
+ * use bowery::wasm::BoweryTable;
354
+ *
355
+ * let mut table = BoweryTable::new("
356
+ * mode: interactive
357
+ * hand:
358
+ * hand: { id: ex, game: holdem }
359
+ * table: { seats: 2, button: 0, stakes: { small_blind: 1.0, big_blind: 2.0 } }
360
+ * players: [ { seat: 0, name: A, stack: 100.0 }, { seat: 1, name: B, stack: 100.0 } ]
361
+ * streets: { preflop: { actions: [ { seat: 0, action: fold } ] } }
362
+ * ").unwrap();
363
+ * assert!(table.take_seat(0).is_ok());
364
+ * ```
365
+ * @param {number} seat
366
+ */
367
+ take_seat(seat) {
368
+ const ret = wasm.bowerytable_take_seat(this.__wbg_ptr, seat);
369
+ if (ret[1]) {
370
+ throw takeFromExternrefTable0(ret[0]);
371
+ }
372
+ }
373
+ /**
374
+ * This crate's version.
375
+ *
376
+ * # Examples
377
+ *
378
+ * ```
379
+ * use bowery::wasm::BoweryTable;
380
+ *
381
+ * assert_eq!(env!("CARGO_PKG_VERSION"), BoweryTable::version());
382
+ * ```
383
+ * @returns {string}
384
+ */
385
+ static version() {
386
+ let deferred1_0;
387
+ let deferred1_1;
388
+ try {
389
+ const ret = wasm.bowerytable_version();
390
+ deferred1_0 = ret[0];
391
+ deferred1_1 = ret[1];
392
+ return getStringFromWasm0(ret[0], ret[1]);
393
+ } finally {
394
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
395
+ }
396
+ }
397
+ }
398
+ if (Symbol.dispose) BoweryTable.prototype[Symbol.dispose] = BoweryTable.prototype.free;
399
+ function __wbg_get_imports() {
400
+ const import0 = {
401
+ __proto__: null,
402
+ __wbg___wbindgen_throw_41e9ee4f547fc59a: function(arg0, arg1) {
403
+ throw new Error(getStringFromWasm0(arg0, arg1));
404
+ },
405
+ __wbindgen_generic_0000000000000001: function(arg0, arg1) {
406
+ // Cast intrinsic for `Ref(String) -> Externref`.
407
+ const ret = getStringFromWasm0(arg0, arg1);
408
+ return ret;
409
+ },
410
+ __wbindgen_init_externref_table: function() {
411
+ const table = wasm.__wbindgen_externrefs;
412
+ const offset = table.grow(4);
413
+ table.set(0, undefined);
414
+ table.set(offset + 0, undefined);
415
+ table.set(offset + 1, null);
416
+ table.set(offset + 2, true);
417
+ table.set(offset + 3, false);
418
+ },
419
+ };
420
+ return {
421
+ __proto__: null,
422
+ "./bowery_wasm_bg.js": import0,
423
+ };
424
+ }
425
+
426
+ const BoweryTableFinalization = (typeof FinalizationRegistry === 'undefined')
427
+ ? { register: () => {}, unregister: () => {} }
428
+ : new FinalizationRegistry(ptr => wasm.__wbg_bowerytable_free(ptr, 1));
429
+
430
+ function getStringFromWasm0(ptr, len) {
431
+ return decodeText(ptr >>> 0, len);
432
+ }
433
+
434
+ let cachedUint8ArrayMemory0 = null;
435
+ function getUint8ArrayMemory0() {
436
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
437
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
438
+ }
439
+ return cachedUint8ArrayMemory0;
440
+ }
441
+
442
+ function passStringToWasm0(arg, malloc, realloc) {
443
+ if (realloc === undefined) {
444
+ const buf = cachedTextEncoder.encode(arg);
445
+ const ptr = malloc(buf.length, 1) >>> 0;
446
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
447
+ WASM_VECTOR_LEN = buf.length;
448
+ return ptr;
449
+ }
450
+
451
+ let len = arg.length;
452
+ let ptr = malloc(len, 1) >>> 0;
453
+
454
+ const mem = getUint8ArrayMemory0();
455
+
456
+ let offset = 0;
457
+
458
+ for (; offset < len; offset++) {
459
+ const code = arg.charCodeAt(offset);
460
+ if (code > 0x7F) break;
461
+ mem[ptr + offset] = code;
462
+ }
463
+ if (offset !== len) {
464
+ if (offset !== 0) {
465
+ arg = arg.slice(offset);
466
+ }
467
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
468
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
469
+ const ret = cachedTextEncoder.encodeInto(arg, view);
470
+
471
+ offset += ret.written;
472
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
473
+ }
474
+
475
+ WASM_VECTOR_LEN = offset;
476
+ return ptr;
477
+ }
478
+
479
+ function takeFromExternrefTable0(idx) {
480
+ const value = wasm.__wbindgen_externrefs.get(idx);
481
+ wasm.__externref_table_dealloc(idx);
482
+ return value;
483
+ }
484
+
485
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
486
+ cachedTextDecoder.decode();
487
+ const MAX_SAFARI_DECODE_BYTES = 2146435072;
488
+ let numBytesDecoded = 0;
489
+ function decodeText(ptr, len) {
490
+ numBytesDecoded += len;
491
+ if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
492
+ cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
493
+ cachedTextDecoder.decode();
494
+ numBytesDecoded = len;
495
+ }
496
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
497
+ }
498
+
499
+ const cachedTextEncoder = new TextEncoder();
500
+
501
+ if (!('encodeInto' in cachedTextEncoder)) {
502
+ cachedTextEncoder.encodeInto = function (arg, view) {
503
+ const buf = cachedTextEncoder.encode(arg);
504
+ view.set(buf);
505
+ return {
506
+ read: arg.length,
507
+ written: buf.length
508
+ };
509
+ };
510
+ }
511
+
512
+ let WASM_VECTOR_LEN = 0;
513
+
514
+ let wasmModule, wasmInstance, wasm;
515
+ function __wbg_finalize_init(instance, module) {
516
+ wasmInstance = instance;
517
+ wasm = instance.exports;
518
+ wasmModule = module;
519
+ cachedUint8ArrayMemory0 = null;
520
+ wasm.__wbindgen_start();
521
+ return wasm;
522
+ }
523
+
524
+ async function __wbg_load(module, imports) {
525
+ if (typeof Response === 'function' && module instanceof Response) {
526
+ if (!module.ok) {
527
+ throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
528
+ }
529
+
530
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
531
+ try {
532
+ return await WebAssembly.instantiateStreaming(module, imports);
533
+ } catch (e) {
534
+ const validResponse = expectedResponseType(module.type);
535
+
536
+ if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
537
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
538
+
539
+ } else { throw e; }
540
+ }
541
+ }
542
+
543
+ const bytes = await module.arrayBuffer();
544
+ return await WebAssembly.instantiate(bytes, imports);
545
+ } else {
546
+ const instance = await WebAssembly.instantiate(module, imports);
547
+
548
+ if (instance instanceof WebAssembly.Instance) {
549
+ return { instance, module };
550
+ } else {
551
+ return instance;
552
+ }
553
+ }
554
+
555
+ function expectedResponseType(type) {
556
+ switch (type) {
557
+ case 'basic': case 'cors': case 'default': return true;
558
+ }
559
+ return false;
560
+ }
561
+ }
562
+
563
+ function initSync(module) {
564
+ if (wasm !== undefined) return wasm;
565
+
566
+
567
+ if (module !== undefined) {
568
+ if (Object.getPrototypeOf(module) === Object.prototype) {
569
+ ({module} = module)
570
+ } else {
571
+ console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
572
+ }
573
+ }
574
+
575
+ const imports = __wbg_get_imports();
576
+ if (!(module instanceof WebAssembly.Module)) {
577
+ module = new WebAssembly.Module(module);
578
+ }
579
+ const instance = new WebAssembly.Instance(module, imports);
580
+ return __wbg_finalize_init(instance, module);
581
+ }
582
+
583
+ async function __wbg_init(module_or_path) {
584
+ if (wasm !== undefined) return wasm;
585
+
586
+
587
+ if (module_or_path !== undefined) {
588
+ if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
589
+ ({module_or_path} = module_or_path)
590
+ } else {
591
+ console.warn('using deprecated parameters for the initialization function; pass a single object instead')
592
+ }
593
+ }
594
+
595
+ if (module_or_path === undefined) {
596
+ module_or_path = new URL('bowery_wasm_bg.wasm', import.meta.url);
597
+ }
598
+ const imports = __wbg_get_imports();
599
+
600
+ if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
601
+ module_or_path = fetch(module_or_path);
602
+ }
603
+
604
+ const { instance, module } = await __wbg_load(await module_or_path, imports);
605
+
606
+ return __wbg_finalize_init(instance, module);
607
+ }
608
+
609
+ export { initSync, __wbg_init as default };
Binary file
package/package.json CHANGED
@@ -1,8 +1,30 @@
1
1
  {
2
2
  "name": "@imperialbower/bowery",
3
- "version": "0.0.0",
4
- "description": "One poker table control, configured not compiled: <bowery-table>. Placeholder; the element ships in 0.1.0.",
3
+ "version": "0.1.0",
4
+ "description": "One poker table control, configured not compiled: <bowery-table>.",
5
5
  "license": "MIT OR Apache-2.0",
6
- "repository": { "type": "git", "url": "git+https://github.com/ImperialBower/bowery.git" },
7
- "files": ["README.md", "LICENSE-MIT", "LICENSE-APACHE"]
6
+ "type": "module",
7
+ "main": "bowery.js",
8
+ "exports": {
9
+ ".": "./bowery.js",
10
+ "./bowery.css": "./bowery.css"
11
+ },
12
+ "files": [
13
+ "bowery.js",
14
+ "bowery.css",
15
+ "bowery_wasm.js",
16
+ "bowery_wasm_bg.wasm",
17
+ "LICENSE-MIT",
18
+ "LICENSE-APACHE",
19
+ "MANIFEST.sha256",
20
+ "README.md"
21
+ ],
22
+ "sideEffects": true,
23
+ "keywords": ["poker", "web-component", "custom-element", "wasm", "svg", "hand-history"],
24
+ "homepage": "https://github.com/ImperialBower/bowery",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/ImperialBower/bowery.git"
28
+ },
29
+ "bugs": "https://github.com/ImperialBower/bowery/issues"
8
30
  }