Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions examples/platformer/src/machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"
import { makeTextRenderer } from "../../../test/machine/visualization/text.ts"
import {
airJumpMode,
CharacterEvents,
CharacterMachine,
type CharacterSnapshot,
Event,
Expand Down Expand Up @@ -100,37 +101,50 @@ const laws = [
resumeRestoresDeepHistory
]

// Exploration scenarios retain decoded events for trace inspection.
const EventValue = {
Resume: () => Machine.event(CharacterMachine, Event.cases.Resume),
Pause: (fields: { readonly at: number }) => Machine.event(CharacterMachine, Event.cases.Pause, fields),
Reset: () => Machine.event(CharacterMachine, Event.cases.Reset),
JumpPressed: (fields: { readonly at: number; readonly y: number; readonly wall: -1 | 0 | 1 }) =>
Machine.event(CharacterMachine, Event.cases.JumpPressed, fields),
Landed: (fields: { readonly impact: number; readonly axis: -1 | 0 | 1; readonly at: number }) =>
Machine.event(CharacterMachine, Event.cases.Landed, fields),
ApexReached: (fields: { readonly y: number }) => Machine.event(CharacterMachine, Event.cases.ApexReached, fields),
DownPressed: (fields: { readonly at: number }) => Machine.event(CharacterMachine, Event.cases.DownPressed, fields)
}

const explorationEvents = ({ snapshot }: MachineTest.ExplorationStateContext<typeof CharacterMachine>) => {
const locomotion = snapshot.states.locomotion.state
if (locomotion.path === "Character.locomotion.Paused") {
return [Event.cases.Resume.make({})]
return [EventValue.Resume()]
}

const pauseAndReset = [Event.cases.Pause.make({ at: 70 }), Event.cases.Reset.make({})]
const pauseAndReset = [EventValue.Pause({ at: 70 }), EventValue.Reset()]
if (locomotion.state.path === "Character.locomotion.Playing.Grounded") {
return [
Event.cases.JumpPressed.make({ at: 20, y: 100, wall: -1 }),
Event.cases.JumpPressed.make({ at: 20, y: 100, wall: 0 }),
Event.cases.JumpPressed.make({ at: 20, y: 100, wall: 1 }),
EventValue.JumpPressed({ at: 20, y: 100, wall: -1 }),
EventValue.JumpPressed({ at: 20, y: 100, wall: 0 }),
EventValue.JumpPressed({ at: 20, y: 100, wall: 1 }),
...pauseAndReset
]
}

const motion = locomotion.state.states.motion.state
const airborne = [
Event.cases.JumpPressed.make({ at: 30, y: 80, wall: -1 }),
Event.cases.JumpPressed.make({ at: 30, y: 80, wall: 1 }),
Event.cases.Landed.make({ impact: 12, axis: 0, at: 60 }),
EventValue.JumpPressed({ at: 30, y: 80, wall: -1 }),
EventValue.JumpPressed({ at: 30, y: 80, wall: 1 }),
EventValue.Landed({ impact: 12, axis: 0, at: 60 }),
...pauseAndReset
]
return motion.path === "Character.locomotion.Playing.Airborne.motion.Jumping"
? [
Event.cases.ApexReached.make({ y: 50 }),
Event.cases.DownPressed.make({ at: 40 }),
EventValue.ApexReached({ y: 50 }),
EventValue.DownPressed({ at: 40 }),
...airborne
]
: motion.path === "Character.locomotion.Playing.Airborne.motion.Falling"
? [Event.cases.DownPressed.make({ at: 40 }), ...airborne]
? [EventValue.DownPressed({ at: 40 }), ...airborne]
: airborne
}

Expand Down Expand Up @@ -166,20 +180,20 @@ describe("platformer history integration", () => {
const ducking = yield* Machine.plan(
CharacterMachine,
initial.state,
Event.cases.DownPressed.make({ at: 10 })
CharacterEvents.DownPressed({ at: 10 })
)
const beforePause = playingSnapshot(ducking.next)

const paused = yield* Machine.plan(
CharacterMachine,
ducking.next,
Event.cases.Pause.make({ at: 20 })
CharacterEvents.Pause({ at: 20 })
)
const pausedLocomotion = paused.next.states.locomotion.state
expect(pausedLocomotion.path).toBe("Character.locomotion.Paused")
expect(pausedLocomotion.value).toEqual({ _tag: "Paused", pausedAt: 20 })

const resumed = yield* Machine.plan(CharacterMachine, paused.next, Event.cases.Resume.make({}))
const resumed = yield* Machine.plan(CharacterMachine, paused.next, CharacterEvents.Resume())
expect(playingSnapshot(resumed.next)).toEqual(beforePause)
expect(resumed.next.states.facing.state.path).toBe("Character.facing.Right")
expect(resumed.next.states.contact.state.path).toBe("Character.contact.NoWall")
Expand All @@ -192,19 +206,19 @@ describe("platformer history integration", () => {
const airborne = yield* Machine.plan(
CharacterMachine,
initial.state,
Event.cases.JumpPressed.make({ at: 100, y: 207, wall: -1 })
CharacterEvents.JumpPressed({ at: 100, y: 207, wall: -1 })
)
const falling = yield* Machine.plan(
CharacterMachine,
airborne.next,
Event.cases.ApexReached.make({ y: 91 })
CharacterEvents.ApexReached({ y: 91 })
)
const beforePause = playingSnapshot(falling.next)

const paused = yield* Machine.plan(
CharacterMachine,
falling.next,
Event.cases.Pause.make({ at: 180 })
CharacterEvents.Pause({ at: 180 })
)
expect(paused.next.history?.["Character.locomotion.Playing.resume"]?.active).toEqual([
"Character",
Expand All @@ -217,7 +231,7 @@ describe("platformer history integration", () => {
"Character.locomotion.Playing.Airborne.airJump.AirJumpGroundLock"
])

const resumed = yield* Machine.plan(CharacterMachine, paused.next, Event.cases.Resume.make({}))
const resumed = yield* Machine.plan(CharacterMachine, paused.next, CharacterEvents.Resume())
const restored = playingSnapshot(resumed.next)
expect(restored).toEqual(beforePause)

Expand Down
21 changes: 13 additions & 8 deletions examples/platformer/src/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,18 @@ const initialCharacter = () =>
.contact.from((contact) => contact.NoWall.from())
)

export const CharacterMachine = Machine.make({
const definition = Machine.make({
id: "PlatformerCharacter",
states: CharacterStates.states,
events: [Event],
internalEvents: [InternalEvent],
initial: initialCharacter
}).handle({
})

export const CharacterEvents = Machine.events(definition)
const InternalEvents = Machine.internalEvents(definition)

export const CharacterMachine = definition.handle({
Character: {
on: {
Reset: {
Expand Down Expand Up @@ -223,7 +228,7 @@ export const CharacterMachine = Machine.make({
}
},
Landing: {
invoke: Machine.after("140 millis", InternalEvent.cases.LandingSettled.make({}), {
invoke: Machine.after("140 millis", InternalEvents.LandingSettled(), {
id: "landing-settle"
}),
on: {
Expand Down Expand Up @@ -254,8 +259,8 @@ export const CharacterMachine = Machine.make({
const push = awayFrom(event.wall)
enqueue.raise(
push === 0
? InternalEvent.cases.TryAirJump.make({ at: event.at })
: InternalEvent.cases.WallJump.make({ at: event.at, push })
? InternalEvents.TryAirJump({ at: event.at })
: InternalEvents.WallJump({ at: event.at, push })
)
}
},
Expand Down Expand Up @@ -319,7 +324,7 @@ export const CharacterMachine = Machine.make({
},
states: {
AirJumpGroundLock: {
invoke: Machine.after("120 millis", InternalEvent.cases.AirJumpUnlocked.make({}), {
invoke: Machine.after("120 millis", InternalEvents.AirJumpUnlocked(), {
id: "ground-air-jump-unlock"
}),
on: {
Expand All @@ -330,7 +335,7 @@ export const CharacterMachine = Machine.make({
}
},
AirJumpWallLock: {
invoke: Machine.after("240 millis", InternalEvent.cases.AirJumpUnlocked.make({}), {
invoke: Machine.after("240 millis", InternalEvents.AirJumpUnlocked(), {
id: "wall-air-jump-unlock"
}),
on: {
Expand All @@ -345,7 +350,7 @@ export const CharacterMachine = Machine.make({
TryAirJump: {
targets: ["Character.locomotion.Playing.Airborne.airJump.AirJumpSpent"],
transition: ({ event, target }, enqueue) => {
enqueue.raise(InternalEvent.cases.DoubleJump.make({ at: event.at }))
enqueue.raise(InternalEvents.DoubleJump({ at: event.at }))
return target.local.AirJumpSpent.from()
}
}
Expand Down
26 changes: 13 additions & 13 deletions examples/playground/src/examples/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { Effect } from "effect"
import { MicrowaveEvent, MicrowaveMachine } from "./microwave/machine.ts"
import { TrafficLightMachine } from "./traffic-light/machine.ts"
import { TurnstileEvent, TurnstileMachine } from "./turnstile/machine.ts"
import { SharedMachine, SharedMachineEvent } from "./worker-tabs/machine.ts"
import { SharedMachine, SharedTransportEvents } from "./worker-tabs/machine.ts"

describe("playground machines", () => {
it.effect("accepts only the command enabled by the current turnstile state", () =>
Effect.gen(function*() {
const trace = yield* MachineTest.run(TurnstileMachine, {
events: [
TurnstileEvent.cases.GatePushed.make({}),
TurnstileEvent.cases.CoinInserted.make({}),
TurnstileEvent.cases.CoinInserted.make({}),
TurnstileEvent.cases.GatePushed.make({})
Machine.event(TurnstileMachine, TurnstileEvent.cases.GatePushed),
Machine.event(TurnstileMachine, TurnstileEvent.cases.CoinInserted),
Machine.event(TurnstileMachine, TurnstileEvent.cases.CoinInserted),
Machine.event(TurnstileMachine, TurnstileEvent.cases.GatePushed)
]
})

Expand Down Expand Up @@ -46,11 +46,11 @@ describe("playground machines", () => {
Effect.gen(function*() {
const trace = yield* MachineTest.run(MicrowaveMachine, {
events: [
MicrowaveEvent.cases.PowerPressed.make({}),
MicrowaveEvent.cases.DoorOpened.make({}),
MicrowaveEvent.cases.PowerPressed.make({}),
MicrowaveEvent.cases.DoorClosed.make({}),
MicrowaveEvent.cases.PowerPressed.make({})
Machine.event(MicrowaveMachine, MicrowaveEvent.cases.PowerPressed),
Machine.event(MicrowaveMachine, MicrowaveEvent.cases.DoorOpened),
Machine.event(MicrowaveMachine, MicrowaveEvent.cases.PowerPressed),
Machine.event(MicrowaveMachine, MicrowaveEvent.cases.DoorClosed),
Machine.event(MicrowaveMachine, MicrowaveEvent.cases.PowerPressed)
]
})

Expand All @@ -67,9 +67,9 @@ describe("playground machines", () => {
Effect.gen(function*() {
const trace = yield* MachineTest.run(SharedMachine, {
events: [
SharedMachineEvent.cases.Started.make({}),
SharedMachineEvent.cases.Incremented.make({}),
SharedMachineEvent.cases.Synchronized.make({ active: false, count: 12 })
SharedTransportEvents.Started(),
SharedTransportEvents.Incremented(),
SharedTransportEvents.Synchronized({ active: false, count: 12 })
]
})

Expand Down
28 changes: 14 additions & 14 deletions examples/playground/src/examples/media-player/MediaPlayerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Match } from "effect"
import { useCallback, useEffect, useState } from "react"
import { ExamplePage } from "../../components/ExamplePage.tsx"
import { mediaPlayerAtom, mediaPlayerViewAtom, registerMediaPlayerElement } from "./atoms.ts"
import { MediaPlayerEvent } from "./schemas.ts"
import { MediaPlayerEvents } from "./definition.ts"

interface AudioSource {
readonly name: string
Expand Down Expand Up @@ -75,7 +75,7 @@ export function MediaPlayerPage() {
if (file === undefined) return
const next = { name: file.name, url: URL.createObjectURL(file) }
setSource(next)
send(MediaPlayerEvent.cases.SourceSelected.make({ url: next.url }))
send(MediaPlayerEvents.SourceSelected({ url: next.url }))
}}
/>
</label>
Expand All @@ -86,16 +86,16 @@ export function MediaPlayerPage() {
ref={registerAudioElement}
className="media-audio-element"
preload="auto"
onWaiting={() => send(MediaPlayerEvent.cases.MediaWaiting.make({}))}
onCanPlay={() => send(MediaPlayerEvent.cases.MediaCanPlay.make({}))}
onWaiting={() => send(MediaPlayerEvents.MediaWaiting())}
onCanPlay={() => send(MediaPlayerEvents.MediaCanPlay())}
onError={({ currentTarget }) =>
send(MediaPlayerEvent.cases.MediaFailed.make({
send(MediaPlayerEvents.MediaFailed({
message: currentTarget.error?.message ?? "The selected audio file could not be loaded"
}))}
onTimeUpdate={({ currentTarget }) =>
send(MediaPlayerEvent.cases.TimeUpdated.make({ currentTime: currentTarget.currentTime }))}
send(MediaPlayerEvents.TimeUpdated({ currentTime: currentTarget.currentTime }))}
onEnded={({ currentTarget }) =>
send(MediaPlayerEvent.cases.PlaybackEnded.make({ currentTime: currentTarget.currentTime }))}
send(MediaPlayerEvents.PlaybackEnded({ currentTime: currentTarget.currentTime }))}
/>

<div className="media-player-layout">
Expand All @@ -116,21 +116,21 @@ export function MediaPlayerPage() {
<button
type="button"
disabled={!canPlay || source === undefined}
onClick={() => send(MediaPlayerEvent.cases.PlayRequested.make({}))}
onClick={() => send(MediaPlayerEvents.PlayRequested())}
>
Play
</button>
<button
type="button"
disabled={!canPause}
onClick={() => send(MediaPlayerEvent.cases.PauseRequested.make({}))}
onClick={() => send(MediaPlayerEvents.PauseRequested())}
>
Pause
</button>
<button
type="button"
disabled={!canRestart}
onClick={() => send(MediaPlayerEvent.cases.RestartRequested.make({}))}
onClick={() => send(MediaPlayerEvents.RestartRequested())}
>
Restart
</button>
Expand Down Expand Up @@ -164,7 +164,7 @@ export function MediaPlayerPage() {
step="0.01"
value={settings.volume}
onChange={({ currentTarget }) =>
send(MediaPlayerEvent.cases.VolumeChanged.make({
send(MediaPlayerEvents.VolumeChanged({
volume: currentTarget.valueAsNumber
}))}
/>
Expand All @@ -177,8 +177,8 @@ export function MediaPlayerPage() {
onChange={({ currentTarget }) =>
send(
currentTarget.checked
? MediaPlayerEvent.cases.MuteRequested.make({})
: MediaPlayerEvent.cases.UnmuteRequested.make({})
? MediaPlayerEvents.MuteRequested()
: MediaPlayerEvents.UnmuteRequested()
)}
/>
</label>
Expand All @@ -187,7 +187,7 @@ export function MediaPlayerPage() {
<select
value={settings.playbackRate}
onChange={({ currentTarget }) =>
send(MediaPlayerEvent.cases.PlaybackRateChanged.make({
send(MediaPlayerEvents.PlaybackRateChanged({
playbackRate: Number(currentTarget.value)
}))}
>
Expand Down
25 changes: 25 additions & 0 deletions examples/playground/src/examples/media-player/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Machine } from "@typeonce/effect-machine"
import { initialAudioSettings, MediaPlayerEvent, MediaPlayerInternalEvent, MediaPlayerStates } from "./schemas.ts"

const initialPlayer = () =>
MediaPlayerStates.initial.Player.from((player) =>
player
.transport.from((transport) => transport.Empty.from())
.settings.from((settings) =>
settings.Audible.from({
volume: initialAudioSettings.volume,
playbackRate: initialAudioSettings.playbackRate
})
)
)

export const MediaPlayerDefinition = Machine.make({
id: "MediaPlayer",
states: MediaPlayerStates.states,
events: [MediaPlayerEvent],
internalEvents: [MediaPlayerInternalEvent],
initial: initialPlayer
})

export const MediaPlayerEvents = Machine.events(MediaPlayerDefinition)
export const MediaPlayerInternalEvents = Machine.internalEvents(MediaPlayerDefinition)
Loading