Kotlin DSL to write crime stories while programming.
Leave a star, if you'd like to see this project out and write your own crime story!
Project is in progress. Stay tuned for tutorials about Kotlin / DSL / Event-Driven and Event Sourcing and follow me on dev.to: https://dev.to/mateusznowak
If you speak Polish, subscribe my mailing list for more: https://zycienakodach.pl/lista-mailingowa
Let's see an example on how you'd test your stories:
package pl.zycienakodach.crimestories.scenarios.mysterydeath
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.jupiter.api.Test
import pl.zycienakodach.crimestories.domain.capability.character.AskAboutCharacter
import pl.zycienakodach.crimestories.domain.capability.character.AskAboutItem
import pl.zycienakodach.crimestories.domain.capability.character.LetsChatWith
import pl.zycienakodach.crimestories.domain.capability.detective.*
import pl.zycienakodach.crimestories.domain.capability.item.ItemWasFound
import pl.zycienakodach.crimestories.domain.capability.location.*
import pl.zycienakodach.crimestories.domain.policy.investigation.Investigation
import pl.zycienakodach.crimestories.domain.policy.investigation.SinglePlayerInvestigation
import pl.zycienakodach.crimestories.domain.policy.investigation.currentTime
import pl.zycienakodach.crimestories.domain.shared.*
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
val detectiveThomas = DetectiveId("Thomas")
class MysteryDeathScenarioTest {
@Test
fun `scenario action starts at Police Station`() {
val investigation = mysteryDeathInvestigation()
assertThat(investigation.detectiveLocation()).isEqualTo(policeStation.id)
}
@Test
fun `scenario action starts on 2020_11_25 at 12_00`() {
val investigation = mysteryDeathInvestigation()
assertThat(investigation.currentTime()).isEqualTo(
LocalDateTime.of(
LocalDate.of(2020, 11, 25),
LocalTime.of(12, 0, 0)
)
)
}
@Test
fun `start investigation should say about found human body`() {
mysteryDeathInvestigation()
.whenDetective(
StartInvestigation(detectiveThomas)
).then(
event = InvestigationStarted(detectiveThomas),
storyMessage = "Police is on the crime scene. Neighbour call to you that they have found Harry death body. His apartment is in city center."
)
}
@Test
fun `detective can move to victim house`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas)
).whenDetective(
VisitLocation(detectiveThomas, where = harryHouse.id)
).then(
event = DetectiveMoved(detectiveThomas, to = harryHouse.id),
storyMessage = "You have visited victims house. Police officer is waiting for you here."
)
}
@Test
fun `after moved to victim hose, current detective location is harry house`() {
val investigation = mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id)
)
assertThat(investigation.detectiveLocation()).isEqualTo(harryHouse.id)
}
@Test
fun `detective can search crime scene at victim house`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id)
).whenDetective(
SearchCrimeScene(detectiveThomas, at = harryHouseId)
).then(
event = CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
storyMessage = "You have searched crime scene. Try to secure items."
)
}
@Test
fun `at victim house detective can talk with victim daughter`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id)
).whenDetective(
LetsChatWith(ask = aliceId, askedBy = detectiveThomas)
).then(
"Alice: I'm really scared! My dad was killed by someone..."
)
}
@Test
fun `after search crime scene, detective can secure knife`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas)
).whenDetective(
SecureTheEvidence(detectiveThomas, at = harryHouseId, itemId = Knife.id)
).then(
event = ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas),
storyMessage = "Item was secured!"
)
}
@Test
fun `when detective found knife, victim daughter tell that knife belongs to her brother`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas)
).whenDetective(
AskAboutItem(ask = aliceId, askedBy = detectiveThomas, askAbout = Knife.id)
).then(
"Alice: Oh! This knife belongs to my brother."
)
}
@Test
fun `when detective found knife, lab technician tell that knife has fingerprint of victim daughter`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas)
).whenDetective(
AskAboutItem(ask = labTechnicianJohnId, askedBy = detectiveThomas, askAbout = Knife.id)
).then(
"John: On the Knife, I've found fingerprints of Alice - Harry's daughter."
)
}
@Test
fun `closing investigation is not possible at victim hose`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas)
).whenDetective(
CloseInvestigation(detectiveThomas)
).then(
"You can close investigation only at Police Station."
)
}
@Test
fun `closing investigation - incorrect murdered`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas),
DetectiveMoved(detectiveThomas, to = policeStation.id)
).whenDetective(
CloseInvestigation(detectiveThomas,answers = mapOf(
"Who has killed Harry?" to harry.first,
"What was the murder weapon?" to Knife.id
))
).then(
InvestigationClosed(
detectiveThomas,
questionsWithAnswers = mapOf(
"Who has killed Harry?" to GivenAnswer(harry.first, false),
"What was the murder weapon?" to GivenAnswer(Knife.id, true)
)
),
"You have closed this investigation!"
)
}
@Test
fun `closing investigation - correct murdered, alice killed harry by knife`() {
mysteryDeathInvestigation(
InvestigationStarted(detectiveThomas),
DetectiveMoved(detectiveThomas, to = harryHouse.id),
CrimeSceneSearched(at = harryHouseId, by = detectiveThomas),
ItemWasFound(itemId = Knife.id, detectiveId = detectiveThomas),
DetectiveMoved(detectiveThomas, to = policeStation.id)
).whenDetective(
CloseInvestigation(detectiveThomas,answers = mapOf(
"Who has killed Harry?" to aliceId,
"What was the murder weapon?" to Knife.id
))
).then(
InvestigationClosed(
detectiveThomas,
questionsWithAnswers = mapOf(
"Who has killed Harry?" to GivenAnswer(aliceId, true),
"What was the murder weapon?" to GivenAnswer(Knife.id, true)
)
),
"You have closed this investigation!"
)
}
}
private fun Investigation.whenDetective(command: Command): ICommandResult = this.investigate(command)
private fun ICommandResult.then(commandResult: CommandResult) = assertThat(this).isEqualTo(commandResult)
private fun ICommandResult.then(event: DomainEvent, storyMessage: StoryMessage) =
assertThat(this).isEqualTo(CommandResult(event, storyMessage))
private fun ICommandResult.then(storyMessage: StoryMessage) =
assertThat(this).isEqualTo(CommandResult.onlyMessage(storyMessage))
private fun mysteryDeathInvestigation(vararg event: DomainEvent) =
mysteryDeathInvestigation(listOf(*event), emptyList())
private fun mysteryDeathInvestigation(vararg command: Command) =
mysteryDeathInvestigation(emptyList(), listOf(*command))
private fun mysteryDeathInvestigation(
history: DomainEvents = emptyList(),
commands: List<Command> = emptyList()
) =
SinglePlayerInvestigation(
scenario = mysteryDeathScenario,
detectiveId = detectiveThomas,
history = history
).apply {
commands.forEach { investigate(it) }
}
Such tests are so easy that even ChatGPT can understand them and give answers for questions based on them:
val alice: ScenarioCharacter = aliceId to { command, history ->
when (command) {
is AskAboutItem -> {
if (command.askAbout === Knife.id) {
if (Knife.wasFoundBy(command.askedBy).inThe(history)) {
CommandResult.onlyMessage("Alice: Oh! This knife belongs to my brother.")
} else {
CommandResult.onlyMessage("Alice: You cannot ask about item which you have not found.")
}
} else {
CommandResult.onlyMessage("Alice: I dont know anything about it.")
}
}
else -> CommandResult.onlyMessage("Alice: I'm really scared! My dad was killed by someone...")
}
}
val mysteryScenario: (context: MysteryScenarioContext) -> CriminologyScenario = { context ->
scenario(context) {
doOnEvery<SearchCrimeScene>(MinutesHasPassed(5))
characters.alice {
whenChat {
storyMessage = "Alice: What are you ask me about!?"
}
whenAskedAbout(Knife) then {
storyMessage = "Alice: What are you ask me about!?"
event = Knife.wasFound
}
whenAskedAbout(Knife) and Knife.wasFound then {
storyMessage = "Alice: Ough... this knife belongs to my brother"
}
}
characters.alice
.whenChat { storyMessage = "Alice: What are you ask me about!?" }
.whenAskedAbout(Knife) then { storyMessage = "Alice: What are you ask me about!?" }
}
}