my-sea: async witness — spectators see the owner's draw land live via WS, no refresh — TDD
The bud watching @owner's sea now sees each card appear in real time instead of having to refresh. Follows the epic RoomConsumer broadcast pattern (view -> group_send -> consumer handler -> send_json), keyed on the owner (mysea_<owner>) since my-sea has no Room. - apps/gameboard/consumers.py: MySeaSpectateConsumer — read-only WS; membership gate matches voice (owner OR present invitee: ACCEPTED + deposited + not left). Relays a `sea_draw` event carrying the owner's full hand. - apps/gameboard/routing.py + core/asgi.py: ws/my-sea-spectate/<owner_id>/. - gameboard/views.py: _notify_sea_draw(owner_id, hand) — best-effort, guarded group_send so a down/missing channel layer can't break the solo draw. Fired from my_sea_lock (both the create + the mid-draw-upsert branch) and from my_sea_delete (empty hand -> clears the spectators' cross). - my_sea_visit.html: a WS listener fills the cross live — SeaDeal.register(card, '.sea-pos-'+pos, isLevity) reuses _fillSlot (incl. the --rank-long squeeze) + seeds the slot clickable into the stage; a DEL re-empties cleared slots. Capped reconnect for transient blips. Tests: 5 channels ITs (owner/present-invitee connect + receive; unauth / stranger / accepted-not-present rejected); +2 view ITs (lock broadcasts owner+ hand; lock still 200s when the broadcast raises). Client fill needs live two-party verification on staging (Redis up). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,70 @@
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{# Async witness — a WS to `mysea_<owner>` pushes the owner's hand as each #}
|
||||
{# card lands; SeaDeal.register fills the cross slot (+ seeds it clickable) #}
|
||||
{# live, no refresh. A DEL (empty hand) re-empties the cleared slots. #}
|
||||
<script>
|
||||
(function () {
|
||||
if (!('WebSocket' in window)) return;
|
||||
var deckEl = document.getElementById('id_my_sea_deck');
|
||||
var deck = {};
|
||||
try { deck = JSON.parse((deckEl && deckEl.textContent) || '{}'); } catch (e) {}
|
||||
var byId = {};
|
||||
(deck.levity || []).concat(deck.gravity || []).forEach(function (c) { byId[c.id] = c; });
|
||||
|
||||
function _applyHand(hand) {
|
||||
if (!window.SeaDeal || !window.SeaDeal.register) return;
|
||||
var cross = document.querySelector('.my-sea-cross');
|
||||
if (!cross) return;
|
||||
var inHand = {};
|
||||
(hand || []).forEach(function (e) {
|
||||
if (!e || !e.position) return;
|
||||
inHand[e.position] = true;
|
||||
var card = byId[e.card_id];
|
||||
if (!card) return;
|
||||
var c = {};
|
||||
for (var k in card) { if (Object.prototype.hasOwnProperty.call(card, k)) c[k] = card[k]; }
|
||||
c.reversed = !!e.reversed;
|
||||
window.SeaDeal.register(c, '.sea-pos-' + e.position, e.polarity === 'levity');
|
||||
});
|
||||
// Re-empty any slot the owner cleared (DEL → empty-hand broadcast).
|
||||
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
|
||||
var pos = slot.dataset.posKey;
|
||||
if (pos && !inHand[pos]) {
|
||||
var crossing = slot.classList.contains('sea-card-slot--crossing');
|
||||
slot.className = 'sea-card-slot sea-card-slot--empty' + (crossing ? ' sea-card-slot--crossing' : '');
|
||||
slot.innerHTML = '';
|
||||
slot.removeAttribute('data-card-id');
|
||||
slot.removeAttribute('data-pos-key');
|
||||
}
|
||||
});
|
||||
}
|
||||
window._mySeaApplyHand = _applyHand; // test seam
|
||||
|
||||
var tries = 0;
|
||||
function _connect() {
|
||||
var scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
var ws;
|
||||
try {
|
||||
ws = new WebSocket(scheme + '://' + window.location.host + '/ws/my-sea-spectate/{{ owner.id }}/');
|
||||
} catch (e) { return; }
|
||||
ws.onmessage = function (ev) {
|
||||
var msg;
|
||||
try { msg = JSON.parse(ev.data); } catch (e) { return; }
|
||||
if (msg && msg.type === 'sea_draw') _applyHand(msg.hand);
|
||||
};
|
||||
// Brief capped reconnect for transient blips (no infinite loop if
|
||||
// the spectate server/Redis is down).
|
||||
ws.onclose = function () { if (tries++ < 5) setTimeout(_connect, 3000); };
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', _connect);
|
||||
} else {
|
||||
_connect();
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
Reference in New Issue
Block a user