From c7f124d2cc405fe357f8f2368296fb5d27af6078 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 27 Apr 2026 18:18:13 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(contentkit):=20Add=20find=20th?= =?UTF-8?q?e=20right=20number=20gameplay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #2044 --- ...530C243D2973F4181BD99F59C.new_activity.yml | 93 +++++-- ...123BD4E1182D7105AB31FF396.new_activity.yml | 93 +++++-- ...3AE424CCB836115C4E9911017.new_activity.yml | 93 +++++-- .../Exercise/Exercise+Gameplay.swift | 1 + .../CurrentExerciseCoordinator.swift | 23 +- .../NewGameplayFindTheRightNumber.swift | 70 ++++++ .../TTSCoordinator+FindTheRightNumber.swift | 238 ++++++++++++++++++ ...CoordinatorGameplayChoiceModelDecode.swift | 51 ++++ .../NewGameplayFindTheRightNumber_Tests.swift | 60 +++++ Specs/jtd/activity.jtd.json | 1 + Specs/jtd/new_activity.jtd.json | 1 + 11 files changed, 637 insertions(+), 87 deletions(-) create mode 100644 Modules/ContentKit/Sources/GameEngine/_NewSystem/Gameplays/NewGameplayFindTheRightNumber.swift create mode 100644 Modules/ContentKit/Sources/GameEngine/_NewSystem/Interfaces/TTS/Coordinators/TTSCoordinator+FindTheRightNumber.swift create mode 100644 Modules/ContentKit/Tests/GameEngine/NewGameplayFindTheRightNumber_Tests.swift diff --git a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_animals-313569E530C243D2973F4181BD99F59C.new_activity.yml b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_animals-313569E530C243D2973F4181BD99F59C.new_activity.yml index 6d4123b7c6..f8875ac9b6 100644 --- a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_animals-313569E530C243D2973F4181BD99F59C.new_activity.yml +++ b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_animals-313569E530C243D2973F4181BD99F59C.new_activity.yml @@ -8,7 +8,7 @@ uuid: 313569E530C243D2973F4181BD99F59C name: counting_robot_animals created_at: "2024-06-17T17:38:12.804177" -last_edited_at: "2026-03-12T11:05:42.296805" +last_edited_at: "2026-04-27T18:33:36.928172" status: published launch_requirements: @@ -50,14 +50,14 @@ l10n: Une activité ludique pour apprendre à compter avec le robot Leka. description: | - Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher le même nombre d’éléments demandés sur la tablette. + Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher exactement ce nombre d’éléments sur la tablette. - L’activité suit une structure de gauche à droite, ligne par ligne, pour renforcer l’organisation spatiale et éviter les erreurs de comptage. Le comptage avec le doigt est encouragé pour favoriser la coordination œil-main et ancrer la compréhension des quantités. + Tous les éléments affichés sont des réponses possibles : seule la quantité sélectionnée compte. L’objectif est de sélectionner ni moins ni plus que le nombre demandé, en comptant chaque élément touché. instructions: | - Demandez à la personne accompagnée d’observer le chiffre affiché par le robot. - - Expliquez qu’elle doit toucher sur la tablette le même nombre d’éléments. - - Dites-lui de commencer par la gauche et d’avancer vers la droite, ligne par ligne. + - Expliquez qu’elle doit toucher exactement ce nombre d’éléments sur la tablette, ni plus ni moins. + - Rappelez-lui que tous les éléments affichés peuvent être choisis. - Encouragez-la à compter à voix haute en touchant chaque élément. - locale: en_US @@ -71,14 +71,14 @@ l10n: A fun activity to learn counting with the Leka robot. description: | - This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap the same number of requested items on the tablet. + This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap exactly that many items on the tablet. - The activity follows a left-to-right, line-by-line structure to reinforce spatial organization and prevent counting mistakes. Counting with the finger is encouraged to improve hand-eye coordination and strengthen quantity understanding. + All displayed items are valid choices: only the selected quantity matters. The goal is to select no fewer and no more than the requested number while counting each tapped item. instructions: | - Ask the care receiver to observe the number displayed by the robot. - - Explain that they need to tap the same number of items. - - Tell them to start from the left and move to the right, line by line. + - Explain that they need to tap exactly that many items on the tablet, no more and no fewer. + - Remind them that all displayed items can be chosen. - Encourage them to count out loud while tapping each item. payload: @@ -90,11 +90,11 @@ payload: - group: - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -102,6 +102,10 @@ payload: value: magicCardNumbers1One options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 1 + maximumToSelect: 1 payload: choices: - value: curriculum_counting_robot_animals_forest_bear_brown @@ -109,22 +113,27 @@ payload: is_right_answer: true - value: curriculum_counting_robot_animals_forest_bear_brown type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bear_brown type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bear_brown type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bear_brown type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bear_brown type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -132,6 +141,10 @@ payload: value: magicCardNumbers2Two options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 2 + maximumToSelect: 2 payload: choices: - value: curriculum_counting_robot_animals_forest_bird_robin_red @@ -142,20 +155,24 @@ payload: is_right_answer: true - value: curriculum_counting_robot_animals_forest_bird_robin_red type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bird_robin_red type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bird_robin_red type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_bird_robin_red type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -163,6 +180,10 @@ payload: value: magicCardNumbers3Three options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 3 + maximumToSelect: 3 payload: choices: - value: curriculum_counting_robot_animals_forest_frog_green @@ -176,18 +197,21 @@ payload: is_right_answer: true - value: curriculum_counting_robot_animals_forest_frog_green type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_frog_green type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_frog_green type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -195,6 +219,10 @@ payload: value: magicCardNumbers4Four options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 4 + maximumToSelect: 4 payload: choices: - value: curriculum_counting_robot_animals_forest_hedgehog_brown @@ -211,16 +239,18 @@ payload: is_right_answer: true - value: curriculum_counting_robot_animals_forest_hedgehog_brown type: image + is_right_answer: true - value: curriculum_counting_robot_animals_forest_hedgehog_brown type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -228,6 +258,10 @@ payload: value: magicCardNumbers5Five options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 5 + maximumToSelect: 5 payload: choices: - value: curriculum_counting_robot_animals_forest_owl_brown @@ -247,14 +281,15 @@ payload: is_right_answer: true - value: curriculum_counting_robot_animals_forest_owl_brown type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -262,6 +297,10 @@ payload: value: magicCardNumbers6Six options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 6 + maximumToSelect: 6 payload: choices: - value: curriculum_counting_robot_animals_forest_squirrel_orange diff --git a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_fruits-26652AC123BD4E1182D7105AB31FF396.new_activity.yml b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_fruits-26652AC123BD4E1182D7105AB31FF396.new_activity.yml index 9abb4aa256..08b748d500 100644 --- a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_fruits-26652AC123BD4E1182D7105AB31FF396.new_activity.yml +++ b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_fruits-26652AC123BD4E1182D7105AB31FF396.new_activity.yml @@ -8,7 +8,7 @@ uuid: 26652AC123BD4E1182D7105AB31FF396 name: counting_robot_fruits created_at: "2024-06-17T17:38:12.804177" -last_edited_at: "2026-03-12T11:05:42.087336" +last_edited_at: "2026-04-27T18:33:36.928651" status: published launch_requirements: @@ -50,14 +50,14 @@ l10n: Une activité ludique pour apprendre à compter avec le robot Leka. description: | - Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher le même nombre d’éléments demandés sur la tablette. + Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher exactement ce nombre d’éléments sur la tablette. - L’activité suit une structure de gauche à droite, ligne par ligne, pour renforcer l’organisation spatiale et éviter les erreurs de comptage. Le comptage avec le doigt est encouragé pour favoriser la coordination œil-main et ancrer la compréhension des quantités. + Tous les éléments affichés sont des réponses possibles : seule la quantité sélectionnée compte. L’objectif est de sélectionner ni moins ni plus que le nombre demandé, en comptant chaque élément touché. instructions: | - Demandez à la personne accompagnée d’observer le chiffre affiché par le robot. - - Expliquez qu’elle doit toucher sur la tablette le même nombre d’éléments. - - Dites-lui de commencer par la gauche et d’avancer vers la droite, ligne par ligne. + - Expliquez qu’elle doit toucher exactement ce nombre d’éléments sur la tablette, ni plus ni moins. + - Rappelez-lui que tous les éléments affichés peuvent être choisis. - Encouragez-la à compter à voix haute en touchant chaque élément. - locale: en_US @@ -71,14 +71,14 @@ l10n: A fun activity to learn counting with the Leka robot. description: | - This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap the same number of requested items on the tablet. + This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap exactly that many items on the tablet. - The activity follows a left-to-right, line-by-line structure to reinforce spatial organization and prevent counting mistakes. Counting with the finger is encouraged to improve hand-eye coordination and strengthen quantity understanding. + All displayed items are valid choices: only the selected quantity matters. The goal is to select no fewer and no more than the requested number while counting each tapped item. instructions: | - Ask the care receiver to observe the number displayed by the robot. - - Explain that they need to tap the same number of items. - - Tell them to start from the left and move to the right, line by line. + - Explain that they need to tap exactly that many items on the tablet, no more and no fewer. + - Remind them that all displayed items can be chosen. - Encourage them to count out loud while tapping each item. payload: @@ -90,12 +90,12 @@ payload: - group: - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -103,6 +103,10 @@ payload: value: magicCardNumbers1One options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 1 + maximumToSelect: 1 payload: choices: - value: curriculum_counting_robot_photo_fruit_apple @@ -110,22 +114,27 @@ payload: is_right_answer: true - value: curriculum_counting_robot_photo_fruit_apple type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_apple type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_apple type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_apple type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_apple type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -133,6 +142,10 @@ payload: value: magicCardNumbers2Two options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 2 + maximumToSelect: 2 payload: choices: - value: curriculum_counting_robot_photo_fruit_banana @@ -143,20 +156,24 @@ payload: is_right_answer: true - value: curriculum_counting_robot_photo_fruit_banana type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_banana type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_banana type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_banana type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -164,6 +181,10 @@ payload: value: magicCardNumbers3Three options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 3 + maximumToSelect: 3 payload: choices: - value: curriculum_counting_robot_photo_fruit_lemon @@ -177,18 +198,21 @@ payload: is_right_answer: true - value: curriculum_counting_robot_photo_fruit_lemon type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_lemon type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_lemon type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -196,6 +220,10 @@ payload: value: magicCardNumbers4Four options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 4 + maximumToSelect: 4 payload: choices: - value: curriculum_counting_robot_photo_fruit_orange @@ -212,16 +240,18 @@ payload: is_right_answer: true - value: curriculum_counting_robot_photo_fruit_orange type: image + is_right_answer: true - value: curriculum_counting_robot_photo_fruit_orange type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -229,6 +259,10 @@ payload: value: magicCardNumbers5Five options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 5 + maximumToSelect: 5 payload: choices: - value: curriculum_counting_robot_photo_fruit_pear @@ -248,14 +282,15 @@ payload: is_right_answer: true - value: curriculum_counting_robot_photo_fruit_pear type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -263,6 +298,10 @@ payload: value: magicCardNumbers6Six options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 6 + maximumToSelect: 6 payload: choices: - value: curriculum_counting_robot_photo_fruit_strawberry diff --git a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_musical_instruments-A1EFFF53AE424CCB836115C4E9911017.new_activity.yml b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_musical_instruments-A1EFFF53AE424CCB836115C4E9911017.new_activity.yml index 838f7bd5a2..8124fe14af 100644 --- a/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_musical_instruments-A1EFFF53AE424CCB836115C4E9911017.new_activity.yml +++ b/Modules/ContentKit/Resources/Content/curriculums/curriculum_counting_robot-53E4A0E2B181472ABF3E089C833615D8/new_activities/counting_robot_musical_instruments-A1EFFF53AE424CCB836115C4E9911017.new_activity.yml @@ -8,7 +8,7 @@ uuid: A1EFFF53AE424CCB836115C4E9911017 name: counting_robot_musical_instruments created_at: "2024-06-17T17:38:12.804177" -last_edited_at: "2026-03-12T11:05:42.407929" +last_edited_at: "2026-04-27T18:33:36.928978" status: published launch_requirements: @@ -50,14 +50,14 @@ l10n: Une activité ludique pour apprendre à compter avec le robot Leka. description: | - Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher le même nombre d’éléments demandés sur la tablette. + Cette activité aide la personne accompagnée à développer ses compétences en comptage. Le robot affiche un chiffre sur son écran, et la personne accompagnée doit toucher exactement ce nombre d’éléments sur la tablette. - L’activité suit une structure de gauche à droite, ligne par ligne, pour renforcer l’organisation spatiale et éviter les erreurs de comptage. Le comptage avec le doigt est encouragé pour favoriser la coordination œil-main et ancrer la compréhension des quantités. + Tous les éléments affichés sont des réponses possibles : seule la quantité sélectionnée compte. L’objectif est de sélectionner ni moins ni plus que le nombre demandé, en comptant chaque élément touché. instructions: | - Demandez à la personne accompagnée d’observer le chiffre affiché par le robot. - - Expliquez qu’elle doit toucher sur la tablette le même nombre d’éléments. - - Dites-lui de commencer par la gauche et d’avancer vers la droite, ligne par ligne. + - Expliquez qu’elle doit toucher exactement ce nombre d’éléments sur la tablette, ni plus ni moins. + - Rappelez-lui que tous les éléments affichés peuvent être choisis. - Encouragez-la à compter à voix haute en touchant chaque élément. - locale: en_US @@ -71,14 +71,14 @@ l10n: A fun activity to learn counting with the Leka robot. description: | - This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap the same number of requested items on the tablet. + This activity helps the care receiver develop counting skills. The robot displays a number on its screen, and the care receiver must tap exactly that many items on the tablet. - The activity follows a left-to-right, line-by-line structure to reinforce spatial organization and prevent counting mistakes. Counting with the finger is encouraged to improve hand-eye coordination and strengthen quantity understanding. + All displayed items are valid choices: only the selected quantity matters. The goal is to select no fewer and no more than the requested number while counting each tapped item. instructions: | - Ask the care receiver to observe the number displayed by the robot. - - Explain that they need to tap the same number of items. - - Tell them to start from the left and move to the right, line by line. + - Explain that they need to tap exactly that many items on the tablet, no more and no fewer. + - Remind them that all displayed items can be chosen. - Encourage them to count out loud while tapping each item. payload: @@ -90,11 +90,11 @@ payload: - group: - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -102,6 +102,10 @@ payload: value: magicCardNumbers1One options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 1 + maximumToSelect: 1 payload: choices: - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A @@ -109,22 +113,27 @@ payload: is_right_answer: true - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-accordion_orange-013A type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -132,6 +141,10 @@ payload: value: magicCardNumbers2Two options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 2 + maximumToSelect: 2 payload: choices: - value: curriculum_counting_robot_musical_instrument-guitar_brown-0134 @@ -142,20 +155,24 @@ payload: is_right_answer: true - value: curriculum_counting_robot_musical_instrument-guitar_brown-0134 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-guitar_brown-0134 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-guitar_brown-0134 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-guitar_brown-0134 type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -163,6 +180,10 @@ payload: value: magicCardNumbers3Three options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 3 + maximumToSelect: 3 payload: choices: - value: curriculum_counting_robot_musical_instrument-harp_brown-0135 @@ -176,18 +197,21 @@ payload: is_right_answer: true - value: curriculum_counting_robot_musical_instrument-harp_brown-0135 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-harp_brown-0135 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-harp_brown-0135 type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -195,6 +219,10 @@ payload: value: magicCardNumbers4Four options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 4 + maximumToSelect: 4 payload: choices: - value: curriculum_counting_robot_musical_instrument-piano_gray-0138 @@ -211,16 +239,18 @@ payload: is_right_answer: true - value: curriculum_counting_robot_musical_instrument-piano_gray-0138 type: image + is_right_answer: true - value: curriculum_counting_robot_musical_instrument-piano_gray-0138 type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -228,6 +258,10 @@ payload: value: magicCardNumbers5Five options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 5 + maximumToSelect: 5 payload: choices: - value: curriculum_counting_robot_musical_instrument-saxophone_yellow-013C @@ -247,14 +281,15 @@ payload: is_right_answer: true - value: curriculum_counting_robot_musical_instrument-saxophone_yellow-013C type: image + is_right_answer: true - instructions: - locale: fr_FR - value: Regarde le chiffre sur Leka. Compte et touche, de gauche à droite et ligne par ligne, le même nombre d'éléments. + value: Regarde le chiffre sur Leka. Compte et touche exactement ce nombre d'éléments, ni plus ni moins. - locale: en_US - value: Look at the number on Leka. Count and touch, from left to right and line by line, the same number of elements. + value: Look at the number on Leka. Count and tap exactly that many items, no more and no fewer. interface: touchToSelect - gameplay: findTheRightAnswers + gameplay: findTheRightNumber action: type: robot value: @@ -262,6 +297,10 @@ payload: value: magicCardNumbers6Six options: shuffle_choices: false + validation: + type: manual + minimumToSelect: 6 + maximumToSelect: 6 payload: choices: - value: curriculum_counting_robot_musical_instrument-violin_brown-013D diff --git a/Modules/ContentKit/Sources/Content/_NewSystem/Exercise/Exercise+Gameplay.swift b/Modules/ContentKit/Sources/Content/_NewSystem/Exercise/Exercise+Gameplay.swift index ff7bebacee..3fb1b1c3cf 100644 --- a/Modules/ContentKit/Sources/Content/_NewSystem/Exercise/Exercise+Gameplay.swift +++ b/Modules/ContentKit/Sources/Content/_NewSystem/Exercise/Exercise+Gameplay.swift @@ -5,6 +5,7 @@ public enum NewExerciseGameplay: String, Decodable { case associateCategories case findTheRightAnswers + case findTheRightNumber case findTheRightOrder case openPlay } diff --git a/Modules/ContentKit/Sources/GameEngine/_NewSystem/Coordinators/CurrentExerciseCoordinator.swift b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Coordinators/CurrentExerciseCoordinator.swift index 895a49d54c..4afe430fa2 100644 --- a/Modules/ContentKit/Sources/GameEngine/_NewSystem/Coordinators/CurrentExerciseCoordinator.swift +++ b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Coordinators/CurrentExerciseCoordinator.swift @@ -5,6 +5,8 @@ import Combine import SwiftUI +private typealias TTSFindTheRightGameplayCoordinator = TTSGameplayCoordinatorProtocol & ExerciseCompletionObservable & ExerciseEvaluationStrategy + // MARK: - CurrentExerciseCoordinator public class CurrentExerciseCoordinator { @@ -49,13 +51,22 @@ public class CurrentExerciseCoordinator { } .frame(maxWidth: .infinity, maxHeight: .infinity) - case .findTheRightAnswers: + case .findTheRightAnswers, + .findTheRightNumber: let model = CoordinatorFindTheRightAnswersModel(data: payload) - let coordinator = TTSCoordinatorFindTheRightAnswers( - model: model, - action: exercise.action, - options: self.exercise.options - ) + let coordinator: TTSFindTheRightGameplayCoordinator = if gameplay == .findTheRightNumber { + TTSCoordinatorFindTheRightNumber( + model: model, + action: self.exercise.action, + options: self.exercise.options + ) + } else { + TTSCoordinatorFindTheRightAnswers( + model: model, + action: self.exercise.action, + options: self.exercise.options + ) + } let viewModel = TTSViewViewModel(coordinator: coordinator) TTSView(viewModel: viewModel) diff --git a/Modules/ContentKit/Sources/GameEngine/_NewSystem/Gameplays/NewGameplayFindTheRightNumber.swift b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Gameplays/NewGameplayFindTheRightNumber.swift new file mode 100644 index 0000000000..6e7b388808 --- /dev/null +++ b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Gameplays/NewGameplayFindTheRightNumber.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import RobotKit + +// MARK: - NewGameplayFindTheRightNumberChoiceModel + +public struct NewGameplayFindTheRightNumberChoiceModel: Identifiable { + // MARK: Lifecycle + + public init(id: UUID, isRightAnswer: Bool) { + self.id = id + self.isRightAnswer = isRightAnswer + } + + // MARK: Public + + public let id: UUID + + // MARK: Internal + + let isRightAnswer: Bool +} + +// MARK: - NewGameplayFindTheRightNumber + +public class NewGameplayFindTheRightNumber: GameplayProtocol { + // MARK: Lifecycle + + public init(choices: [NewGameplayFindTheRightNumberChoiceModel], requestedNumber: Int? = nil) { + self.choices = choices + self.rightAnswerIDs = Set(choices.filter(\.isRightAnswer).map(\.id)) + self.requestedNumber = requestedNumber ?? self.rightAnswerIDs.count + } + + // MARK: Public + + public let choices: [NewGameplayFindTheRightNumberChoiceModel] + public var isCompleted = CurrentValueSubject(false) + + public func process(choiceIDs: [UUID]) -> [(id: UUID, isCorrect: Bool)] { + let selectedChoiceIDs = Set(choiceIDs) + let hasNoDuplicateSelection = selectedChoiceIDs.count == choiceIDs.count + let hasRequestedNumberOfChoices = choiceIDs.count == self.requestedNumber + let hasOnlyRightAnswers = selectedChoiceIDs.isSubset(of: self.rightAnswerIDs) + let isSelectionCorrect = hasNoDuplicateSelection && hasRequestedNumberOfChoices && hasOnlyRightAnswers + + self.isCompleted.send(isSelectionCorrect) + + return choiceIDs.map { id in + (id, self.rightAnswerIDs.contains(id)) + } + } + + public func reset() { + self.isCompleted.send(false) + } + + // MARK: Internal + + typealias ChoiceType = NewGameplayFindTheRightNumberChoiceModel + + // MARK: Private + + private let requestedNumber: Int + private let rightAnswerIDs: Set +} diff --git a/Modules/ContentKit/Sources/GameEngine/_NewSystem/Interfaces/TTS/Coordinators/TTSCoordinator+FindTheRightNumber.swift b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Interfaces/TTS/Coordinators/TTSCoordinator+FindTheRightNumber.swift new file mode 100644 index 0000000000..26a8c142e9 --- /dev/null +++ b/Modules/ContentKit/Sources/GameEngine/_NewSystem/Interfaces/TTS/Coordinators/TTSCoordinator+FindTheRightNumber.swift @@ -0,0 +1,238 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI +import UtilsKit + +// MARK: - TTSCoordinatorFindTheRightNumber + +public class TTSCoordinatorFindTheRightNumber: TTSGameplayCoordinatorProtocol, ExerciseCompletionObservable { + // MARK: Lifecycle + + public init(choices: [CoordinatorFindTheRightAnswersChoiceModel], action: NewExerciseAction? = nil, options: NewExerciseOptions? = nil) { + let options = options ?? NewExerciseOptions() + self.rawChoices = options.shuffleChoices ? choices.shuffled() : choices + + self.gameplay = NewGameplayFindTheRightNumber( + choices: self.rawChoices + .map { .init(id: $0.id, isRightAnswer: $0.isRightAnswer) + }, + requestedNumber: Self.requestedNumber(from: options) + ) + + self.uiModel.value.action = action + self.uiModel.value.choices = self.rawChoices.map { choice in + let view = ChoiceView(value: choice.value, + type: choice.type, + size: self.uiModel.value.choiceSize(for: self.gameplay.choices.count), + state: .idle) + return TTSUIChoiceModel(id: choice.id, view: view) + } + self.validationState.value = .disabled + } + + public convenience init(model: CoordinatorFindTheRightAnswersModel, action: NewExerciseAction? = nil, options: NewExerciseOptions? = nil) { + self.init(choices: model.choices, action: action, options: options) + } + + // MARK: Public + + public private(set) var uiModel = CurrentValueSubject(.zero) + public private(set) var validationState = CurrentValueSubject(.disabled) + + public var didComplete: PassthroughSubject = .init() + + public func processUserSelection(choiceID: UUID) { + var choiceState: State { + if let index = currentChoices.firstIndex(where: { $0 == choiceID }) { + self.currentChoices.remove(at: index) + return .idle + } else { + self.currentChoices.append(choiceID) + return .selected + } + } + + self.updateChoiceState(for: choiceID, to: choiceState) + self.validationState.send(self.currentChoices.isNotEmpty ? .enabled : .disabled) + } + + public func validateUserSelection() { + self.completionData.numberOfTrials += 1 + + let choiceIDs = self.currentChoices.compactMap { choice in + self.rawChoices.first(where: { $0.id == choice })?.id + } + + let results = self.gameplay.process(choiceIDs: choiceIDs) + + guard results.allSatisfy(\.isCorrect), self.gameplay.isCompleted.value else { + results.forEach { result in + self.updateChoiceState(for: result.id, to: .idle) + } + + self.gameplay.reset() + self.resetCurrentChoices() + return + } + + results.forEach { result in + self.updateChoiceState(for: result.id, to: result.isCorrect ? .correct : .wrong) + } + + withAnimation { + self.validationState.send(.hidden) + } + + logGEK.debug("Exercise completed") + self.didComplete.send(self.completionData) + } + + // MARK: Private + + private let gameplay: NewGameplayFindTheRightNumber + + private var currentChoices: [UUID] = [] + private let rawChoices: [CoordinatorFindTheRightAnswersChoiceModel] + + private var completionData: ExerciseCompletionData = .init() + + private static func requestedNumber(from options: NewExerciseOptions) -> Int? { + guard case let .manualWithSelectionLimit(minimumToSelect, maximumToSelect) = options.validation else { + return nil + } + + return maximumToSelect ?? minimumToSelect + } + + private func resetCurrentChoices() { + self.currentChoices = [] + self.validationState.send(.disabled) + } + + private func updateChoiceState(for choiceID: UUID, to state: State) { + guard let index = self.rawChoices.firstIndex(where: { $0.id == choiceID }) else { return } + + let view = ChoiceView(value: self.rawChoices[index].value, + type: self.rawChoices[index].type, + size: self.uiModel.value.choiceSize(for: self.rawChoices.count), + state: state) + + let isChoiceDisabled = (state == .correct || state == .wrong) + self.uiModel.value.choices[index] = TTSUIChoiceModel(id: choiceID, view: view, disabled: isChoiceDisabled) + } +} + +extension TTSCoordinatorFindTheRightNumber { + enum State { + case idle + case selected + case correct + case wrong + } + + struct ChoiceView: View { + // MARK: Lifecycle + + init(value: String, type: ChoiceType, size: CGFloat, state: State) { + self.value = value + self.type = type + self.size = size + self.state = state + } + + // MARK: Internal + + var body: some View { + switch self.state { + case .correct: + TTSChoiceViewDefaultCorrect(value: self.value, type: self.type, size: self.size) + case .wrong: + TTSChoiceViewDefaultWrong(value: self.value, type: self.type, size: self.size) + case .selected: + TTSChoiceViewDefaultSelected(value: self.value, type: self.type, size: self.size) + case .idle: + TTSChoiceViewDefaultIdle(value: self.value, type: self.type, size: self.size) + } + } + + // MARK: Private + + private let value: String + private let type: ChoiceType + private let size: CGFloat + private let state: State + } +} + +// MARK: ExerciseEvaluationStrategy + +extension TTSCoordinatorFindTheRightNumber: ExerciseEvaluationStrategy { + public func evaluate(in context: EvaluationContext = .practice) -> ExerciseEvaluationLevel { + let numberOfTrials = self.completionData.numberOfTrials + let numberOfAllowedTrials = self.getNumberOfAllowedTrials(from: self.getEvaluationLUT(for: context)) + + let trialsPercentage = Double(numberOfAllowedTrials) / Double(numberOfTrials) * 100.0 + + switch trialsPercentage { + case 90...: + return .excellent + case 80..<90: + return .good + case 70..<80: + return .average + case 60..<70: + return .belowAverage + default: + return .fail + } + } + + private func getEvaluationLUT(for context: EvaluationContext) -> EvaluationLUT { + switch context { + default: + [ + 1: [1: 1], + 2: [1: 1, 2: 2], + 3: [1: 1, 2: 2, 3: 3], + 4: [1: 2, 2: 2, 3: 3, 4: 4], + 5: [1: 2, 2: 3, 3: 3, 4: 4, 5: 5], + 6: [1: 3, 2: 3, 3: 4, 4: 4, 5: 5, 6: 6], + ] + } + } + + private func getNumberOfAllowedTrials(from table: EvaluationLUT) -> Int { + let numberOfRightAnswers = self.rawChoices.filter(\.isRightAnswer).count + let numberOfChoices = self.rawChoices.count + + guard let number = table[numberOfChoices]?[numberOfRightAnswers] else { + logGEK.error("No number of allowed trials found for \(numberOfChoices) choices and \(numberOfRightAnswers) right answers") + fatalError("No number of allowed trials found for \(numberOfChoices) choices and \(numberOfRightAnswers) right answers") + } + + return number + } +} + +#if DEBUG + + #Preview { + let kDefaultChoices: [CoordinatorFindTheRightAnswersChoiceModel] = [ + .init(value: "Choice 1\nCorrect", isRightAnswer: true), + .init(value: "Choice 2\nCorrect", isRightAnswer: true), + .init(value: "Choice 3\nCorrect", isRightAnswer: true), + .init(value: "checkmark.seal.fill", type: .sfsymbol, isRightAnswer: false), + .init(value: "Choice 5", isRightAnswer: false), + .init(value: "exclamationmark.triangle.fill", type: .sfsymbol, isRightAnswer: false), + ] + + let coordinator = TTSCoordinatorFindTheRightNumber(choices: kDefaultChoices) + let viewModel = TTSViewViewModel(coordinator: coordinator) + + return TTSView(viewModel: viewModel) + } + +#endif diff --git a/Modules/ContentKit/Tests/GameEngine/CoordinatorGameplayChoiceModelDecode.swift b/Modules/ContentKit/Tests/GameEngine/CoordinatorGameplayChoiceModelDecode.swift index a21474c8c9..9a2ba4f182 100644 --- a/Modules/ContentKit/Tests/GameEngine/CoordinatorGameplayChoiceModelDecode.swift +++ b/Modules/ContentKit/Tests/GameEngine/CoordinatorGameplayChoiceModelDecode.swift @@ -55,6 +55,57 @@ final class CoordinatorGameplayModelDecode: XCTestCase { XCTAssertEqual(model.choices.count, 6) } + func test_FindTheRightNumber() throws { + let kExercise = + """ + instructions: + - locale: fr_FR + value: Touche le bon nombre d'emojis + - locale: en_US + value: Tap the right number of emojis + interface: touchToSelect + gameplay: findTheRightNumber + action: + type: robot + value: + type: image + value: magicCardNumbers3Three + options: + shuffle_choices: true + validation: + type: manual + minimumToSelect: 3 + maximumToSelect: 3 + payload: + choices: + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍉 + type: emoji + - value: 🐢 + type: emoji + - value: 🐢 + type: emoji + """ + + let exercise = Exercise(yaml: kExercise)! + + XCTAssertEqual(exercise.gameplay, .findTheRightNumber) + XCTAssertEqual(exercise.options?.validation, .manualWithSelectionLimit(minimumToSelect: 3, maximumToSelect: 3)) + + let model = try JSONDecoder().decode(CoordinatorFindTheRightAnswersModel.self, from: exercise.payload!) + + XCTAssertEqual(model.choices.count, 6) + XCTAssertEqual(model.choices.filter(\.isRightAnswer).count, 3) + } + func test_AssociateCategories() throws { let kExercise = """ diff --git a/Modules/ContentKit/Tests/GameEngine/NewGameplayFindTheRightNumber_Tests.swift b/Modules/ContentKit/Tests/GameEngine/NewGameplayFindTheRightNumber_Tests.swift new file mode 100644 index 0000000000..bbadebe637 --- /dev/null +++ b/Modules/ContentKit/Tests/GameEngine/NewGameplayFindTheRightNumber_Tests.swift @@ -0,0 +1,60 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +@testable import ContentKit + +final class NewGameplayFindTheRightNumber_Tests: XCTestCase { + // MARK: Internal + + func test_process_whenSelectionMatchesRequestedNumber_completes() { + let choices = self.makeChoices(numberOfRightAnswers: 4) + let gameplay = NewGameplayFindTheRightNumber(choices: choices, requestedNumber: 3) + + let results = gameplay.process(choiceIDs: [choices[0].id, choices[1].id, choices[2].id]) + + XCTAssertTrue(results.allSatisfy(\.isCorrect)) + XCTAssertTrue(gameplay.isCompleted.value) + } + + func test_process_whenSelectionHasTooFewRightAnswers_doesNotComplete() { + let choices = self.makeChoices() + let gameplay = NewGameplayFindTheRightNumber(choices: choices) + + let results = gameplay.process(choiceIDs: [choices[0].id, choices[1].id]) + + XCTAssertTrue(results.allSatisfy(\.isCorrect)) + XCTAssertFalse(gameplay.isCompleted.value) + } + + func test_process_whenSelectionHasDistractor_doesNotComplete() { + let choices = self.makeChoices() + let gameplay = NewGameplayFindTheRightNumber(choices: choices) + + let results = gameplay.process(choiceIDs: [choices[0].id, choices[1].id, choices[3].id]) + + XCTAssertEqual(results.map(\.isCorrect), [true, true, false]) + XCTAssertFalse(gameplay.isCompleted.value) + } + + func test_process_whenSelectionHasTooManyRightAnswers_doesNotComplete() { + let choices = self.makeChoices(numberOfRightAnswers: 4) + let gameplay = NewGameplayFindTheRightNumber(choices: choices, requestedNumber: 3) + + let results = gameplay.process(choiceIDs: [choices[0].id, choices[1].id, choices[2].id, choices[3].id]) + + XCTAssertEqual(results.map(\.isCorrect), [true, true, true, true]) + XCTAssertFalse(gameplay.isCompleted.value) + } + + // MARK: Private + + private func makeChoices(numberOfRightAnswers: Int = 3) -> [NewGameplayFindTheRightNumberChoiceModel] { + (0..<6).map { index in + .init(id: UUID(), isRightAnswer: index < numberOfRightAnswers) + } + } +} diff --git a/Specs/jtd/activity.jtd.json b/Specs/jtd/activity.jtd.json index f37fe463c6..421ff6a6ff 100644 --- a/Specs/jtd/activity.jtd.json +++ b/Specs/jtd/activity.jtd.json @@ -478,6 +478,7 @@ "$exercise/gameplay": { "enum": [ "findTheRightAnswers", + "findTheRightNumber", "associateCategories", "findTheRightOrder" ] diff --git a/Specs/jtd/new_activity.jtd.json b/Specs/jtd/new_activity.jtd.json index 59d9ee6f7a..22ba6bbe06 100644 --- a/Specs/jtd/new_activity.jtd.json +++ b/Specs/jtd/new_activity.jtd.json @@ -437,6 +437,7 @@ "enum": [ "associateCategories", "findTheRightAnswers", + "findTheRightNumber", "findTheRightOrder", "openPlay" ]