Skip to content

MateuszNaKodach/CrimeStoriesGame

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Crime Stories Kotlin DSL

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: image

Crime Domain-Specific Language is evolving:

Version 1

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...")
    }
}

Version 2

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!?" }

    }

}

About

Kotlin DSL which helps you to write crime stories as a programmer!

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published