Skip to content

Commit ac55eb8

Browse files
committed
Add skater profiles data handling to translate2js.py; update prompt construction for JSON integration
1 parent 9a227dd commit ac55eb8

File tree

2 files changed

+237
-76
lines changed

2 files changed

+237
-76
lines changed

src/wzrdbrain/wzrdbrain.js

Lines changed: 215 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,107 @@
33
* @license Apache-2.0
44
*/
55

6-
// Data from tricks.json, embedded directly as constants
7-
const DIRECTIONS = ["front", "back"];
8-
const STANCES = ["open", "closed"];
9-
const MOVES = [
10-
"predator", "predator one", "parallel", "tree", "gazelle", "gazelle s",
11-
"lion", "lion s", "toe press", "heel press", "toe roll", "heel roll",
12-
"360", "180", "540", "parallel slide", "soul slide", "acid slide",
13-
"mizu slide", "star slide", "fast slide", "back slide", "stunami",
14-
"ufo swivel", "toe pivot", "heel pivot"
15-
];
6+
// Data from tricks.json, embedded directly as a constant
7+
const TRICK_DATA = {
8+
"DIRECTIONS": ["front", "back"],
9+
"STANCES": ["open", "closed"],
10+
"MOVES": [
11+
"predator", "predator one", "parallel", "tree", "gazelle", "gazelle s",
12+
"lion", "lion s", "toe press", "heel press", "toe roll", "heel roll",
13+
"360", "180", "540", "parallel slide", "soul slide", "acid slide",
14+
"mizu slide", "star slide", "fast slide", "back slide", "stunami",
15+
"ufo swivel", "toe pivot", "heel pivot"
16+
],
17+
"RULES": {
18+
"ONLY_FIRST": ["predator", "predator one", "parallel"],
19+
"USE_FAKIE": [
20+
"toe press", "toe roll", "heel press", "heel roll", "360", "180", "540",
21+
"parallel slide", "soul slide", "acid slide", "mizu slide", "star slide",
22+
"fast slide", "back slide"
23+
],
24+
"EXCLUDE_STANCE_BASE": ["predator", "predator one"],
25+
"ROTATING_MOVES": ["gazelle", "lion", "180", "540", "stunami", "ufo swivel"]
26+
}
27+
};
28+
29+
// Data from skater_profiles.json, embedded directly as a constant
30+
const SKATER_PROFILES = [{
31+
"name": "Flow Purist",
32+
"traits": ["Process", "Intrinsic", "Adaptive", "Solo", "Risk-averse", "Flow", "Subtle", "Flatland"]
33+
}, {
34+
"name": "Spot Hunter",
35+
"traits": ["Process", "Intrinsic", "Adaptive", "Solo", "Flow", "Risk-tolerant", "Physical", "3D"]
36+
}, {
37+
"name": "Precision Builder",
38+
"traits": ["Process", "Intrinsic", "Solo", "Risk-averse", "Subtle", "Flatland", "Structured", "Trick"]
39+
}, {
40+
"name": "Creative Connector",
41+
"traits": ["Process", "Intrinsic", "Adaptive", "Flow", "Flatland", "Risk-tolerant", "Physical", "3D", "Social"]
42+
}, {
43+
"name": "Social Flow Rider",
44+
"traits": ["Process", "Intrinsic", "Adaptive", "Risk-averse", "Flow", "Subtle", "Flatland", "Social"]
45+
}, {
46+
"name": "Performer",
47+
"traits": ["Process", "Adaptive", "Flow", "Flatland", "Risk-tolerant", "Physical", "3D", "Social", "Extrinsic"]
48+
}, {
49+
"name": "Urban Technician",
50+
"traits": ["Intrinsic", "Solo", "Subtle", "Risk-tolerant", "3D", "Structured", "Trick", "Result"]
51+
}, {
52+
"name": "Goal Chaser",
53+
"traits": ["Intrinsic", "Solo", "Risk-averse", "Subtle", "Flatland", "Structured", "Trick", "Result"]
54+
}, {
55+
"name": "Gearhead",
56+
"traits": ["Process", "Intrinsic", "Risk-averse", "Subtle", "Flatland", "3D", "Structured", "Trick", "Social"]
57+
}, {
58+
"name": "Competitive Stylist",
59+
"traits": ["Adaptive", "Flow", "Flatland", "Risk-tolerant", "Physical", "3D", "Social", "Extrinsic", "Result"]
60+
}, ];
61+
62+
63+
const DIRECTIONS = TRICK_DATA.DIRECTIONS;
64+
const STANCES = TRICK_DATA.STANCES;
65+
const MOVES = TRICK_DATA.MOVES;
1666

1767
// Rules converted to Set for efficient lookups, mirroring Python's set usage
18-
/** @type {Set<string>} */
19-
const onlyFirst = new Set(["predator", "predator one", "parallel"]);
20-
/** @type {Set<string>} */
21-
const useFakie = new Set([
22-
"toe press", "toe roll", "heel press", "heel roll", "360", "180", "540",
23-
"parallel slide", "soul slide", "acid slide", "mizu slide", "star slide",
24-
"fast slide", "back slide"
25-
]);
26-
/** @type {Set<string>} */
27-
const excludeStanceBase = new Set(["predator", "predator one"]);
28-
/** @type {Set<string>} */
29-
const rotatingMoves = new Set(["gazelle", "lion", "180", "540", "stunami", "ufo swivel"]);
30-
31-
32-
// Derived rules, mirroring Python's `exclude_stance` and `SUBSEQUENT_MOVES`
33-
/**
34-
* A set of moves that exclude an automatically determined stance.
35-
* This is the union of EXCLUDE_STANCE_BASE and USE_FAKIE from the JSON rules,
36-
* directly translating Python's `exclude_stance_base.union(use_fakie)`.
37-
* @type {Set<string>}
38-
*/
39-
const excludeStance = new Set([...excludeStanceBase, ...useFakie]);
68+
const onlyFirst = new Set(TRICK_DATA.RULES.ONLY_FIRST);
69+
const useFakie = new Set(TRICK_DATA.RULES.USE_FAKIE);
70+
const rotatingMoves = new Set(TRICK_DATA.RULES.ROTATING_MOVES);
71+
const excludeStanceBase = new Set(TRICK_DATA.RULES.EXCLUDE_STANCE_BASE);
4072

41-
/**
42-
* An array of moves that are valid for subsequent tricks (i.e., not "ONLY_FIRST").
43-
* This is pre-calculated for efficiency, directly translating Python's `set(MOVES) - only_first`.
44-
* @type {string[]}
45-
*/
73+
// Derived rules, mirroring Python's logic
74+
const excludeStance = new Set([...excludeStanceBase, ...useFakie]);
4675
const subsequentMoves = MOVES.filter(move => !onlyFirst.has(move));
4776

4877

4978
/**
5079
* @typedef {'front' | 'back'} Direction
5180
* @typedef {'open' | 'closed'} Stance
5281
* @typedef {string} Move
82+
* @typedef {object} TrickObject
83+
* @property {Direction | null} direction
84+
* @property {Stance | null} stance
85+
* @property {Move | null} move
86+
* @property {Direction | null} enterIntoTrick
87+
* @property {Direction | null} exitFromTrick
88+
* @property {string} name
89+
* @typedef {object} SkaterProfile
90+
* @property {string} name
91+
* @property {string[]} traits
92+
* @typedef {object} ProfileMatch
93+
* @property {string} name
94+
* @property {number} score
95+
* @property {string[]} matched_traits
96+
* @typedef {object} ClarifyingQuestion
97+
* @property {string} question
98+
* @property {Object<string, string[]>} choices
5399
*/
54100

101+
55102
/**
56103
* Represents a single trick with its direction, stance, and move.
57104
* Automatically generates random values for unspecified properties
58105
* and adjusts properties like exit direction based on the move,
59-
* directly translating the Python `Trick` dataclass logic, especially its `__post_init__` method.
106+
* directly translating the Python `Trick` dataclass logic.
60107
*/
61108
export class Trick {
62109
/** @type {Direction | null} */
@@ -89,7 +136,7 @@ export class Trick {
89136
enterIntoTrick = null,
90137
exitFromTrick = null,
91138
} = {}) {
92-
// 1. Input validation, mirroring Python's `__post_init__` validation
139+
// 1. Input validation
93140
if (direction !== null && !DIRECTIONS.includes(direction)) {
94141
throw new Error(`Invalid direction: '${direction}'. Must be one of ${DIRECTIONS.join(', ')}`);
95142
}
@@ -107,7 +154,7 @@ export class Trick {
107154
this.enterIntoTrick = enterIntoTrick;
108155
this.exitFromTrick = exitFromTrick;
109156

110-
// 3. Generate default values if not provided, mirroring Python's `__post_init__`
157+
// 3. Generate default values if not provided
111158
if (this.direction === null) {
112159
this.direction = DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)];
113160
}
@@ -142,14 +189,12 @@ export class Trick {
142189
/**
143190
* Returns a human-readable string representation of the trick,
144191
* directly translating the Python `__str__` method.
145-
* This handles "fakie" / "forward" display names for relevant moves.
146192
* @returns {string} The formatted name of the trick.
147193
*/
148194
getName() {
149195
const parts = [];
150196
let displayDirection = this.direction;
151197

152-
// Handle fakie/forward display name, mirroring Python's `__str__`
153198
if (this.move !== null && useFakie.has(this.move)) {
154199
if (this.direction === "back") {
155200
displayDirection = "fakie";
@@ -173,9 +218,8 @@ export class Trick {
173218

174219
/**
175220
* Returns a plain JavaScript object representation of the trick,
176-
* including all its properties and its full display name,
177221
* directly translating the Python `to_dict` method.
178-
* @returns {object} An object containing the trick's properties and its name.
222+
* @returns {TrickObject} An object containing the trick's properties and its name.
179223
*/
180224
toObject() {
181225
return {
@@ -190,46 +234,39 @@ export class Trick {
190234
}
191235

192236
/**
193-
* Generates a combination (combo) of tricks,
194-
* directly translating the Python `generate_combo` function.
195-
*
196-
* @param {number | null} [numTricks=null] - The number of tricks to generate in the combo.
197-
* If null, a random number between 2 and 5 (inclusive) will be chosen, mirroring Python's `random.randint(2, 5)`.
198-
* @returns {object[]} A list of trick objects, each with their properties and a 'name' field.
199-
* Returns an empty array if numTricks is 0 or less.
237+
* Generates a combination (combo) of tricks.
238+
* @param {number | null} [numTricks=null] - The number of tricks to generate.
239+
* If null, a random number between 2 and 5 will be chosen.
240+
* @returns {TrickObject[]} An array of trick objects.
200241
*/
201242
export function generateCombo(numTricks = null) {
202-
// Mirroring Python's default num_of_tricks = random.randint(2, 5)
203-
if (numTricks === null) {
204-
numTricks = Math.floor(Math.random() * (5 - 2 + 1)) + 2;
243+
let count = numTricks;
244+
if (count === null) {
245+
count = Math.floor(Math.random() * (5 - 2 + 1)) + 2;
205246
}
206247

207-
if (numTricks <= 0) {
248+
if (count <= 0) {
208249
return [];
209250
}
210251

211-
/** @type {Trick[]} */
212252
const trickObjects = [];
213-
/** @type {Trick | null} */
214253
let previousTrick = null;
215254

216-
for (let i = 0; i < numTricks; i++) {
217-
/** @type {Trick} */
255+
for (let i = 0; i < count; i++) {
218256
let newTrick;
219257

220258
if (i === 0) {
221-
// First trick: choose from all moves, mirroring Python's logic
259+
// First trick: choose from all moves
222260
const move = MOVES[Math.floor(Math.random() * MOVES.length)];
223-
newTrick = new Trick({ move });
261+
newTrick = new Trick({
262+
move
263+
});
224264
} else {
225-
// Subsequent tricks: respect exit direction of previous trick
226-
// and choose from moves not marked as ONLY_FIRST.
265+
// Subsequent tricks: respect exit direction and choose from allowed moves
227266
if (!previousTrick) {
228-
// This case should ideally not be reached, mirroring Python's `assert`
229267
throw new Error("Previous trick is undefined for subsequent trick generation.");
230268
}
231269
const requiredDirection = previousTrick.exitFromTrick;
232-
// Choose from the pre-filtered array of subsequent moves for efficiency
233270
const move = subsequentMoves[Math.floor(Math.random() * subsequentMoves.length)];
234271
newTrick = new Trick({
235272
direction: requiredDirection,
@@ -241,6 +278,118 @@ export function generateCombo(numTricks = null) {
241278
previousTrick = newTrick;
242279
}
243280

244-
// Mirroring Python's `[trick.to_dict() for trick in trick_objects]`
245281
return trickObjects.map(trick => trick.toObject());
282+
}
283+
284+
/**
285+
* Finds and ranks skater profiles based on user-provided traits.
286+
* @param {string[]} userTraits - A list of strings representing the user's skating style.
287+
* @returns {ProfileMatch[]} A list of profile match objects, sorted by score descending.
288+
*/
289+
export function findProfileMatches(userTraits) {
290+
if (!SKATER_PROFILES || SKATER_PROFILES.length === 0) {
291+
return [];
292+
}
293+
294+
const normalizedUserTraits = new Set(userTraits.map(t => t.toLowerCase()));
295+
296+
const matches = SKATER_PROFILES.map(profile => {
297+
const profileTraitsLower = new Set(profile.traits.map(t => t.toLowerCase()));
298+
const matchedSet = new Set([...normalizedUserTraits].filter(trait => profileTraitsLower.has(trait)));
299+
const score = matchedSet.size;
300+
301+
return {
302+
name: profile.name,
303+
score: score,
304+
matched_traits: Array.from(matchedSet).sort(),
305+
};
306+
});
307+
308+
return matches.sort((a, b) => b.score - a.score);
309+
}
310+
311+
/**
312+
* Finds a single skater profile by its name (case-insensitive).
313+
* @param {string} name - The name of the profile to find.
314+
* @returns {SkaterProfile | undefined} The profile object if found, otherwise undefined.
315+
*/
316+
export function getProfileByName(name) {
317+
return SKATER_PROFILES.find(profile => profile.name.toLowerCase() === name.toLowerCase());
318+
}
319+
320+
/**
321+
* Gets a unique, sorted list of all traits available across all profiles.
322+
* @returns {string[]} A sorted list of unique trait strings.
323+
*/
324+
export function getAllTraits() {
325+
const allTraits = new Set(SKATER_PROFILES.flatMap(p => p.traits));
326+
return Array.from(allTraits).sort();
327+
}
328+
329+
/**
330+
* Generates a clarifying question to differentiate between closely matched profiles.
331+
* @param {ProfileMatch[]} topProfiles - A list of the top profile matches from `findProfileMatches`.
332+
* @returns {ClarifyingQuestion | null} An object with a question and choices, or null if a question cannot be generated.
333+
*/
334+
export function generateClarifyingQuestion(topProfiles) {
335+
if (!topProfiles || topProfiles.length < 2) {
336+
return null;
337+
}
338+
339+
const OPPOSING_TRAITS_MAP = {
340+
"Solo": "Social",
341+
"Social": "Solo",
342+
"Risk-averse": "Risk-tolerant",
343+
"Risk-tolerant": "Risk-averse",
344+
"Flatland": "3D",
345+
"3D": "Flatland",
346+
"Subtle": "Physical",
347+
"Physical": "Subtle",
348+
"Structured": "Adaptive",
349+
"Adaptive": "Structured",
350+
"Process": "Result",
351+
"Result": "Process"
352+
};
353+
354+
const profileA_full = getProfileByName(topProfiles[0].name);
355+
const profileB_full = getProfileByName(topProfiles[1].name);
356+
357+
if (!profileA_full || !profileB_full) {
358+
return null;
359+
}
360+
361+
const traitsA = new Set(profileA_full.traits);
362+
const traitsB = new Set(profileB_full.traits);
363+
364+
const uniqueToA = new Set([...traitsA].filter(x => !traitsB.has(x)));
365+
const uniqueToB = new Set([...traitsB].filter(x => !traitsA.has(x)));
366+
367+
// 1. Prioritize finding a clear opposing pair
368+
for (const trait of uniqueToA) {
369+
const opposite = OPPOSING_TRAITS_MAP[trait];
370+
if (opposite && uniqueToB.has(opposite)) {
371+
return {
372+
question: `Which is more important to you: '${trait}' or '${opposite}'?`,
373+
choices: {
374+
[trait]: [trait],
375+
[opposite]: [opposite],
376+
},
377+
};
378+
}
379+
}
380+
381+
// 2. If no direct opposites, use the most significant unique traits
382+
if (uniqueToA.size > 0 && uniqueToB.size > 0) {
383+
const traitA = [...uniqueToA][0];
384+
const traitB = [...uniqueToB][0];
385+
return {
386+
question: `To help narrow it down, which sounds more like you: '${traitA}' or '${traitB}'?`,
387+
choices: {
388+
[traitA]: [traitA],
389+
[traitB]: [traitB]
390+
},
391+
};
392+
}
393+
394+
return null;
246395
}

0 commit comments

Comments
 (0)