From 3d0378109bd00e8f1f89f82c22b2b7ebaf861456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Maximilian=20Kr=C3=BCger?= Date: Sun, 7 Mar 2021 17:28:13 +0100 Subject: [PATCH] added instruction set includes basic instructions for wall, clock and hand and task control includes clock selection with hands includes basic task that holds a list of instructions added some tests #2 moved validation to HandInteractionService --- src/instructionset/InstructionSet.kt | 54 +++++++++ src/instructionset/Task.kt | 26 +++++ .../clockselections/ClockSelection.kt | 12 ++ .../clockselections/ClockSelectionParser.kt | 25 ++++ .../clockselections/RangeClockSelection.kt | 25 ++++ .../clockselections/SingleClockSelection.kt | 20 ++++ .../instructions/Instruction.kt | 8 ++ .../instructions/InstructionParser.kt | 9 ++ .../instructions/basic/RepeatInstruction.kt | 28 +++++ .../instructions/basic/SleepInstruction.kt | 27 +++++ .../clock/CalibrateInstruction.kt | 23 ++++ .../instructions/clock/ClockInstruction.kt | 18 +++ .../clock/ClockInstructionParser.kt | 21 ++++ .../instructions/hand/DirectionInstruction.kt | 25 ++++ .../instructions/hand/HandInstruction.kt | 23 ++++ .../instructions/hand/PositionInstruction.kt | 38 ++++++ .../instructions/hand/ResetInstruction.kt | 24 ++++ .../instructions/hand/SpeedInstruction.kt | 24 ++++ .../instructions/hand/WaitStepsInstruction.kt | 24 ++++ .../instructions/wall/RunInstruction.kt | 24 ++++ .../instructions/wall/WallInstruction.kt | 14 +++ src/services/HandInteractionService.kt | 10 +- src/util/ADClockUtils.kt | 11 +- test/instructions/ClockSelectionParserTest.kt | 110 ++++++++++++++++++ test/instructions/InstructionParserTest.kt | 105 +++++++++++++++++ 25 files changed, 722 insertions(+), 6 deletions(-) create mode 100644 src/instructionset/InstructionSet.kt create mode 100644 src/instructionset/Task.kt create mode 100644 src/instructionset/clockselections/ClockSelection.kt create mode 100644 src/instructionset/clockselections/ClockSelectionParser.kt create mode 100644 src/instructionset/clockselections/RangeClockSelection.kt create mode 100644 src/instructionset/clockselections/SingleClockSelection.kt create mode 100644 src/instructionset/instructions/Instruction.kt create mode 100644 src/instructionset/instructions/InstructionParser.kt create mode 100644 src/instructionset/instructions/basic/RepeatInstruction.kt create mode 100644 src/instructionset/instructions/basic/SleepInstruction.kt create mode 100644 src/instructionset/instructions/clock/CalibrateInstruction.kt create mode 100644 src/instructionset/instructions/clock/ClockInstruction.kt create mode 100644 src/instructionset/instructions/clock/ClockInstructionParser.kt create mode 100644 src/instructionset/instructions/hand/DirectionInstruction.kt create mode 100644 src/instructionset/instructions/hand/HandInstruction.kt create mode 100644 src/instructionset/instructions/hand/PositionInstruction.kt create mode 100644 src/instructionset/instructions/hand/ResetInstruction.kt create mode 100644 src/instructionset/instructions/hand/SpeedInstruction.kt create mode 100644 src/instructionset/instructions/hand/WaitStepsInstruction.kt create mode 100644 src/instructionset/instructions/wall/RunInstruction.kt create mode 100644 src/instructionset/instructions/wall/WallInstruction.kt create mode 100644 test/instructions/ClockSelectionParserTest.kt create mode 100644 test/instructions/InstructionParserTest.kt diff --git a/src/instructionset/InstructionSet.kt b/src/instructionset/InstructionSet.kt new file mode 100644 index 0000000..6dc1a94 --- /dev/null +++ b/src/instructionset/InstructionSet.kt @@ -0,0 +1,54 @@ +@file:Suppress("UNCHECKED_CAST") + +package com.adclock.instructionset + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.clockselections.ClockSelectionParser +import com.adclock.instructionset.instructions.Instruction +import com.adclock.instructionset.instructions.InstructionParser +import com.adclock.util.findSubclasses +import kotlin.reflect.full.companionObjectInstance + +object InstructionSet { + + private val instructionParsers = findSubclasses(this::class.java.`package`.name, Instruction::class) + .associateWith { it.companionObjectInstance as? InstructionParser<*> } + + private val selectionParsers = findSubclasses(this::class.java.`package`.name, ClockSelection::class) + .associateWith { it.companionObjectInstance as? ClockSelectionParser<*> } + + fun deserialize(input: String): Instruction { + val instruction = input.take(3) + val parser = instructionParsers.values.find { instruction == it?.key } + ?: throw IllegalArgumentException("No parser found for instruction $instruction.") + + if (input.length > 3 && input[3] != ' ') + throw IllegalArgumentException("Blank after instruction expected.") + + return parser.deserialize(input.drop(4)) + } + + + fun serialize(instruction: Instruction): String { + val parser = instructionParsers[instruction::class] + ?: throw IllegalArgumentException("No parser found for instruction type ${instruction::class.simpleName}.") + parser as InstructionParser + return "${parser.key.padEnd(3)} ${parser.serialize(instruction)}".trim() + } + + fun deserializeSelection(selection: String) = + selection.split(",").map { split -> + selectionParsers.values.find { it?.deserializable(split) ?: false }?.deserialize(split) + ?: throw IllegalArgumentException("No parser found for selection part $split") + } + + + fun serializeSelection(selection: ClockSelection): String { + val parser = selectionParsers[selection::class] + ?: throw IllegalArgumentException("No parser found for instruction type ${selection::class.simpleName}.") + parser as ClockSelectionParser + return parser.serialize(selection) + } + + fun serializeSelection(selections: List) = selections.joinToString(",") { serializeSelection(it) } +} \ No newline at end of file diff --git a/src/instructionset/Task.kt b/src/instructionset/Task.kt new file mode 100644 index 0000000..b82e258 --- /dev/null +++ b/src/instructionset/Task.kt @@ -0,0 +1,26 @@ +package com.adclock.instructionset + +import com.adclock.instructionset.instructions.Instruction +import com.adclock.services.WallInteractionService + +class Task(val instructions: List) { + private var current: Int = 0 + var sleepUntil: Long = 0 + internal set + + fun isCompleted() = current >= instructions.size + + fun apply(wallService: WallInteractionService) { + if (isCompleted()) + throw IllegalStateException("This task is already completed. Can't perform more steps.") + + if (sleepUntil > System.currentTimeMillis()) + return + + val instruction = instructions[current] + if (instruction.apply(this, wallService)) + current++ + } + + internal fun restart() { current = 0 } +} \ No newline at end of file diff --git a/src/instructionset/clockselections/ClockSelection.kt b/src/instructionset/clockselections/ClockSelection.kt new file mode 100644 index 0000000..a971360 --- /dev/null +++ b/src/instructionset/clockselections/ClockSelection.kt @@ -0,0 +1,12 @@ +package com.adclock.instructionset.clockselections + +import com.adclock.model.Clock +import com.adclock.model.HandType + + +interface ClockSelection { + val hands: Array + + fun selected(clock: Clock): Boolean +} + diff --git a/src/instructionset/clockselections/ClockSelectionParser.kt b/src/instructionset/clockselections/ClockSelectionParser.kt new file mode 100644 index 0000000..a7f8291 --- /dev/null +++ b/src/instructionset/clockselections/ClockSelectionParser.kt @@ -0,0 +1,25 @@ +package com.adclock.instructionset.clockselections + +import com.adclock.model.HandType + +interface ClockSelectionParser { + fun deserializable(string: String): Boolean + + fun deserialize(hands: Array, string: String): T + fun serializeParameters(selection: T): String + + fun deserialize(string: String): T { + var clockPart = string + val hands = mutableListOf() + while (clockPart.isNotBlank() && clockPart.last() in HandType.shortNames) { + hands += HandType.valueOfShort(clockPart.last()) + clockPart = clockPart.dropLast(1) + } + return deserialize(hands.distinct().sorted().toTypedArray().ifEmpty { HandType.values() }, clockPart) + } + + fun serialize(selection: T): String { + val hands = selection.hands.joinToString("") { it.short.toString() } + return serializeParameters(selection) + hands + } +} \ No newline at end of file diff --git a/src/instructionset/clockselections/RangeClockSelection.kt b/src/instructionset/clockselections/RangeClockSelection.kt new file mode 100644 index 0000000..e01e0aa --- /dev/null +++ b/src/instructionset/clockselections/RangeClockSelection.kt @@ -0,0 +1,25 @@ +package com.adclock.instructionset.clockselections + +import com.adclock.model.Clock +import com.adclock.model.HandType + +class RangeClockSelection( + val range: IntRange, + override val hands: Array +) : ClockSelection { + override fun selected(clock: Clock) = clock.id in range + + companion object : ClockSelectionParser { + override fun deserializable(string: String) = "-" in string + + override fun deserialize(hands: Array, string: String): RangeClockSelection { + val from = string.substringBefore("-") + val to = string.substringAfter("-") + val range = from.toInt()..to.toInt() + require(!range.isEmpty()) { "Range ${range.first} to ${range.last} does not select any clocks." } + return RangeClockSelection(range, hands) + } + + override fun serializeParameters(selection: RangeClockSelection) = "${selection.range.first}-${selection.range.last}" + } +} \ No newline at end of file diff --git a/src/instructionset/clockselections/SingleClockSelection.kt b/src/instructionset/clockselections/SingleClockSelection.kt new file mode 100644 index 0000000..b7b6dad --- /dev/null +++ b/src/instructionset/clockselections/SingleClockSelection.kt @@ -0,0 +1,20 @@ +package com.adclock.instructionset.clockselections + +import com.adclock.model.Clock +import com.adclock.model.HandType + +class SingleClockSelection( + val id: Int, + override val hands: Array +) : ClockSelection { + override fun selected(clock: Clock) = clock.id == id + + companion object : ClockSelectionParser { + override fun deserializable(string: String) = "-" !in string + + override fun deserialize(hands: Array, string: String) = + SingleClockSelection(string.toInt(), hands) + + override fun serializeParameters(selection: SingleClockSelection) = selection.id.toString() + } +} \ No newline at end of file diff --git a/src/instructionset/instructions/Instruction.kt b/src/instructionset/instructions/Instruction.kt new file mode 100644 index 0000000..60f8310 --- /dev/null +++ b/src/instructionset/instructions/Instruction.kt @@ -0,0 +1,8 @@ +package com.adclock.instructionset.instructions + +import com.adclock.instructionset.Task +import com.adclock.services.WallInteractionService + +interface Instruction { + fun apply(task: Task, wallService: WallInteractionService): Boolean +} diff --git a/src/instructionset/instructions/InstructionParser.kt b/src/instructionset/instructions/InstructionParser.kt new file mode 100644 index 0000000..e497306 --- /dev/null +++ b/src/instructionset/instructions/InstructionParser.kt @@ -0,0 +1,9 @@ +package com.adclock.instructionset.instructions + +interface InstructionParser { + val key: String + + fun deserialize(input: String): T + + fun serialize(instruction: T): String +} \ No newline at end of file diff --git a/src/instructionset/instructions/basic/RepeatInstruction.kt b/src/instructionset/instructions/basic/RepeatInstruction.kt new file mode 100644 index 0000000..1c8fff8 --- /dev/null +++ b/src/instructionset/instructions/basic/RepeatInstruction.kt @@ -0,0 +1,28 @@ +package com.adclock.instructionset.instructions.basic + +import com.adclock.instructionset.Task +import com.adclock.instructionset.instructions.Instruction +import com.adclock.instructionset.instructions.InstructionParser +import com.adclock.services.WallInteractionService + +class RepeatInstruction : Instruction { + + override fun apply(task: Task, wallService: WallInteractionService): Boolean { + task.restart() + return false + } + + + companion object : InstructionParser { + override val key: String + get() = "RPT" + + override fun deserialize(input: String): RepeatInstruction { + require(input.isBlank()) { "No parameters for repeat instruction expected, but was $input." } + return RepeatInstruction() + } + + override fun serialize(instruction: RepeatInstruction) = "" + } + +} diff --git a/src/instructionset/instructions/basic/SleepInstruction.kt b/src/instructionset/instructions/basic/SleepInstruction.kt new file mode 100644 index 0000000..f6a698a --- /dev/null +++ b/src/instructionset/instructions/basic/SleepInstruction.kt @@ -0,0 +1,27 @@ +package com.adclock.instructionset.instructions.basic + +import com.adclock.instructionset.Task +import com.adclock.instructionset.instructions.Instruction +import com.adclock.instructionset.instructions.InstructionParser +import com.adclock.services.WallInteractionService + +class SleepInstruction(val sleep: Int) : Instruction { + + override fun apply(task: Task, wallService: WallInteractionService): Boolean { + task.sleepUntil = System.currentTimeMillis() + sleep * 1000 + return true + } + + companion object : InstructionParser { + override val key: String + get() = "SLP" + + override fun deserialize(input: String): SleepInstruction { + require(input.isNotBlank()) { "Parameter sleep time (like 5) expected." } + return SleepInstruction(input.toInt()) + } + + override fun serialize(instruction: SleepInstruction) = instruction.sleep.toString() + } + +} diff --git a/src/instructionset/instructions/clock/CalibrateInstruction.kt b/src/instructionset/instructions/clock/CalibrateInstruction.kt new file mode 100644 index 0000000..db965bf --- /dev/null +++ b/src/instructionset/instructions/clock/CalibrateInstruction.kt @@ -0,0 +1,23 @@ +package com.adclock.instructionset.instructions.clock + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.model.Clock +import com.adclock.services.ClockInteractionService + +class CalibrateInstruction(selection: List) : ClockInstruction(selection) { + override fun apply(clockService: ClockInteractionService, clock: Clock) { + clockService.sendInit(clock) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "CAL" + + override fun deserialize(selection: List, parameters: String): CalibrateInstruction { + require(parameters.isBlank()) { "No parameters for calibrate instruction expected, but was $parameters." } + return CalibrateInstruction(selection) + } + + override fun serializeParameters(instruction: CalibrateInstruction) = "" + } +} diff --git a/src/instructionset/instructions/clock/ClockInstruction.kt b/src/instructionset/instructions/clock/ClockInstruction.kt new file mode 100644 index 0000000..9a46477 --- /dev/null +++ b/src/instructionset/instructions/clock/ClockInstruction.kt @@ -0,0 +1,18 @@ +package com.adclock.instructionset.instructions.clock + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.wall.WallInstruction +import com.adclock.model.Clock +import com.adclock.services.ClockInteractionService +import com.adclock.services.WallInteractionService +import com.adclock.util.filter + +abstract class ClockInstruction(internal val selection: List) : WallInstruction { + abstract fun apply(clockService: ClockInteractionService, clock: Clock) + + override fun apply(wallService: WallInteractionService) { + wallService.wall.clocks.filter(selection).forEach { + apply(wallService.clockService, it) + } + } +} diff --git a/src/instructionset/instructions/clock/ClockInstructionParser.kt b/src/instructionset/instructions/clock/ClockInstructionParser.kt new file mode 100644 index 0000000..3e46109 --- /dev/null +++ b/src/instructionset/instructions/clock/ClockInstructionParser.kt @@ -0,0 +1,21 @@ +package com.adclock.instructionset.instructions.clock + +import com.adclock.instructionset.InstructionSet +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.InstructionParser + +interface ClockInstructionParser : InstructionParser { + + fun deserialize(selection: List, parameters: String): T + fun serializeParameters(instruction: T): String + + override fun deserialize(input: String): T { + val selection = InstructionSet.deserializeSelection(input.substringBefore(' ')) + return deserialize(selection, input.substringAfter(' ', "")) + } + + override fun serialize(instruction: T): String { + return InstructionSet.serializeSelection(instruction.selection) + " " + serializeParameters(instruction) + } + +} \ No newline at end of file diff --git a/src/instructionset/instructions/hand/DirectionInstruction.kt b/src/instructionset/instructions/hand/DirectionInstruction.kt new file mode 100644 index 0000000..1fd7707 --- /dev/null +++ b/src/instructionset/instructions/hand/DirectionInstruction.kt @@ -0,0 +1,25 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstructionParser +import com.adclock.model.Direction +import com.adclock.model.Hand +import com.adclock.services.HandInteractionService + +class DirectionInstruction(selection: List, val direction: Direction) : HandInstruction(selection) { + override fun apply(handService: HandInteractionService, hand: Hand) { + handService.setPlannedDirection(hand, direction) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "DIR" + + override fun deserialize(selection: List, parameters: String): DirectionInstruction { + require(parameters.isNotBlank()) { "Parameter direction ${Direction.values().map { it.name }} expected." } + return DirectionInstruction(selection, Direction.valueOf(parameters)) + } + + override fun serializeParameters(instruction: DirectionInstruction) = instruction.direction.name + } +} diff --git a/src/instructionset/instructions/hand/HandInstruction.kt b/src/instructionset/instructions/hand/HandInstruction.kt new file mode 100644 index 0000000..7a0cc1e --- /dev/null +++ b/src/instructionset/instructions/hand/HandInstruction.kt @@ -0,0 +1,23 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstruction +import com.adclock.model.Clock +import com.adclock.model.Hand +import com.adclock.model.HandType +import com.adclock.services.ClockInteractionService +import com.adclock.services.HandInteractionService + +abstract class HandInstruction(selection: List) : ClockInstruction(selection) { + abstract fun apply(handService: HandInteractionService, hand: Hand) + + override fun apply(clockService: ClockInteractionService, clock: Clock) { + val selectedHands = selection.find { it.selected(clock) }?.hands ?: emptyArray() + + if (HandType.HOUR in selectedHands) + apply(clockService.handService, clock.hour) + + if (HandType.MINUTE in selectedHands) + apply(clockService.handService, clock.minute) + } +} diff --git a/src/instructionset/instructions/hand/PositionInstruction.kt b/src/instructionset/instructions/hand/PositionInstruction.kt new file mode 100644 index 0000000..ace6f5c --- /dev/null +++ b/src/instructionset/instructions/hand/PositionInstruction.kt @@ -0,0 +1,38 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstructionParser +import com.adclock.model.Hand +import com.adclock.services.HandInteractionService + +class PositionInstruction( + selection: List, + val position: Int, + val relative: Boolean = false +) : HandInstruction(selection) { + override fun apply(handService: HandInteractionService, hand: Hand) { + handService.setPlannedPosition(hand, position, relative) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "POS" + + override fun deserialize(selection: List, parameters: String): PositionInstruction { + require(parameters.isNotBlank()) { "Parameter position (like 132R) expected." } + var relative = false + var positionString = parameters + if (positionString.last() == 'R') { + positionString = positionString.dropLast(1) + relative = true + } else if (positionString.last() == 'A') { + positionString = positionString.dropLast(1) + relative = false + } + return PositionInstruction(selection, positionString.toInt(), relative) + } + + override fun serializeParameters(instruction: PositionInstruction) = + "${instruction.position}${if (instruction.relative) "R" else "A"}" + } +} diff --git a/src/instructionset/instructions/hand/ResetInstruction.kt b/src/instructionset/instructions/hand/ResetInstruction.kt new file mode 100644 index 0000000..1b88b85 --- /dev/null +++ b/src/instructionset/instructions/hand/ResetInstruction.kt @@ -0,0 +1,24 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstructionParser +import com.adclock.model.Hand +import com.adclock.services.HandInteractionService + +class ResetInstruction(selection: List) : HandInstruction(selection) { + override fun apply(handService: HandInteractionService, hand: Hand) { + handService.resetPlanned(hand) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "RST" + + override fun deserialize(selection: List, parameters: String): ResetInstruction { + if (parameters.isNotBlank()) throw IllegalArgumentException("No parameters for reset instruction expected.") + return ResetInstruction(selection) + } + + override fun serializeParameters(instruction: ResetInstruction) = "" + } +} diff --git a/src/instructionset/instructions/hand/SpeedInstruction.kt b/src/instructionset/instructions/hand/SpeedInstruction.kt new file mode 100644 index 0000000..5c129f0 --- /dev/null +++ b/src/instructionset/instructions/hand/SpeedInstruction.kt @@ -0,0 +1,24 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstructionParser +import com.adclock.model.Hand +import com.adclock.services.HandInteractionService + +class SpeedInstruction(selection: List, val speed: Int) : HandInstruction(selection) { + override fun apply(handService: HandInteractionService, hand: Hand) { + handService.setPlannedSpeed(hand, speed) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "SPD" + + override fun deserialize(selection: List, parameters: String): SpeedInstruction { + require(parameters.isNotBlank()) { "Parameter speed (like 5) expected." } + return SpeedInstruction(selection, parameters.toInt()) + } + + override fun serializeParameters(instruction: SpeedInstruction) = instruction.speed.toString() + } +} diff --git a/src/instructionset/instructions/hand/WaitStepsInstruction.kt b/src/instructionset/instructions/hand/WaitStepsInstruction.kt new file mode 100644 index 0000000..412bdb1 --- /dev/null +++ b/src/instructionset/instructions/hand/WaitStepsInstruction.kt @@ -0,0 +1,24 @@ +package com.adclock.instructionset.instructions.hand + +import com.adclock.instructionset.clockselections.ClockSelection +import com.adclock.instructionset.instructions.clock.ClockInstructionParser +import com.adclock.model.Hand +import com.adclock.services.HandInteractionService + +class WaitStepsInstruction(selection: List, val waitSteps: Int) : HandInstruction(selection) { + override fun apply(handService: HandInteractionService, hand: Hand) { + handService.setPlannedWaitSteps(hand, waitSteps) + } + + companion object : ClockInstructionParser { + override val key: String + get() = "DLY" + + override fun deserialize(selection: List, parameters: String): WaitStepsInstruction { + require(parameters.isNotBlank()) { "Parameter waitSteps (like 5) expected." } + return WaitStepsInstruction(selection, parameters.toInt()) + } + + override fun serializeParameters(instruction: WaitStepsInstruction) = instruction.waitSteps.toString() + } +} diff --git a/src/instructionset/instructions/wall/RunInstruction.kt b/src/instructionset/instructions/wall/RunInstruction.kt new file mode 100644 index 0000000..7bea8eb --- /dev/null +++ b/src/instructionset/instructions/wall/RunInstruction.kt @@ -0,0 +1,24 @@ +package com.adclock.instructionset.instructions.wall + +import com.adclock.instructionset.instructions.InstructionParser +import com.adclock.services.WallInteractionService + +class RunInstruction : WallInstruction { + + override fun apply(wallService: WallInteractionService) { + wallService.sendUpdate() + } + + companion object : InstructionParser { + override val key: String + get() = "RUN" + + override fun deserialize(input: String): RunInstruction { + require(input.isBlank()) { "No parameters for run instruction expected, but was $input." } + return RunInstruction() + } + + override fun serialize(instruction: RunInstruction) = "" + } + +} diff --git a/src/instructionset/instructions/wall/WallInstruction.kt b/src/instructionset/instructions/wall/WallInstruction.kt new file mode 100644 index 0000000..f8ec9fb --- /dev/null +++ b/src/instructionset/instructions/wall/WallInstruction.kt @@ -0,0 +1,14 @@ +package com.adclock.instructionset.instructions.wall + +import com.adclock.instructionset.Task +import com.adclock.instructionset.instructions.Instruction +import com.adclock.services.WallInteractionService + +interface WallInstruction : Instruction { + fun apply(wallService: WallInteractionService) + + override fun apply(task: Task, wallService: WallInteractionService): Boolean { + apply(wallService) + return true // a wall instruction is always completed. It's a single instruction + } +} diff --git a/src/services/HandInteractionService.kt b/src/services/HandInteractionService.kt index a0c4c9a..182bda0 100644 --- a/src/services/HandInteractionService.kt +++ b/src/services/HandInteractionService.kt @@ -2,12 +2,15 @@ package com.adclock.services import com.adclock.model.Direction import com.adclock.model.Hand +import com.adclock.util.MAX_STEPS import com.adclock.util.move class HandInteractionService { fun setPlannedPosition(hand: Hand, position: Int, relative: Boolean) { - hand.planned.position = if (relative) position else hand.planned.direction.move(hand.planned.position, position) + require(position >= 0) { "Position $position invalid. Must be >= 0" } + require(position < MAX_STEPS) { "Position $position invalid. Must be < $MAX_STEPS" } + hand.planned.position = if (relative) hand.planned.direction.move(hand.planned.position, position) else position } fun setPlannedSpeed(hand: Hand, speed: Int) { @@ -18,6 +21,11 @@ class HandInteractionService { hand.planned.direction = direction } + fun setPlannedWaitSteps(hand: Hand, waitSteps: Int) { + require(waitSteps >= 0) { "WaitSteps $waitSteps invalid. Must be >= 0" } + hand.planned.waitSteps = waitSteps + } + fun resetPlanned(hand: Hand) { hand.planned = hand.target.copy() } diff --git a/src/util/ADClockUtils.kt b/src/util/ADClockUtils.kt index 17f7b80..29e7e2b 100644 --- a/src/util/ADClockUtils.kt +++ b/src/util/ADClockUtils.kt @@ -1,7 +1,9 @@ package com.adclock.util +import com.adclock.model.Clock import com.adclock.model.Direction import com.adclock.model.Movement +import com.adclock.instructionset.clockselections.ClockSelection const val MAX_STEPS = 1705 @@ -13,9 +15,8 @@ fun Direction.step(position: Int): Int { } } -fun Movement.validate() { - require(position >= 0) { "Position $position invalid. Must be >= 0" } - require(position < MAX_STEPS) { "Position $position invalid. Must be < $MAX_STEPS" } +fun Direction.move(position: Int, steps: Int) = + (position + (if (this == Direction.FORWARD) steps else -steps) % MAX_STEPS + MAX_STEPS) % MAX_STEPS + +fun Array.filter(selection: List) = filter { c -> selection.any { it.selected(c) } } - require(waitSteps >= 0) { "WaitSteps $waitSteps invalid. Must be >= 0" } -} \ No newline at end of file diff --git a/test/instructions/ClockSelectionParserTest.kt b/test/instructions/ClockSelectionParserTest.kt new file mode 100644 index 0000000..b8d95cc --- /dev/null +++ b/test/instructions/ClockSelectionParserTest.kt @@ -0,0 +1,110 @@ +package com.adclock.instructions + +import com.adclock.instructionset.InstructionSet +import com.adclock.instructionset.clockselections.RangeClockSelection +import com.adclock.instructionset.clockselections.SingleClockSelection +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ClockSelectionParserTest { + + @Test + fun parseBasicSelection() { + val selection = InstructionSet.deserializeSelection("1") + assertEquals(1, selection.size, "Amount of selection entries") + assertTrue(selection.first() is SingleClockSelection, "Type of first selection") + assertEquals("1HM", InstructionSet.serializeSelection(selection), "Serialized String") + } + + @Test + fun parseDoubleSelection() { + val selection = InstructionSet.deserializeSelection("1,5") + assertEquals(2, selection.size, "Amount of selection entries") + assertTrue(selection.component1() is SingleClockSelection, "Type of first selection") + assertTrue(selection.component2() is SingleClockSelection, "Type of second selection") + assertEquals("1HM,5HM", InstructionSet.serializeSelection(selection), "Serialized String") + } + + + @Test + fun parseTripleSelection() { + val selection = InstructionSet.deserializeSelection("1H,5M,7HM") + assertEquals(3, selection.size, "Amount of selection entries") + assertTrue(selection.component1() is SingleClockSelection, "Type of first selection") + assertTrue(selection.component2() is SingleClockSelection, "Type of second selection") + assertTrue(selection.component3() is SingleClockSelection, "Type of third selection") + assertEquals("1H,5M,7HM", InstructionSet.serializeSelection(selection), "Serialized String") + } + + @Test + fun parseComplexSelection() { + val selection = InstructionSet.deserializeSelection("1H,5M,7HM,12-14H") + assertEquals(4, selection.size, "Amount of selection entries") + assertTrue(selection.component1() is SingleClockSelection, "Type of first selection") + assertTrue(selection.component2() is SingleClockSelection, "Type of second selection") + assertTrue(selection.component3() is SingleClockSelection, "Type of third selection") + assertTrue(selection.component4() is RangeClockSelection, "Type of fourth selection") + assertEquals("1H,5M,7HM,12-14H", InstructionSet.serializeSelection(selection), "Serialized String") + } + + @Test(IllegalArgumentException::class) + fun parseIllegalSingleSelection() { + InstructionSet.deserializeSelection("A") + } + + @Test(IllegalArgumentException::class) + fun parseIllegalRangeSelection() { + InstructionSet.deserializeSelection("A-B") + } + + @Test(IllegalArgumentException::class) + fun parseIllegalDoubleRangeSelection() { + InstructionSet.deserializeSelection("1-3-4") + } + + + @Test(IllegalArgumentException::class) + fun parseIllegalRangeBoundsSelection() { + InstructionSet.deserializeSelection("5-2") + } + + @Test(IllegalArgumentException::class) + fun parseIllegalRangeWithHandSelection() { + InstructionSet.deserializeSelection("5M-2H") + } + + @Test(IllegalArgumentException::class) + fun parseIllegalDelimiterSelection() { + InstructionSet.deserializeSelection("1-2;3") + } + + @Test(IllegalArgumentException::class) + fun parseOnlyDelimiterSelection() { + InstructionSet.deserializeSelection(",") + } + + + @Test(IllegalArgumentException::class) + fun parseIllegalSpacesSelection() { + InstructionSet.deserializeSelection("1 ") + } + + + @Test(IllegalArgumentException::class) + fun parseIllegalSpacesBeforeDelimiterSelection() { + InstructionSet.deserializeSelection("1 ,4") + } + + + @Test(IllegalArgumentException::class) + fun parseEmptySelection() { + InstructionSet.deserializeSelection("") + } + + + @Test(IllegalArgumentException::class) + fun parseBlankSelection() { + InstructionSet.deserializeSelection(" ") + } +} \ No newline at end of file diff --git a/test/instructions/InstructionParserTest.kt b/test/instructions/InstructionParserTest.kt new file mode 100644 index 0000000..5473026 --- /dev/null +++ b/test/instructions/InstructionParserTest.kt @@ -0,0 +1,105 @@ +package com.adclock.instructions + +import com.adclock.instructionset.InstructionSet +import com.adclock.instructionset.instructions.basic.RepeatInstruction +import com.adclock.instructionset.instructions.basic.SleepInstruction +import com.adclock.instructionset.instructions.clock.CalibrateInstruction +import com.adclock.instructionset.instructions.hand.* +import com.adclock.instructionset.instructions.wall.RunInstruction +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals + +class InstructionParserTest { + + private fun assertInstructionParse(string: String) { + val instruction = InstructionSet.deserialize(string) + val serialized = InstructionSet.serialize(instruction) + assertEquals(string, serialized, "Instruction deserialized and serialized") + } + + private fun assertInstructionType(string: String, type: KClass<*>) { + val instruction = InstructionSet.deserialize(string) + assertEquals(type, instruction::class, "Instruction type") + } + + @Test + fun parseCalibrateInstruction() { + assertInstructionType("CAL 1", CalibrateInstruction::class) + assertInstructionParse("CAL 12-14HM,15M") + } + + @Test + fun parseDirectionInstruction() { + assertInstructionType("DIR 1 FORWARD", DirectionInstruction::class) + assertInstructionParse("DIR 1H FORWARD") + assertInstructionParse("DIR 1H BACKWARD") + } + + @Test + fun parsePositionInstruction() { + assertInstructionType("POS 1 12", PositionInstruction::class) + assertInstructionParse("POS 1H 12R") + assertInstructionParse("POS 1H 12A") + } + + @Test + fun parseResetInstruction() { + assertInstructionType("RST 1", ResetInstruction::class) + assertInstructionParse("RST 1H") + } + + @Test + fun parseSpeedInstruction() { + assertInstructionType("SPD 1 12", SpeedInstruction::class) + assertInstructionParse("SPD 1H 12") + assertInstructionParse("SPD 1H 12") + } + + @Test + fun parseWaitStepsInstruction() { + assertInstructionType("DLY 1 12", WaitStepsInstruction::class) + assertInstructionParse("DLY 1H 12") + assertInstructionParse("DLY 1H 12") + } + + + @Test + fun parseRunInstruction() { + assertInstructionType("RUN", RunInstruction::class) + assertInstructionParse("RUN") + } + + @Test + fun parseSleepInstruction() { + assertInstructionType("SLP 42", SleepInstruction::class) + assertInstructionParse("SLP 12") + } + + @Test + fun parseRepeatInstruction() { + assertInstructionType("RPT", RepeatInstruction::class) + assertInstructionParse("RPT") + } + + @Test(IllegalArgumentException::class) + fun parseEmptyInstruction() { + InstructionSet.deserialize("") + } + + @Test(IllegalArgumentException::class) + fun parseBlankInstruction() { + InstructionSet.deserialize(" ") + } + + + @Test(IllegalArgumentException::class) + fun parseUnknownInstruction() { + InstructionSet.deserialize("R2D") + } + + @Test(IllegalArgumentException::class) + fun parseIllegal4thCharInstruction() { + InstructionSet.deserialize("CAL_") + } +} \ No newline at end of file