Svelte, XState 및 SpeechRecognition으로 게임 빌드
43769 단어 sveltexstatetypescript
XState
, Svelte
및 SpeechRecognition API 🎤 을 가지고 놀았기 때문에 미니 숫자 추측 게임을 만들고 statechart 으로 상태를 모델링하기로 결정했습니다. 그것.If you want to try it out go to this 🌎 Live demo (only works on Chrome desktop or mobile).
참고: SpeechRecognition은 영어 단어만 인식하므로(또는 적어도 스페인어 😝에서는 작동하지 않습니다), 게임이 스페인어로 되어 있더라도 영어로 숫자를 말해야 합니다.
유형
TypeScript를 사용할 것이기 때문에 먼저
types
를 정의합시다.export type NumberGuessContextType = {
recognition: SpeechRecognition | null
randomNumber: number
hint: string
error: string
isChrome: boolean
}
export type NotSupportedErrorType = {
type: 'NOT_SUPPORTED_ERROR'
error: string
}
export type CheckReadinessType = {
type: 'CHECK_READINESS'
}
type NotAllowedErrorType = {
type: 'NOT_ALLOWED_ERROR'
error: string
}
type SpeakType = {
type: 'SPEAK'
message: string
}
type PlayAgainType = {
type: 'PLAY_AGAIN'
}
export type UpdateHintType = {
type: 'UPDATE_HINT'
data: string
}
export type NumberGuessEventType =
| NotSupportedErrorType
| CheckReadinessType
| NotAllowedErrorType
| SpeakType
| PlayAgainType
| UpdateHintType
export type NumberGuessStateType = {
context: NumberGuessContextType
value: 'verifyingBrowser' | 'failure' | 'playing' | 'checkNumber' | 'gameOver'
}
SpeechRecognition에 대한 전역 유형 추가
SpeechRecognition API는 매우 실험적이므로 TS가 이에 대해 알 수 있도록 이 API를 처리하는 방법을 기술해야 합니다.
webkitSpeechRecognition
유형에 대한 전역 인터페이스를 선언하겠습니다.export declare global {
interface Window {
webkitSpeechRecognition: SpeechRecognition
}
}
기계
이제 상태 머신의 차례입니다. 여기에서 우리의 작은 게임 뒤에 모든 논리를 넣을 것입니다.
import { createMachine, assign } from 'xstate'
import type {
NumberGuessContextType,
NumberGuessEventType,
NumberGuessStateType,
NotSupportedErrorType,
UpdateHintType,
} from 'src/machine/types'
const numberGuessMachine = createMachine<
NumberGuessContextType,
NumberGuessEventType,
NumberGuessStateType
>(
{
id: 'guessNumber',
initial: 'verifyingBrowser',
context: {
hint: '',
recognition: null,
randomNumber: -1,
error: '',
isChrome: false,
},
states: {
verifyingBrowser: {
entry: 'checkBrowser',
on: {
NOT_SUPPORTED_ERROR: {
target: 'failure',
actions: 'displayError',
},
CHECK_READINESS: {
target: 'playing',
actions: 'initGame',
cond: 'isSpeechRecognitionReady',
},
NOT_ALLOWED_ERROR: {
target: 'failure',
actions: 'displayError',
cond: 'hasError',
},
},
},
playing: {
after: {
2500: {
actions: 'clearHint',
cond: 'hasHint',
},
},
on: {
SPEAK: {
target: 'checkNumber',
},
},
},
checkNumber: {
invoke: {
id: 'checkingNumber',
src: 'checkNumber',
onDone: {
actions: 'updateHint',
target: 'gameOver',
},
onError: {
actions: 'updateHint',
target: 'playing',
},
},
},
gameOver: {
exit: 'initGame',
on: {
PLAY_AGAIN: {
target: 'playing',
},
SPEAK: {
target: 'playing',
cond: 'isPlayAgain',
},
},
},
failure: {
type: 'final',
},
},
},
{
actions: {
checkBrowser: assign({
isChrome: _ => navigator.userAgent.includes('Chrome'),
}),
displayError: assign<NumberGuessContextType, NotSupportedErrorType>({
error: (_, event) => event.error,
}) as any,
initGame: assign({
hint: _ => '',
recognition: _ => new window.SpeechRecognition(),
randomNumber: _ => Math.floor(Math.random() * 100) + 1,
}),
updateHint: assign<NumberGuessContextType, UpdateHintType>({
hint: (_, event) => event.data,
}) as any,
clearHint: assign({
hint: _ => '',
}),
},
guards: {
hasError(_, event: NumberGuessEventType) {
if (event.type === 'NOT_ALLOWED_ERROR') {
return event.error !== ''
}
return false
},
hasHint(context) {
return context.hint !== ''
},
isUnsupportedBrowser(_, event: NumberGuessEventType) {
return event.type !== 'NOT_SUPPORTED_ERROR'
},
isSpeechRecognitionReady() {
window.SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition
return window.SpeechRecognition !== undefined
},
isPlayAgain(_, event: NumberGuessEventType) {
if (event.type === 'SPEAK') {
return event.message === 'play'
}
return false
},
},
services: {
checkNumber(
context: NumberGuessContextType,
event: NumberGuessEventType
) {
if (event.type !== 'SPEAK') {
return Promise.reject('Acción no válida.')
}
const num = +event.message
if (Number.isNaN(num)) {
return Promise.reject('Ese no es un número válido, intenta de nuevo')
}
if (num > 100 || num < 1) {
return Promise.reject('El número debe estar entre 1 y 100')
}
if (num === context.randomNumber) {
return Promise.resolve('¡Felicidades has ganado!')
}
if (num > context.randomNumber) {
return Promise.reject('MENOR')
}
return Promise.reject('MAYOR')
},
},
}
)
export { numberGuessMachine }
우리 기계 사용하기
numberGuessMachine
구성 요소에서 App
를 사용할 시간입니다.<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { interpret } from 'xstate'
import { realisticLook } from 'src/utils'
import { numberGuessMachine } from 'src/machine/numberGuess'
const service = interpret(numberGuessMachine).start()
function onSpeak(event: SpeechRecognitionEvent) {
const [result] = event.results
const [transcripts] = result
const { transcript: message } = transcripts
service.send({
message,
type: 'SPEAK',
})
}
onMount(() => {
if (!$service.context.isChrome) {
return service.send({
type: 'NOT_SUPPORTED_ERROR',
error: 'Lo siento, tu navegador no soporta la API SpeechRecognition.',
})
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(() => {
service.send({
type: 'CHECK_READINESS',
})
const recognition = $service.context.recognition
if (!recognition) {
return
}
recognition.start()
recognition.addEventListener('result', onSpeak)
recognition.addEventListener('end', () => recognition.start())
})
.catch(() => {
service.send({
type: 'NOT_ALLOWED_ERROR',
error:
'Por favor, permita el uso del 🎤 para poder jugar. Y después recargue la página.',
})
})
})
onDestroy(() => {
$service.context?.recognition?.stop()
service.stop()
})
service.onTransition(state => {
if (state.matches('gameOver')) {
realisticLook()
}
})
</script>
<section class="container" data-state={$service.toStrings().join(' ')}>
{#if $service.matches('failure') && !$service.context.isChrome}
<div>{$service.context.error}</div>
{/if}
{#if $service.matches('playing')}
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
class="mic"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
<h1>Adivina el número entre 1 y 100</h1>
<h3>Menciona el número que desees (en inglés).</h3>
<div class="msg">
{$service.context.hint}
</div>
</div>
{/if}
{#if $service.matches('gameOver')}
<div>
<h2>
{$service.context.hint}
<br />
<br />
El número era: {$service.context.randomNumber}
</h2>
<button
class="play-again"
on:click={() =>
service.send({
type: 'PLAY_AGAIN',
})}>Play</button
>
<p class="mt-1">O menciona "play"</p>
</div>
{/if}
{#if $service.matches('failure') && $service.context.isChrome}
<div>
{$service.context.error}
</div>
{/if}
</section>
메모
💻 소스 코드: number-guess
즐거운 코딩 👋🏽
Reference
이 문제에 관하여(Svelte, XState 및 SpeechRecognition으로 게임 빌드), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/gcdcoder/build-a-game-with-svelte-xstate-and-speedrecognition-5f9i텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)