Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@
# sqlx-macros référence sans l'activer : il n'est jamais compilé ici (vérifié
# par `cargo tree -i rsa --target all`, vide). À retirer si rsa apparaît
# dans l'arbre des dépendances.
ignore = ["RUSTSEC-2023-0071"]
#
# libsqlite3-sys 0.30 (RUSTSEC-2026-0459, correctif en 0.39) est dans le même cas :
# il n'est dans Cargo.lock que par sqlx-sqlite, que sqlx-macros référence sans
# l'activer (vérifié par `cargo tree -i libsqlite3-sys --target all -e all`,
# vide). À retirer si libsqlite3-sys apparaît dans l'arbre des dépendances.
ignore = ["RUSTSEC-2023-0071", "RUSTSEC-2026-0459"]
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Reportés : file d’attente des fusions hors connexion (supprimée, ADR 0041),
| Ricochets : format « assembler » ([ADR 0067](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0067-ricochets-assembler-un-mot-depuis-sa-definition.md)) : `FormatRicochet::Assembler`, réponse `Briques`, colonnes tirées par `colonnes_assemblage`, manifeste `ricochets` (`position`, `assemblage`), composant `Assembler` ; essai local, tests à compléter côté PWA | fait |
| Ricochets, PWA : moins de touchers ([ADR 0068](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0068-ricochets-moins-de-touchers-pour-decouper-et-assembler.md)) : la lame de découpe arrive, et repart après chaque coupe, sur une jointure tirée au hasard qui n’est jamais une coupe attendue ; *Assembler* envoie la réponse à la seconde brique posée, sans « Valider » ; tests du composant `Assembler` | fait |
| Ricochets, PWA : *Paires* envoie les réponses à la dernière paire reliée, sans « Valider les paires », comme *Découper* et *Assembler* ; la place du numéro de paire est réservée, les colonnes ne se redimensionnent plus à chaque paire reliée ; tests du composant `Paires` | fait |
| Ricochets, *Découper* en chaîne ([ADR 0084](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0084-ricochets-decouper-toutes-les-decoupes-justes.md)) : le contenu publie toutes les coupes d'un mot (`coupes`), parties composées comprises ; le joueur les pose d'un coup, seule la coupe complète est juste ; le verdict et la leçon montrent les morceaux élémentaires (`morceauxDuMot`, miroir du compilateur de contenu) | fait |
| « En savoir plus » (I-16) : chemin des langues, relations, transformations, certitude et sources citées ; dépliée depuis la carte du codex (`plus=1`) | fait (Content publie les types de relation et de transformation dans les référentiels) |
| Vue Réseau (App#91) : la constellation en deux rives (1ʳᵉ case à gauche, 2ᵉ à droite, une bande par fascicule débloqué, ordre alphabétique, rangées de 44 px), dernière découverte en Corail, barre de famille Rosée, antennes chiffrées dans la vue « Tous », famille suivie dans un panneau collé, liste en repli ; calculée côté client (`services/constellation.ts`), sans légendaire ni fascicule verrouillé | fait en local ; ADR et maquette à écrire dans Design |

Expand Down
44 changes: 43 additions & 1 deletion web/src/services/ricochets.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'

import { type DonneesRicochets, juger, retour } from './ricochets'
import { type DonneesRicochets, juger, morceauxDuMot, retour } from './ricochets'

const d: DonneesRicochets = {
briques: new Map([
Expand Down Expand Up @@ -54,6 +54,48 @@ describe('juger', () => {
})
})

describe('morceauxDuMot', () => {
const composition = (parties: [string, number, number][]) => ({
preferee: true,
parties: parties.map(([unite_id, segment_debut, segment_fin], i) => ({ rang: i + 1, unite_id, segment_debut, segment_fin })),
})
const jeu = (cartes: Record<string, unknown>) => ({ cartes: new Map(Object.entries(cartes)) }) as never

it('descend dans les parties qui sont des mots composés : étymologiste = étymo · logie · iste', () => {
const j = jeu({
mot: { nature: 'mot', compositions: [composition([['logie_mot', 0, 10], ['iste', 10, 13]])] },
logie_mot: { nature: 'mot', compositions: [composition([['etymo', 0, 5], ['logie', 5, 10]])] },
etymo: { nature: 'prefixe' },
logie: { nature: 'suffixe' },
iste: { nature: 'suffixe' },
})
expect(morceauxDuMot(j, 'mot')).toEqual([
{ debut: 0, fin: 5, nature: 'prefixe' },
{ debut: 5, fin: 10, nature: 'suffixe' },
{ debut: 10, fin: 13, nature: 'suffixe' },
])
})
it('une partie élidée est bornée par son segment : biologie dans biologiste', () => {
// biologie (8 lettres) n'occupe que [0, 6) de biologiste : sa dernière partie perd « ie ».
const j = jeu({
mot: { nature: 'mot', compositions: [composition([['biologie', 0, 6], ['iste', 6, 10]])] },
biologie: { nature: 'mot', compositions: [composition([['bio', 0, 3], ['logie', 3, 8]])] },
bio: { nature: 'prefixe' },
logie: { nature: 'suffixe' },
iste: { nature: 'suffixe' },
})
expect(morceauxDuMot(j, 'mot')).toEqual([
{ debut: 0, fin: 3, nature: 'prefixe' },
{ debut: 3, fin: 6, nature: 'suffixe' },
{ debut: 6, fin: 10, nature: 'suffixe' },
])
})
it('sans segments continus, les parties gardent leurs bornes absentes', () => {
const j = jeu({ mot: { nature: 'mot', compositions: [{ preferee: true, parties: [{ rang: 1, unite_id: 'a' }, { rang: 2, unite_id: 'b' }] }] } })
expect(morceauxDuMot(j, 'mot').map((m) => m.debut)).toEqual([-1, -1])
})
})

describe('retour', () => {
const carte = (jours: number, maitrisee = false) => ({
niveau: 1,
Expand Down
33 changes: 31 additions & 2 deletions web/src/services/ricochets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,37 @@ export function retour(carte: CarteRicochet, aujourdhui: Date = new Date()): str
return carte.maitrisee ? `Maîtrisée · ${delai.toLowerCase()}` : delai
}

/** Morceaux du mot dans sa composition préférée, aux couleurs de leurs briques (verdict de « découper »). */
export function morceauxDuMot(jeu: CatalogueJeu, motId: string): { debut: number; fin: number; nature: BriqueJeu['nature'] }[] {
type Morceau = { debut: number; fin: number; nature: BriqueJeu['nature'] }

/**
* Morceaux élémentaires d'un mot, une fois toutes ses coupes posées : une partie qui est elle-même un mot
* composé se coupe à son tour (*étymologiste* : étymo · logie · iste). Une partie élidée est bornée par son
* segment. `null` : les segments ne pavent pas le mot, il ne se coupe pas. Miroir des coupes que publie
* le compilateur du contenu (`coupes_completes`, ADR 0084) : changez les deux ensemble.
*/
function feuilles(jeu: CatalogueJeu, motId: string, debut: number, limite: number, vus: string[]): Morceau[] | null {
const compositions = jeu.cartes.get(motId)?.compositions ?? []
const choisie = compositions.find((c) => c.preferee) ?? compositions[0]
const parties = [...(choisie?.parties ?? [])].sort((a, b) => a.rang - b.rang)
const continues = parties.every((p, i) => p.segment_debut != null && p.segment_fin != null && p.segment_fin > p.segment_debut && p.segment_debut === (i === 0 ? 0 : parties[i - 1]?.segment_fin))
if (!parties.length || !continues) return null
const fin = Math.min(debut + (parties[parties.length - 1]?.segment_fin ?? 0), limite)
const morceaux: Morceau[] = []
for (const p of parties) {
const d = debut + (p.segment_debut ?? 0)
if (d >= fin) break
const f = Math.min(debut + (p.segment_fin ?? 0), fin)
const sous = vus.includes(p.unite_id) ? null : feuilles(jeu, p.unite_id, d, f, [...vus, motId])
if (sous && sous.length > 1) morceaux.push(...sous)
else morceaux.push({ debut: d, fin: f, nature: jeu.cartes.get(p.unite_id)?.nature ?? 'mot' })
}
return morceaux
}

/** Morceaux du mot, aux couleurs de leurs briques (verdict de « découper »), chaînage compris. */
export function morceauxDuMot(jeu: CatalogueJeu, motId: string): Morceau[] {
const elementaires = feuilles(jeu, motId, 0, Number.POSITIVE_INFINITY, [])
if (elementaires) return elementaires
const compositions = jeu.cartes.get(motId)?.compositions ?? []
const choisie = compositions.find((c) => c.preferee) ?? compositions[0]
return [...(choisie?.parties ?? [])]
Expand Down
Loading