Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mathieu/feature/Add AssociationSix to GEK #455

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Leka - iOS Monorepo
# Copyright 2023 APF France handicap
# SPDX-License-Identifier: Apache-2.0

id: 5e5394ee819a11eeb9620242ac120002
name: Association - Two Categories Of Three Items - Images
description: L'objectif est de trier les images par categories.
image: activity_image_recognition_1
sequence:
- exercises:
- instructions: Rassemble les instruments par familles d'instruments
type: association
interface: association
gameplay: association
payload:
type: association
shuffle_choices: true
choices:
- value: image-instrument-violin-no_background
type: image
category: catA
- value: image-instrument-guitar-no_background
type: image
category: catA
- value: image-instrument-piano-no_background
type: image
category: catA
- value: image-instrument-flute-no_background
type: image
category: catB
- value: image-instrument-saxophone-no_background
type: image
category: catB
- value: image-instrument-harmonica-no_background
type: image
category: catB


- instructions: Rassemble les instruments par familles d'instruments
type: association
interface: association
gameplay: association
payload:
type: association
shuffle_choices: true
choices:
- value: image-instrument-harmonica-no_background
type: image
category: catB
- value: image-instrument-violin-no_background
type: image
category: catA
- value: image-instrument-piano-no_background
type: image
category: catA
- value: image-instrument-saxophone-no_background
type: image
category: catB
- value: image-instrument-guitar-no_background
type: image
category: catA
- value: image-instrument-flute-no_background
type: image
category: catB


- instructions: Rassemble les instruments par familles d'instruments
type: association
interface: association
gameplay: association
payload:
type: association
shuffle_choices: true
choices:
- value: image-instrument-piano-no_background
type: image
category: catA
- value: image-instrument-harmonica-no_background
type: image
category: catB
- value: image-instrument-guitar-no_background
type: image
category: catA
- value: image-instrument-flute-no_background
type: image
category: catB
- value: image-instrument-violin-no_background
type: image
category: catA
- value: image-instrument-saxophone-no_background
type: image
category: catB



Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

id: 24bf568972da46c294f1737c3ed36994
name: Association - Two Categories - Multiple Right Answers - Images
name: Association - Two Categories Of Two Items - Images
description: L'objectif est de trier les images par categories.
image: activity_image_recognition_1
sequence:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ let kActivities: [Activity] = [
// ContentKit.decodeActivity("activity-dragAndDrop-two_zones-multiple_right_answer-mixed"),

ContentKit.decodeActivity("activity-medley"),
ContentKit.decodeActivity("activity-association-two_categories-multiple_right_answers-images"),
ContentKit.decodeActivity("activity-association-two_categories_of_two_items-images"),
ContentKit.decodeActivity("activity-association-two_categories_of_three_items-images"),
]

struct GEKNewSystemView: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ public struct CharacteristicModelNotifying {
public let cbCharacteristic: CBCharacteristic?
public let onNotification: Callback?

public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, cbCharacteristic: CBCharacteristic? = nil, onNotification: Callback? = nil ) {
public init(
characteristicUUID: CBUUID, serviceUUID: CBUUID, cbCharacteristic: CBCharacteristic? = nil,
onNotification: Callback? = nil
) {
self.characteristicUUID = characteristicUUID
self.serviceUUID = serviceUUID
self.cbCharacteristic = cbCharacteristic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct CharacteristicModelWriteOnly {
public let serviceUUID: CBUUID
public let onWrite: Callback?

public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onWrite: Callback? = nil ) {
public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onWrite: Callback? = nil) {
self.characteristicUUID = characteristicUUID
self.serviceUUID = serviceUUID
self.onWrite = onWrite
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class GameplayAssociation<ChoiceModelType>: StatefulGameplayProtocol
where ChoiceModelType: GameplayChoiceModelProtocol {

var choices: CurrentValueSubject<[GameplayAssociationChoiceModel], Never> = .init([])
var rightAnswers: [ChoiceModelType] = []
var state: CurrentValueSubject<ExerciseState, Never> = .init(.idle)

func updateChoice(_ choice: ChoiceModelType, state: GameplayChoiceState) {
Expand All @@ -24,7 +23,7 @@ where ChoiceModelType: GameplayChoiceModelProtocol {

extension GameplayAssociation where ChoiceModelType == GameplayAssociationChoiceModel {

convenience init(choices: [GameplayAssociationChoiceModel]) {
convenience init(choices: [GameplayAssociationChoiceModel], shuffle: Bool = false) {
self.init()
self.choices.send(choices)
self.state.send(.playing)
Expand All @@ -33,15 +32,16 @@ extension GameplayAssociation where ChoiceModelType == GameplayAssociationChoice
func process(_ choice: ChoiceModelType, _ destination: ChoiceModelType) {
if choice.choice.category == destination.choice.category {
updateChoice(choice, state: .rightAnswer)
rightAnswers.append(choice)
rightAnswers.append(destination)
updateChoice(destination, state: .rightAnswer)

} else {
updateChoice(choice, state: .wrongAnswer)
}

if rightAnswers.count == self.choices.value.count {
state.send(.completed)
guard choices.value.allSatisfy({ $0.state == .rightAnswer }) else {
return
}
state.send(.completed)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import SpriteKit

final class DragAndDropAssociationFourChoicesScene: DragAndDropAssociationBaseScene {

override func setFirstAnswerPosition() {
spacer = 455
initialNodeX = (size.width - spacer) / 2
verticalSpacing = self.size.height / 3
defaultPosition = CGPoint(x: initialNodeX, y: verticalSpacing - 30)
}

override func setNextAnswerPosition(_ index: Int) {
if [0, 2].contains(index) {
defaultPosition.x += spacer
} else {
defaultPosition.x = initialNodeX
defaultPosition.y += verticalSpacing + 60
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Leka - iOS Monorepo
// Copyright 2023 APF France handicap
// SPDX-License-Identifier: Apache-2.0

import SpriteKit

final class DragAndDropAssociationSixChoicesScene: DragAndDropAssociationBaseScene {

override func setFirstAnswerPosition() {
spacer = 340
initialNodeX = (size.width - (spacer * 2)) / 2
verticalSpacing = self.size.height / 3
defaultPosition = CGPoint(x: initialNodeX, y: verticalSpacing - 30)
}

override func setNextAnswerPosition(_ index: Int) {
if [0, 1, 3, 4].contains(index) {
defaultPosition.x += spacer
} else {
defaultPosition.x = initialNodeX
defaultPosition.y += verticalSpacing + 60
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,21 @@ import SwiftUI

class DragAndDropAssociationBaseScene: SKScene {
var viewModel: DragAndDropAssociationViewViewModel
var spacer = CGFloat.zero
var defaultPosition = CGPoint.zero
var initialNodeX: CGFloat = .zero
var verticalSpacing: CGFloat = .zero
private var biggerSide: CGFloat = 150
private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:]
private var playedNode: DraggableImageAnswerNode?
private var spacer: CGFloat = 455
private var defaultPosition = CGPoint.zero
private var expectedItemsNodes: [String: [SKSpriteNode]] = [:]
private var playedDestination: DraggableImageAnswerNode?
private var dropDestinations: [DraggableImageAnswerNode] = []
private var dropDestinationAnchor: CGPoint = .zero
private var initialNodeX: CGFloat = .zero
private var verticalSpacing: CGFloat = .zero
private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:]
private var expectedItemsNodes: [String: [SKSpriteNode]] = [:]
private var cancellables: Set<AnyCancellable> = []

init(viewModel: DragAndDropAssociationViewViewModel) {
self.viewModel = viewModel
super.init(size: CGSize.zero)
self.defaultPosition = CGPoint(x: spacer, y: self.size.height)

subscribeToChoicesUpdates()
}
Expand Down Expand Up @@ -92,7 +91,7 @@ class DragAndDropAssociationBaseScene: SKScene {
}
}

func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 120) {
func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 80) {
let xRange = SKRange(lowerLimit: 0, upperLimit: size.width - limit)
let yRange = SKRange(lowerLimit: 0, upperLimit: size.height - limit)
for node in nodes {
Expand All @@ -101,27 +100,18 @@ class DragAndDropAssociationBaseScene: SKScene {
}

func setFirstAnswerPosition() {
initialNodeX = (size.width - spacer) / 2
verticalSpacing = self.size.height / 3
defaultPosition = CGPoint(x: initialNodeX, y: verticalSpacing - 30)
fatalError("setFirstAnswerPosition() has not been implemented")
}

func setNextAnswerPosition(_ index: Int) {
if [0, 2].contains(index) {
defaultPosition.x += spacer
} else {
defaultPosition.x = initialNodeX
defaultPosition.y += verticalSpacing + 60
}
fatalError("setNextAnswerPosition(_ index:) has not been implemented")
}

func goodAnswerBehavior(_ node: DraggableImageAnswerNode) {
node.scaleForMax(sizeOf: biggerSide * 0.8)
node.position = CGPoint(
x: dropDestinationAnchor.x - 60,
y: dropDestinationAnchor.y - 60)
node.zPosition = 10
node.zPosition = (self.playedDestination?.zPosition ?? 10) + 10
node.isDraggable = false
playedDestination?.isDraggable = false
onDropAction(node)
if viewModel.exercicesSharedData.state == .completed {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [self] in
Expand Down Expand Up @@ -193,7 +183,7 @@ class DragAndDropAssociationBaseScene: SKScene {
node.run(SKAction.move(to: location, duration: 0.05).moveAnimation(.linear))
node.position = location
} else {
self.touchesEnded(touches, with: event)
wrongAnswerBehavior(node)
}
}
}
Expand All @@ -207,7 +197,6 @@ class DragAndDropAssociationBaseScene: SKScene {
playedNode = selectedNodes[touch]!
playedNode!.scaleForMax(sizeOf: biggerSide)

// make dropArea out of target node
guard
let destinationNode = dropDestinations.first(where: {
$0.frame.contains(touch.location(in: self)) && $0.name != playedNode!.name
Expand All @@ -216,15 +205,13 @@ class DragAndDropAssociationBaseScene: SKScene {
wrongAnswerBehavior(playedNode!)
break
}
dropDestinationAnchor = destinationNode.position
playedDestination = destinationNode

guard let destination = viewModel.choices.first(where: { $0.choice.value == destinationNode.name })
else { return }
guard let choice = viewModel.choices.first(where: { $0.choice.value == playedNode!.name })
else { return }

// dropped within the bounds of the proper sibling
destinationNode.isDraggable = false
viewModel.onChoiceTapped(choice: choice, destination: destination)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public struct DragAndDropAssociationView: View {
@StateObject private var viewModel: DragAndDropAssociationViewViewModel
@State private var scene: SKScene = SKScene()

public init(choices: [AssociationChoice]) {
public init(choices: [AssociationChoice], shuffle: Bool = false) {
self._viewModel = StateObject(
wrappedValue: DragAndDropAssociationViewViewModel(choices: choices)
wrappedValue: DragAndDropAssociationViewViewModel(choices: choices, shuffle: shuffle)
)
}

Expand All @@ -23,9 +23,13 @@ public struct DragAndDropAssociationView: View {
fatalError("Exercise payload is not .association")
}

// self._viewModel = StateObject(wrappedValue: DragAndDropAssociationViewViewModel(choices: payload.choices))
self._viewModel = StateObject(
wrappedValue: DragAndDropAssociationViewViewModel(choices: payload.choices, shared: data))
wrappedValue: DragAndDropAssociationViewViewModel(
choices: payload.choices,
shuffle: payload.shuffleChoices,
shared: data
)
)
}

public var body: some View {
Expand All @@ -36,7 +40,11 @@ public struct DragAndDropAssociationView: View {
)
.frame(width: proxy.size.width, height: proxy.size.height)
.onAppear {
scene = DragAndDropAssociationBaseScene(viewModel: viewModel)
if viewModel.choices.count == 4 {
scene = DragAndDropAssociationFourChoicesScene(viewModel: viewModel)
} else {
scene = DragAndDropAssociationSixChoicesScene(viewModel: viewModel)
}
}
}
.edgesIgnoringSafeArea(.horizontal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ class DragAndDropAssociationViewViewModel: ObservableObject {
private let gameplay: GameplayAssociation<GameplayAssociationChoiceModel>
private var cancellables: Set<AnyCancellable> = []

init(choices: [AssociationChoice], shared: ExerciseSharedData? = nil) {
init(choices: [AssociationChoice], shuffle: Bool = false, shared: ExerciseSharedData? = nil) {
let gameplayChoiceModel = choices.map { GameplayAssociationChoiceModel(choice: $0) }
self.choices = gameplayChoiceModel
self.gameplay = GameplayAssociation(choices: gameplayChoiceModel)
self.choices = shuffle ? gameplayChoiceModel.shuffled() : gameplayChoiceModel
self.gameplay = GameplayAssociation(choices: gameplayChoiceModel, shuffle: shuffle)
self.exercicesSharedData = shared ?? ExerciseSharedData()

subscribeToGameplayDragAndDropChoicesUpdates()
subscribeToGameplayDragAndDropAssociationChoicesUpdates()
subscribeToGameplayStateUpdates()
}

public func onChoiceTapped(choice: GameplayAssociationChoiceModel, destination: GameplayAssociationChoiceModel) {
gameplay.process(choice, destination)
}

private func subscribeToGameplayDragAndDropChoicesUpdates() {
private func subscribeToGameplayDragAndDropAssociationChoicesUpdates() {
gameplay.choices
.receive(on: DispatchQueue.main)
.sink {
Expand Down
Loading
Loading