Skip to content

Commit f18a980

Browse files
VTT-AI-Agentclaude96LawDawg
authored
Add keepOrder option to SWAPHANDS (#3052)
* Add keepOrder option to SWAPHANDS SWAPHANDS collects the cards of a hand in the order they are in that hand but hands them to the MOVE operation as a collection array, which is resolved via widgetFilter and therefore processed in widget creation order. The receiving player thus gets the cards in the order they were created in instead of the order the previous player had arranged them in. Since games may rely on the current behavior, the order is only preserved when the new keepOrder property is set, in which case the cards are moved one by one. * Move a hand's cards with one ordered MOVE and test the order MOVE iterates its collection in array order - only an array of IDs is re-resolved through widgetFilter, which returns the widgets in creation order. So instead of generating one MOVE per card, keepOrder can hand the cards to a single MOVE as an already ordered collection. That keeps the routine log readable while debugging and avoids re-filtering all widgets of the room once per card. tests/testcafe/routines.js covers both the unchanged default (cards arrive in widget creation order) and keepOrder (cards arrive in the order of the hand), so the ordering contract is no longer only checked by hand. * Log what SWAPHANDS did in the routine log SWAPHANDS was one of the few operations without a jeLoggingRoutineOperationSummary, so its entry in the DEBUG log was the bare word SWAPHANDS - with keepOrder there are now two ways it can behave, which the log should name. It also says so when there are not enough seated players to swap between, which was silent before. * Make the SWAPHANDS routine log name the seats it moves cards between * Keep SWAPHANDS from clobbering a collection of the surrounding routine SWAPHANDS names the temporary collections it moves through after the seats they come from, so the routine log reads "from 'hand of seat1'". Those names are not reserved though: a routine that uses a collection of that name itself lost it, because SWAPHANDS overwrote and then deleted it. Remember what was there and put it back instead. Co-Authored-By: Claude <noreply@anthropic.com> * Fill the SWAPHANDS collections one move at a time The generated MOVEs used to look up their widgets when they ran, so a widget that a routine of an earlier move removed was skipped. Give each MOVE its own routine call and fill its collection right before it, which restores that and also keeps the caller's collection shadowed for one move only, even if a MOVE throws. Also make the log summary read "hands in a random seat order by 1" instead of "hands random by 1" and say that nothing was moved when no seat has a hand. * Update tutorial with keepOrder --------- Co-authored-by: VTT AI Agent <300563116+VTT-AI-Agent@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: 96LawDawg <76912527+96LawDawg@users.noreply.github.com>
1 parent 8bec319 commit f18a980

7 files changed

Lines changed: 243 additions & 37 deletions

File tree

.github/workflows/testcafe-chrome.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
- publiclibrary-2.js
2828
- publiclibrary-3.js
2929
- publiclibrary-4.js
30+
- routines.js
3031
steps:
3132
- name: Checkout repository
3233
uses: actions/checkout@v4

.github/workflows/testcafe-firefox.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
- publiclibrary-2.js
2828
- publiclibrary-3.js
2929
- publiclibrary-4.js
30+
- routines.js
3031
steps:
3132
- name: Checkout repository
3233
uses: actions/checkout@v4

client/js/jsonedit.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,7 +1389,7 @@ function jeAddCommands() {
13891389
jeAddRoutineOperationCommands('SET', { collection: 'DEFAULT', property: 'parent', relation: '=', value: null });
13901390
jeAddRoutineOperationCommands('SHUFFLE', { holder: null, collection: 'DEFAULT', mode: 'true random', modeValue: 1 });
13911391
jeAddRoutineOperationCommands('SORT', { key: 'value', reverse: false, rearrange: false, locales: null, options: null, holder: null, collection: 'DEFAULT' });
1392-
jeAddRoutineOperationCommands('SWAPHANDS', { interval: 1, direction: 'forward', source: 'all' });
1392+
jeAddRoutineOperationCommands('SWAPHANDS', { interval: 1, direction: 'forward', source: 'all', keepOrder: false });
13931393
jeAddRoutineOperationCommands('TIMER', { value: 0, seconds: 0, mode: 'toggle', timer: null, collection: 'DEFAULT' });
13941394
jeAddRoutineOperationCommands('TURN', { turn: 1, turnCycle: 'forward', source: 'all', collection: 'TURN' });
13951395
jeAddRoutineOperationCommands('UPLOAD', { variable: 'uploadedFileName', fileTypes: [ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.json', '.mp3', '.wav', '.ogg', '.m4a' ] });
@@ -3050,7 +3050,7 @@ function jeLoggingJSON(obj) {
30503050
}
30513051

30523052
export function jeLoggingRoutineStart(widget, property, initialVariables, initialCollections, byReference) {
3053-
if( jeHTMLStack.length == 0 || ['CALL', 'CLICK', 'IF', 'loopRoutine'].indexOf( jeHTMLStack[0][3] ) == -1 ) {
3053+
if( jeHTMLStack.length == 0 || ['CALL', 'CLICK', 'IF', 'loopRoutine', 'Moves'].indexOf( jeHTMLStack[0][3] ) == -1 ) {
30543054
if(jeRoutineResetOnNextLog) {
30553055
jeLoggingHTML = '';
30563056
jeRoutineResetOnNextLog = false;
@@ -3068,7 +3068,7 @@ export function jeLoggingRoutineStart(widget, property, initialVariables, initia
30683068
}
30693069

30703070
export function jeLoggingRoutineEnd(variables, collections) {
3071-
if( jeHTMLStack.length == 0 || ['CALL', 'CLICK', 'IF', 'loopRoutine'].indexOf( jeHTMLStack[0][3] ) == -1 ) jeLoggingHTML += '</div></div>';
3071+
if( jeHTMLStack.length == 0 || ['CALL', 'CLICK', 'IF', 'loopRoutine', 'Moves'].indexOf( jeHTMLStack[0][3] ) == -1 ) jeLoggingHTML += '</div></div>';
30723072
--jeLoggingDepth;
30733073
if(!jeLoggingDepth) {
30743074
$('#jeLog').innerHTML = jeLoggingHTML + '</div></div>';

client/js/widgets/widget.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,7 +2032,7 @@ export class Widget extends StateManaged {
20322032
}
20332033

20342034
if(a.func == 'SWAPHANDS') {
2035-
setDefaults(a, { interval: 1, direction: 'forward', source: 'all' });
2035+
setDefaults(a, { interval: 1, direction: 'forward', source: 'all', keepOrder: false });
20362036
if(['forward', 'backward', 'random'].indexOf(a.direction) == -1) {
20372037
problems.push(`Warning: direction ${a.direction} interpreted as forward.`);
20382038
a.direction = 'forward'
@@ -2050,6 +2050,8 @@ export class Widget extends StateManaged {
20502050
[c[i], c[rand]] = [c[rand], c[i]];
20512051
}
20522052
}
2053+
// all hands are collected before anything is moved so that a hand does not
2054+
// pick up the widgets an earlier seat just passed to it
20532055
let moves = [];
20542056
for (let i = 0; i < c.length; i++) {
20552057
let source = c[i];
@@ -2060,26 +2062,51 @@ export class Widget extends StateManaged {
20602062
let contents = widgets.get(hand).children().reduce(
20612063
function (collect, w) {
20622064
if (!perOwner || w.get('owner') == source.get('player')) {
2063-
collect.unshift(w.get('id'));
2065+
collect.unshift(w);
20642066
}
20652067
return collect
20662068
},
20672069
[]
20682070
);
2069-
moves.push({
2070-
func: "MOVE",
2071-
collection: contents,
2072-
to: target.get('id'),
2073-
});
2071+
moves.push({ source, contents, to: target.get('id') });
20742072
}
20752073
}
2076-
if(jeRoutineLogging) {
2077-
jeLoggingRoutineOperationStart("Moves", "Moves");
2074+
if(moves.length) {
2075+
if(jeRoutineLogging)
2076+
jeLoggingRoutineOperationStart("Moves", "Moves");
2077+
for(const move of moves) {
2078+
// the collection is named after the seat it comes from so that the
2079+
// generated MOVE reads like "from 'hand of seat1' to 'seat2'" in the log.
2080+
// a collection of the surrounding routine that happens to use the same
2081+
// name is shadowed only while its MOVE runs and then put back
2082+
const collection = `hand of ${move.source.get('id')}`;
2083+
const shadowed = collections[collection];
2084+
// the widgets are looked up right before their own MOVE so that one which
2085+
// a routine of an earlier MOVE removed is left alone, exactly like when
2086+
// the generated MOVE still received a list of IDs. keepOrder keeps the
2087+
// order of the hand, the default is the creation order because that is
2088+
// the order widgetFilter - and with it MOVE - used all along
2089+
collections[collection] = a.keepOrder
2090+
? move.contents.filter(w=>!w.isBeingRemoved)
2091+
: widgetFilter(w=>move.contents.indexOf(w) != -1);
2092+
try {
2093+
await this.evaluateRoutine([ { func: 'MOVE', collection, to: move.to } ], variables, collections, (depth || 0) + 1, true);
2094+
} finally {
2095+
if(shadowed === undefined)
2096+
delete collections[collection];
2097+
else
2098+
collections[collection] = shadowed;
2099+
}
2100+
}
2101+
if(jeRoutineLogging)
2102+
jeLoggingRoutineOperationEnd([], variables, collections, false);
20782103
}
2079-
await this.evaluateRoutine(moves, variables, collections, (depth || 0) + 1, true);
20802104
if(jeRoutineLogging) {
2081-
jeLoggingRoutineOperationEnd([], variables, collections, false);
2105+
const how = a.direction == 'random' ? `hands in a random seat order by ${a.interval}` : `hands ${a.direction} by ${a.interval}`;
2106+
jeLoggingRoutineOperationSummary(moves.length ? `${how}${a.keepOrder ? ', keeping the card order' : ''}` : 'no seat with a player has a valid hand, nothing to swap');
20822107
}
2108+
} else if(jeRoutineLogging) {
2109+
jeLoggingRoutineOperationSummary('less than two seats with a player, nothing to swap');
20832110
}
20842111
}
20852112

library/tutorials/Functions - SWAPHANDS/0.json

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@
678678
"css": {
679679
"font-size": "25px"
680680
},
681-
"text": "SWAPHANDS is used to swap the contents of the hands of the given collection of seats. This is especially useful for games which use pick-and-pass card drafting, but also in any situation where one or more players need to exchange hands.\n\nIt's possible to use this in a scenario where all seats share one hand with childrenPerOwner: true, but here separate hands are shown for each user to make the effect clearer.\n\nYou can also sit in multiple seats at once in this room, to test out the different buttons."
681+
"text": "SWAPHANDS is used to swap the contents of the hands of the given collection of seats. This is especially useful for games which use pick-and-pass card drafting, but also in any situation where one or more players need to exchange hands.\n\nIt's possible to use this in a scenario where all seats share one hand with childrenPerOwner: true, but here separate hands are shown for each user to make the effect clearer.\n\nYou can also sit in multiple seats at once in this room, to test out the different buttons. The keepOrder button in the bottom middle will, for all buttons, either keep the order of the cards as they are passed the same (if true) or not (if false). Even if keepOrder is false, the order may not change, but setting it to true ensures they will not change."
682682
},
683683
"seat-p1": {
684684
"type": "seat",
@@ -1097,7 +1097,8 @@
10971097
"text": "Forward",
10981098
"clickRoutine": [
10991099
{
1100-
"func": "SWAPHANDS"
1100+
"func": "SWAPHANDS",
1101+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11011102
}
11021103
]
11031104
},
@@ -1112,7 +1113,8 @@
11121113
"clickRoutine": [
11131114
{
11141115
"func": "SWAPHANDS",
1115-
"direction": "backward"
1116+
"direction": "backward",
1117+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11161118
}
11171119
]
11181120
},
@@ -1127,7 +1129,8 @@
11271129
"clickRoutine": [
11281130
{
11291131
"func": "SWAPHANDS",
1130-
"direction": "random"
1132+
"direction": "random",
1133+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11311134
}
11321135
]
11331136
},
@@ -1145,7 +1148,8 @@
11451148
"source": [
11461149
"seat-p1",
11471150
"seat-p2"
1148-
]
1151+
],
1152+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11491153
}
11501154
]
11511155
},
@@ -1163,7 +1167,8 @@
11631167
"source": [
11641168
"seat-p1",
11651169
"seat-p3"
1166-
]
1170+
],
1171+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11671172
}
11681173
]
11691174
},
@@ -1181,7 +1186,8 @@
11811186
"source": [
11821187
"seat-p1",
11831188
"seat-p4"
1184-
]
1189+
],
1190+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
11851191
}
11861192
]
11871193
},
@@ -1199,7 +1205,8 @@
11991205
"source": [
12001206
"seat-p4",
12011207
"seat-p1"
1202-
]
1208+
],
1209+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12031210
}
12041211
]
12051212
},
@@ -1217,7 +1224,8 @@
12171224
"source": [
12181225
"seat-p4",
12191226
"seat-p2"
1220-
]
1227+
],
1228+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12211229
}
12221230
]
12231231
},
@@ -1235,7 +1243,8 @@
12351243
"source": [
12361244
"seat-p4",
12371245
"seat-p3"
1238-
]
1246+
],
1247+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12391248
}
12401249
]
12411250
},
@@ -1253,7 +1262,8 @@
12531262
"source": [
12541263
"seat-p2",
12551264
"seat-p1"
1256-
]
1265+
],
1266+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12571267
}
12581268
]
12591269
},
@@ -1271,7 +1281,8 @@
12711281
"source": [
12721282
"seat-p2",
12731283
"seat-p3"
1274-
]
1284+
],
1285+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12751286
}
12761287
]
12771288
},
@@ -1289,7 +1300,8 @@
12891300
"source": [
12901301
"seat-p2",
12911302
"seat-p4"
1292-
]
1303+
],
1304+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
12931305
}
12941306
]
12951307
},
@@ -1307,7 +1319,8 @@
13071319
"source": [
13081320
"seat-p3",
13091321
"seat-p1"
1310-
]
1322+
],
1323+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
13111324
}
13121325
]
13131326
},
@@ -1325,7 +1338,8 @@
13251338
"source": [
13261339
"seat-p3",
13271340
"seat-p2"
1328-
]
1341+
],
1342+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
13291343
}
13301344
]
13311345
},
@@ -1343,7 +1357,8 @@
13431357
"source": [
13441358
"seat-p3",
13451359
"seat-p4"
1346-
]
1360+
],
1361+
"keepOrder": "${PROPERTY keepOrder OF button-keepOrder}"
13471362
}
13481363
]
13491364
},
@@ -1356,7 +1371,10 @@
13561371
"parent": "holder-deck"
13571372
},
13581373
"_meta": {
1359-
"version": 20,
1374+
"version": 21,
1375+
"gameSettings": {
1376+
"legacyModes": {}
1377+
},
13601378
"info": {
13611379
"name": "Functions - SWAPHANDS",
13621380
"image": "/assets/-407492963_4703",
@@ -1366,7 +1384,7 @@
13661384
"mode": "Tutorial",
13671385
"time": "0",
13681386
"attribution": "",
1369-
"lastUpdate": 1768621533542,
1387+
"lastUpdate": 1785106704884,
13701388
"showName": false,
13711389
"skill": "",
13721390
"description": "",
@@ -1379,10 +1397,52 @@
13791397
"players": "1",
13801398
"language": "",
13811399
"variant": "Basics",
1382-
"variantImage": ""
1383-
},
1384-
"gameSettings": {
1385-
"legacyModes": {}
1400+
"variantImage": "",
1401+
"usesAIImagery": false
13861402
}
1403+
},
1404+
"button-keepOrder": {
1405+
"type": "button",
1406+
"id": "button-keepOrder",
1407+
"x": 740,
1408+
"y": 736,
1409+
"width": 120,
1410+
"height": 50,
1411+
"text": "keepOrder true",
1412+
"clickRoutine": [
1413+
{
1414+
"func": "IF",
1415+
"condition": "${PROPERTY keepOrder}",
1416+
"thenRoutine": [
1417+
{
1418+
"func": "SET",
1419+
"collection": "thisButton",
1420+
"property": "keepOrder",
1421+
"value": false
1422+
},
1423+
{
1424+
"func": "SET",
1425+
"collection": "thisButton",
1426+
"property": "text",
1427+
"value": "keepOrder false"
1428+
}
1429+
],
1430+
"elseRoutine": [
1431+
{
1432+
"func": "SET",
1433+
"collection": "thisButton",
1434+
"property": "keepOrder",
1435+
"value": true
1436+
},
1437+
{
1438+
"func": "SET",
1439+
"collection": "thisButton",
1440+
"property": "text",
1441+
"value": "keepOrder true"
1442+
}
1443+
]
1444+
}
1445+
],
1446+
"keepOrder": true
13871447
}
13881448
}

0 commit comments

Comments
 (0)