Slide 1

Slide 1 text

࣮࿥ϨΨγʔίʔυվળ ٕज़తෛ࠴@ fi OEZ 5BLVUP8"%" +BO !'JOEZ !U@XBEB !UXBEB !UXBEB 📷🙆 🙆 rev.7

Slide 2

Slide 2 text

ߨԋͷഎܠ   w "84%FW%BZ5PLZPͰߨԋͨ͠಺༰Λେ෯ʹվగͯ͠ߨԋ͠·͢ w ࣮ࡍʹ೥ʹߦͬͨडୗ։ൃҊ݅ͷΤϐιʔυͱίʔυΛϓϩμΫτΦʔφʔʢҾ͖ܧ͗લͷίʔυΛॻ͍ͨ ຊਓʣͷڐՄΛಘͯ࢖༻͍ͯ͠·͢ w Ҿ͖ܧ͗લͷίʔυΛॻ͍ͨϓϩμΫτΦʔφʔ͸ݚڀऀͰ͋ͬͯɺϓϩͷ։ൃऀͰ͸͋Γ·ͤΜ w ొ৔͢Δίʔυ͸શͯຊ෺Ͱ͢ w ٕज़͸೥౰࣌ͷ΋ͷͳͷͰɺݱࡏ͔ΒݟΔͱ΍΍ݹ͍Ͱ͢ɻੜ+BWB4DSJQU $PNNPO+4 .PDIB ౳ʑ w ొ৔͢Δσʔλ͸ߨԋ༻ͷՍۭͷ΋ͷͰ͢ w +40/΍Ϩεϙϯεͷத਎͚ͩ͸ߨԋ༻ͷՍۭͷσʔλͰ͢

Slide 3

Slide 3 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 3/118

Slide 4

Slide 4 text

γφϦΦ౎ಓ෎ݝΫΠζΛߦ͏ খ͍͞"MFYB4LJMM։ൃΛҾ͖ܧ͍ͩ

Slide 5

Slide 5 text

ΞϨΫαɺ౎ಓ෎ݝΫΠζΛ։͍ͯ ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ ಢ໦ݝͷݝிॴࡏ஍͸ʁ

Slide 6

Slide 6 text

Ӊ౎ٶʂ ؆୯ͳΫΠζΛ͠·͠ΐ͏ɻ൪ɻ ಢ໦ݝͷݝிॴࡏ஍͸ʁ ͦ͏Ͱ͢ɻͰ͸൪ɻ ԭೄݝͷݝͷՖ͸ʁ

Slide 7

Slide 7 text

͕͍ͪ·͢ɻਖ਼ղ͸σΠΰͰ͢ɻ Ͱ͸൪ɻἚ৓ݝͷϩʔϚࣈදه͸ʁ ͦ͏Ͱ͢ɻͰ͸൪ɻ ԭೄݝͷݝͷՖ͸ʁ ϋΠϏεΧεʁ

Slide 8

Slide 8 text

ͦ͏Ͱ͢ɻͰ͸ऴΘΓͰ͢ɻ ͋ͳͨ͸఺Ͱͨ͠ɻ ਆށʂ ɾ ɾ ɾ ͦ͏Ͱ͢ɻͰ͸൪ɻ ฌݿݝͷݝிॴࡏ஍͸ʁ

Slide 9

Slide 9 text

΅Μ΍Γͱͨ͠ཧղ

Slide 10

Slide 10 text

'use strict'; var Alexa = require('alexa-sdk'); var questions = require('./questions.json'); var handlers = { QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; } else { // 不正解の場合 resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; } if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける } else { // 全ての問題が終了した場合 var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`; this.emit(':tell', resultMessage + endMessage); // 会話を終える } }, 'AMAZON.RepeatIntent': function () { var speechOutput = `${this.attributes['advance']}番。 ${questions[this.attributes['itemIndex']].q}`; this.emit(':ask', speechOutput, speechOutput); }, 'AMAZON.HelpIntent': function () { var speechOutput = 'クイズが出題されたらそれに答えてください。それでは始めましょう。「スタート」と言ってください。 '; this.emit(':ask', speechOutput, speechOutput); }, 'AMAZON.CancelIntent': function () { this.response.speak('終わります。'); this.emit(':responseReady'); }, 'AMAZON.StopIntent': function () { this.response.speak('終わります。'); this.emit(':responseReady'); }, LaunchRequest: function () { this.emit('QuizIntent'); }, 'AMAZON.StartOverIntent': function () { this.emit('QuizIntent'); }, Unhandled: function () { this.emit(':tell', 'すみません、わかりませんでした。終わります。'); } }; exports.handler = function (event, context, callback) { var alexa = Alexa.handler(event, context); alexa.registerHandlers(handlers); alexa.execute(); Ҿ͖ܧ͍ͩίʔυ w ߦͷ+BWB4DSJQU w ϩδοΫ͸"84-BNCEBͰ࣮૷ w ࠓޙ͍ͭ͘΋ͷػೳ௥Ճ͕༧ఆ͞Ε͍ͯΔ w ओཁͳϩδοΫ͸ॳճىಈॲཧʢ2VJ[*OUFOUʣͱճ ౴डཧʢ"OTXFS*OUFOUʣͷΑ͏ͩ w ࣭໰σʔλ͸KTPOͰ؅ཧ͞Ε͍ͯΔ

Slide 11

Slide 11 text

·ͣ͸গ͠མͪண͍ͯίʔυΛಡΜͰΈΔ

Slide 12

Slide 12 text

ॳճىಈॲཧͷίʔυ QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, どうやらthis.attributes に入れたものが リクエストを跨いで引き継がれるらしい

Slide 13

Slide 13 text

ճ౴डཧͷίʔυ AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; } else { // 不正解の場合 resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; } if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける } else { // 全ての問題が終了した場合 var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`; this.emit(':tell', resultMessage + endMessage); // 会話を終える } }, this.event の深いところから ユーザの音声入力を受け取っているらしい 文字列結合のスタイルが異なったり Attributes を使い回しているところに 天然のレガシーコードらしさがある

Slide 14

Slide 14 text

ཧղ͕গ͚ͩ͠ਐΉ

Slide 15

Slide 15 text

'use strict'; var Alexa = require('alexa-sdk'); var questions = require('./questions.json'); var handlers = { QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; } else { // 不正解の場合 resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; } if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける } else { // 全ての問題が終了した場合 var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`; this.emit(':tell', resultMessage + endMessage); // 会話を終える } }, 'AMAZON.RepeatIntent': function () { var speechOutput = `${this.attributes['advance']}番。 ${questions[this.attributes['itemIndex']].q}`; this.emit(':ask', speechOutput, speechOutput); }, 'AMAZON.HelpIntent': function () { var speechOutput = 'クイズが出題されたらそれに答えてください。それでは始めましょう。「スタート」と言ってください。 '; this.emit(':ask', speechOutput, speechOutput); }, 'AMAZON.CancelIntent': function () { this.response.speak('終わります。'); this.emit(':responseReady'); }, 'AMAZON.StopIntent': function () { this.response.speak('終わります。'); this.emit(':responseReady'); }, LaunchRequest: function () { this.emit('QuizIntent'); }, 'AMAZON.StartOverIntent': function () { this.emit('QuizIntent'); }, Unhandled: function () { this.emit(':tell', 'すみません、わかりませんでした。終わります。'); } }; exports.handler = function (event, context, callback) { var alexa = Alexa.handler(event, context); alexa.registerHandlers(handlers); alexa.execute(); w ߦͷ+BWB4DSJQU w ϩδοΫ͸"84-BNCEBͰ࣮૷ w ࠓޙ͍ͭ͘΋ͷػೳ௥Ճ͕༧ఆ͞Ε͍ͯΔ w ओཁͳϩδοΫ͸ॳճىಈॲཧʢ2VJ[*OUFOUʣͱճ ౴डཧʢ"OTXFS*OUFOUʣͷΑ͏ͩ w ࣭໰σʔλ͸KTPOͰ؅ཧ͞Ε͍ͯΔ ཧղ͕গ͚ͩ͠ਐΉ

Slide 16

Slide 16 text

ͳ͓ɺςετίʔυ͸ͳ͍

Slide 17

Slide 17 text

IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM ςετͷͳ͍ίʔυ͸ѱ͍ίʔυͰ͋ΔɻͲΕ͚ͩ͏·͘ॻ͔Εͯ ͍Δ͔͸ؔ܎ͳ͍ɻͲΕ͚ͩඒ͍͔͠ɺΦϒδΣΫτࢦ޲͔ɺ͖ͪ ΜͱΧϓηϧԽ͞Ε͍ͯΔ͔͸ؔ܎ͳ͍ɻ ςετ͕͋Ε͹ɺݕূ͠ͳ͕Βίʔυͷಈ͖Λૉૣ͘มߋ͢Δ͜ͱ ͕Ͱ͖Δɻςετ͕ͳ͚Ε͹ɺίʔυ͕ྑ͘ͳ͍ͬͯΔͷ͔ѱ͘ ͳ͍ͬͯΔͷ͔͕ຊ౰ʹ͸Θ͔Βͳ͍ɻ .JDIBFM'FBUIFSTᐌ͘ ʰϨΨγʔίʔυվળΨΠυʱQWJ

Slide 18

Slide 18 text

ͪͳΈʹɺόʔδϣϯ؅ཧ΋͞Ε͍ͯͳ͍

Slide 19

Slide 19 text

΋ͪΖΜࣗಈԽ΋͞Ε͍ͯͳ͍

Slide 20

Slide 20 text

ιϑτ΢ΣΞ։ൃͷຊப͕Ұຊ΋ͳ͍ͱ͜Ζ͔Βͷελʔτͩͬͨ w 7FSTJPO$POUSPM w 5FTUJOH w "VUPNBUJPO 🙅 🙅 🙅 元のコードを書いたのがプロの開発者では ないので、これはしかたない

Slide 21

Slide 21 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 21/118

Slide 22

Slide 22 text

ຊபͷߏஙʹ༏ઌॱҐΛ͚ͭΔͳΒ Ґ7FSTJPO$POUSPM Ґ5FTUJOH Ґ"VUPNBUJPO

Slide 23

Slide 23 text

·ͣόʔδϣϯ؅ཧͱࣗಈԽ͕ٸ຿ w όʔδϣϯ؅ཧ͸༏ઌ౓࠷େ w όʔδϣϯ؅ཧແ͠ͰਐΊΔͷ͸ةݥ͗͢Δɻແ͍ͱ࿩ʹͳΒͳ͍ w ·ͣ͸ݱঢ়ͷίʔυͱઃఆϑΝΠϧΛશͯ(JUʹೖΕΔʢൿಗ৘ใ·ΘΓ͚ͩ஫ҙʣ w ࣗಈԽ͸ϨόϨοδ͕ޮ͖΍͍͢ͷͰૣظணख͢Δ w νʔϜ։ൃͷ৔߹ɺͩΕ͔Ұਓ͕ࣗಈԽ͢Ε͹શһ͕डӹऀʹͳΕΔ w ྫ͑͹Ϗϧυ΍σϓϩΠ͸Ұ౓ߏங͢Ε͹୯७࡞ۀͷ࣌ؒΛ҆ఆͯ͠࡟ݮͰ͖Δ w ؆୯ͳγΣϧεΫϦϓτ౳Ͱྑ͍ͷͰͱʹ͔͘ख࡞ۀΛݮΒ͢

Slide 24

Slide 24 text

࣍ʹࣗಈςετ

Slide 25

Slide 25 text

खͱ໨ͱࣖͱޱͰͷಈ࡞֬ೝ͸ෆ҆ఆͰେมͭΒ͍ ςετൣғ

Slide 26

Slide 26 text

ࣗಈςετ͸લઢج஍ w ࣗಈςετ͕ͳ͍ͱ҆શͳίʔυมߋͱࠓޙͷ։ൃܧଓ͕೉͍͠ w खͱ໨ͱޱͱࣖͰͷ֬ೝ͸ίετ͕͔͔Γ͗͢ɺෆ҆ఆͰɺϑΟʔυόοΫ΋஗͍ w ࠷ॳ͸໢ཏੑ͸ෆཁɻ೴ఱؾͳਖ਼ৗܥʢ)BQQZ1BUIʣͰ͍͍ͷͰɺಈࣗ͘ಈςετ͕ཉ͍͠ w ϦϑΝΫλϦϯά΁ͷ଱ੑΛߴΊΔͨΊɺ࣮૷͔Βؒ߹͍ΛऔͬͨςετΛॻ͖͍ͨ w طଘͷςετ͕ͳ͍ͷ͸ઃܭ͕ѱ͍ஹީͰ͋Γɺઃܭ͸ࠓޙมߋ͞ΕΔՄೳੑ͕ඇৗʹߴ͍ w मਖ਼ͨ͠Γػೳ௥Ճͨ͠Γ͢ΔͷͰɺطଘͷߏ଄ʹ݁߹ͨ͠ςετΛॻ͍ͯ΋ίεύ͕ѱ͍ w ࣮૷ͷৄࡉ͔Β͋Δఔ౓ڑ཭ΛऔΓͭͭɺ ࣗಈςετΛ҆ఆ࣮ͯ͠ߦͰ͖ΔϙΠϯτΛ୳͍ͨ͠ w ·ͣ͸ૈ͍ςετΛͻͱͭಈ͔͢ͱ͜Ζ·Ͱ͍͖͍࣋ͬͯͨ IUUQTCPPLNZOBWJKQFDQSPEVDUTEFUBJMJE

Slide 27

Slide 27 text

͜ͷཻ౓ͰࣗಈςετΛॻ͚ͳ͍͔ʁ 経験則としてはリクエスト/レスポンスあたりを狙えると 実装から距離を取りつつ安定したテストを書ける ςετൣғ ςετൣғ 経験則としてはリクエスト/レスポンスあたりを狙えると 実装から距離を取りつつ安定したテストを書ける

Slide 28

Slide 28 text

"MFYBͷࣗಈςετʹ࢖͑Δ ϥΠϒϥϦ΍ϑϨʔϜϫʔΫΛௐࠪ

Slide 29

Slide 29 text

"MFYB4LJMMͷࣗಈςετʹ࢖͏ಓ۩ͷީิΛͭݟ͚ͭͨʢ˞౰࣌ʣ w BMFYBTLJMMUFTUGSBNFXPSL w ߴػೳ͔ͭந৅Խ͞Ε͍ͯΔ൓໘ɺ͔Ώ͍ͱ͜Ζʹख͕ಧ͖ʹ͍͘ w &BTZࢦ޲ w BXTMBNCEBNPDLDPOUFYU w ػೳ͕গͳ͘ɺϨΠϠ͕ബ͍ w 4JNQMFࢦ޲ w ͜͜͸ϨΨγʔαόϯφɻ৘ใͷগͳ͍ઓ৔ʹ͸খ͘͞௚ަੑͷߴ͍4JNQMF ͳಓ۩͕ཉ͍͠ͷͰBXTMBNCEBNPDLDPOUFYUΛબ୒

Slide 30

Slide 30 text

BXTMBNCEBNPDLDPOUFYUͰςετΛॻ͍ͯΈΔ const assert = require('assert').strict; const index = require('../index'); const context = require('aws-lambda-mock-context'); describe('LaunchRequest を起動して最初の問題を出題', () => { let speechResponse; before((done) => { const ctx = context(); index.handler(require('./fixtures/launch.json'), ctx); ctx.Promise.then(resp => { speechResponse = resp; done(); }).catch(err => done(err)); }); it('handler の response', () => { assert(speechResponse !== undefined); }); }); 「undefined でなければ良い」程度の雑さ で最初のテストを書く Lambda レベルのテストは若干重いので beforeEach ではなくあえて before で書いている

Slide 31

Slide 31 text

ϦΫΤετΠϕϯτͷKTPO͸։ൃίϯιʔϧͷ௨৴ϩά͔ΒݟΑ͏ݟ·ͶͰࣗ࡞ { "version": "1.0", "session": { "new": true, "sessionId": "amzn1.echo-api.session.XXXXXXXX", "application": { "applicationId": "amzn1.ask.skill.XXXXXXXX" }, "attributes": { }, "user": { "userId": "amzn1.ask.account.XXXXXXXX" } }, "request": { "type": "IntentRequest", "requestId": "amzn1.echo-api.request.XXXXXXXX", "timestamp": "2018-10-26T11:38:40Z", "locale": "ja-JP", "intent": { "name": "LaunchRequest", "confirmationStatus": "NONE" } } }

Slide 32

Slide 32 text

Α͠Α͠ಈͧ͘ɻ͜Ε͸େ͖ͳҰา ( SFFO 👍 $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 ✓ handler の response 1 passing (19ms)

Slide 33

Slide 33 text

VOEF fi OFEͱͷൺֱͰ͸βϧͳͷͰ׬શҰகͷςετʹॻ͖׵͑Δ }); it('handler の response', () => { assert.deepEqual(speechResponse, { version: '1.0', response: { shouldEndSession: false, outputSpeech: { type: 'SSML', ssml: ' 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? ' }, reprompt: { outputSpeech: { type: 'SSML', ssml: ' 1番。 青森県の県庁所在地は? ' } } }, sessionAttributes: { advance: 1, score: 0, itemIndex: 4 }, userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0' }); }); });

Slide 34

Slide 34 text

ςετ͕ࣦഊ͢Δ 3 FE  $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 1) handler の response 0 passing (33ms) 1 failing

Slide 35

Slide 35 text

+ expected - actual { "response": { "outputSpeech": { - "ssml": " 簡単なクイズをしましょう。1番。 福井県の都道府県コード番号は? " + "ssml": " 簡単なクイズをしましょう。1番。 青森県の県庁所在地は? " "type": "SSML" } "reprompt": { "outputSpeech": { - "ssml": " 1番。 福井県の都道府県コード番号は? " + "ssml": " 1番。 青森県の県庁所在地は? " "type": "SSML" } } "shouldEndSession": false } "sessionAttributes": { "advance": 1 - "itemIndex": 71 + "itemIndex": 4 "score": 0 } "userAgent": "ask-nodejs/1.0.25 Node/v8.10.0" "version": "1.0" ग़ྗ͕ظ଴஋ͱ׬શҰக͠ͳ͍ 3 FE 🤔

Slide 36

Slide 36 text

ϥϯμϜੑΛ൐͏ϩδοΫ͕ࣦഊͷݩڟͩͬͨ QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 var random = Math.floor(Math.random() * questions.length); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, 3 FE

Slide 37

Slide 37 text

{ "q": "茨城県の県庁所在地は?", "a": "水戸", "g": "PrefecturalOfficeLocation" }, { "q": "茨城県の県の花は?", "a": "バラ", "g": "PrefectureFlower" }, { "q": "茨城県のローマ字表記は?", "a": "ibaraki", "g": "Romanization" }, { "q": "茨城県の都道府県コード番号は?", "a": "8", "g": "PrefectureOrder" }, { "q": "栃木県の県庁所在地は?", "a": "宇都宮", "g": "PrefecturalOfficeLocation" }, ࣭໰σʔλʢKTPOʣ

Slide 38

Slide 38 text

ϨΨγʔίʔυͷδϨϯϚ IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM lίʔυΛมߋ͢ΔͨΊʹ͸ςετΛ੔උ͢Δඞཁ ͕͋Δɻଟ͘ͷ৔߹ɺςετΛ੔උ͢ΔͨΊʹ͸ɺ ίʔυΛมߋ͢Δඞཁ͕͋Δz ʰϨΨγʔίʔυվળΨΠυʱQ

Slide 39

Slide 39 text

઀߹෦ʢ4FBNʣ l઀߹෦ʢ4FBNʣͱ͸ɺͦͷ৔ॴΛ௚઀ฤू͠ͳͯ͘΋ɺ ϓϩάϥϜͷৼΔ෣͍Λม͑Δ͜ͱͷͰ͖Δ৔ॴͰ͋Δz IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM ʰϨΨγʔίʔυվળΨΠυʱQ

Slide 40

Slide 40 text

Ҿ਺Λ઀߹෦ͱ͠ɺϥϯμϜੑΛ൐͏ؔ਺Λ֎͔Βࠩ͠ࠐΊΔΑ͏ʹ͢Δ var createHandler = function (getNextItemIndex) { return function (event, context, callback) { var alexa = Alexa.handler(event, context); alexa.registerHandlers(createHandlers(getNextItemIndex)); alexa.execute(); }; }; exports.createHandler = createHandler; exports.handler = function (event, context, callback) { var alexa = Alexa.handler(event, context); alexa.registerHandlers(handlers); alexa.execute(); }; exports.handler = function () { var getNextItemIndex = () => Math.floor(Math.random() * questions.length); return createHandler(getNextItemIndex); }(); 変更前 変更後 次の質問のインデックスを返す関数を渡す 変更後

Slide 41

Slide 41 text

var questions = require('./questions.json'); -var handlers = { +var createHandlers = function (getNextItemIndex) { + +return { QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 - var random = Math.floor(Math.random() * questions.length); + var random = getNextItemIndex(); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; @@ -29,7 +31,7 @@ var handlers = { if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; - var random = Math.floor(Math.random() * questions.length); + var random = getNextItemIndex(); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける @@ -65,9 +67,18 @@ var handlers = { ֎͔Β౉͞Εͨؔ਺Λ࢖͏Α͏ʹॱ࣍ॻ͖׵͑Δ

Slide 42

Slide 42 text

ςετ͔Β͸ϥϯμϜͰ͸ͳ͘ݻఆ஋Λฦؔ͢਺Λࠩ͠ࠐΉ before((done) => { const ctx = context(); const getNextItemIndex = () => 4; const handler = index.createHandler(getNextItemIndex); handler(require('./fixtures/launch.json'), ctx); ctx.Promise.then(resp => { speechResponse = resp; done(); }).catch(err => done(err)); }); 自動テストの決定性を担保するため 値を決め打ちする

Slide 43

Slide 43 text

)VNCMF0CKFDU1BUUFSO IUUQTXXXBNB[PODPKQEQ ςετ༰қੑΛԼ͍͛ͯΔཁૉΛബ͘੾Γग़͠ɺ ςετՄೳൣғΛ޿͘औΔجຊύλʔϯɻ ࠓճͰݴ͑͹ϥϯμϜੑΛബ͘੾Γग़ͨ͠

Slide 44

Slide 44 text

ɹɹɹग़ྗ͕ظ଴஋ͱҰக͢ΔΑ͏ʹͳͬͨɻ͜Ε͸͞Βʹେ͖ͳҰา $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 ✓ handler の response 1 passing (19ms) ( SFFO 👍

Slide 45

Slide 45 text

ɹɹɹ·ͣ͸ॳճىಈ࣌ɺਖ਼ղɺෆਖ਼ղͷςετέʔεΛ੔උ ( SFFO 👍 $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 ✓ handler の response 問題に正解した場合 ✓ handler の response 問題に不正解の場合 ✓ handler の response 3 passing (23ms)

Slide 46

Slide 46 text

ϨΨγʔίʔυվળٕ๏ͷதͰ͜͜·Ͱʹ࢖ͬͨ΋ͷ w ઀߹෦ͷݕ౼ͱۛຯ w ߜΓࠐΈ఺ͷൃݟͱґଘͷ෼཭ w )VNCMF0CKFDU1BUUFSO w ࠶ݱੑ͋Δςετϋʔωεͷ੔උ w εϓϥ΢τΫϥε

Slide 47

Slide 47 text

͔Ζ͏ͯ͡ಆ͏४උ͕੔ͬͨ

Slide 48

Slide 48 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 48/118

Slide 49

Slide 49 text

͜͜Ͱ࢓༷มߋͰ͢ ͕ͩɺ͍·΍ࢲͨͪʹ͸ςετ͕͋Δʂ ΍͍ͬͯͧ͘ʂʂ w ؒҧ͑ͯ΋ɺಉ͡໰୊Λճ·Ͱ܁Γฦ͍ͨ͠ w ؒҧ͑ͨճ਺ʹԠͯ͡ϝοηʔδΛม͍͑ͨ w ਖ਼ղͨ͠Β࣍ͷ໰୊ʹਐΉ w ճؒҧ͑ͨΒෆਖ਼ղͱͯ࣍͠ͷ໰୊ʹਐΉ

Slide 50

Slide 50 text

5%%ͷαΠΫϧ  ࣍ͷ໨ඪΛߟ͑ͯϦετΞοϓ͢Δ  Ϧετ͔ΒͻͱͭϐοΫΞοϓͯͦ͠ͷ໨ඪΛࣔ͢ςετΛͻͱͭॻ͘  ͦͷςετΛ࣮ߦࣦͯ͠ഊͤ͞Δ 3FE   ໨తͷίʔυΛॻ͘  Ͱॻ͍ͨςετΛ੒ޭͤ͞Δ (SFFO   ςετ͕௨Δ··ͰϦϑΝΫλϦϯάΛߦ͏ 3FGBDUPS   ̍ʙΛ܁Γฦ͢ IUUQTXXXPINTIBDPKQCPPL

Slide 51

Slide 51 text

}); }); -describe('問題に不正解の場合', () => { +describe('1回目の不正解の場合', () => { let speechResponse; before((done) => { const ctx = context(); const getNextItemIndex = () => 4; const handler = index.createHandler(getNextItemIndex); - handler(require('./fixtures/req_incorrect.json'), ctx); + const req = require('./fixtures/req_incorrect.json'); + Object.assign(req.session.attributes, { // 事前状態 + advance: 2, + score: 1, + accumIncorrects: 0, + itemIndex: 8 + }); + handler(req, ctx); ctx.Promise.then(resp => { speechResponse = resp; done(); }).catch(err => done(err)); }); + it('連続不正解数が増えていること', () => { + assert(speechResponse.sessionAttributes.accumIncorrects === 1); + }); - it('handler の response', () => { + it.skip('handler の response', () => { const expected = { ·ͣ͸ࣦഊ͢ΔςετΛॻ͘ 3 FE テストコード内で事前状態を作る 完全一致のテストはしばらくの間スキップ

Slide 52

Slide 52 text

༧૝௨Γςετ͕ࣦഊ͢Δ͜ͱΛ֬ೝ͢Δ $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 ✓ handler の response 問題に正解した場合 ✓ handler の response 1回目の不正解の場合 - handler の response 1) 連続不正解数が増えていること 2 passing (33ms) 1 pending 1 failing 1) 1回目の不正解の場合 連続不正解数が増えていること: AssertionError [ERR_ASSERTION]: # test/test.js:128 assert(speechResponse.sessionAttributes.accumIncorrects === 1) | | | | | | 0 false | Object{advance:3,score:1,accumIncorrects:0,itemIndex:4} Object{version:"1.0",response:#Object#,sessionAttributes:#Object#,userAgent:"ask-nodejs/1.0.25 Node/v8.10.0"} [number] 1 => 1 [number] speechResponse.sessionAttributes.accumIncorrects => 0 3 FE 完全一致のテストはしばらくの間スキップ

Slide 53

Slide 53 text

--- a/index.js +++ b/index.js @@ -21,15 +21,21 @@ return { var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; + var shouldRepeatSameQuestion = false; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; } else { // 不正解の場合 + this.attributes['accumIncorrects']++; + shouldRepeatSameQuestion = true; resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; } - if (this.attributes['advance'] < 7) { // 続きの問題がある場合 + if (shouldRepeatSameQuestion) { + var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`; + this.emit(':ask', resultMessage, reprompt); + } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; ࣦഊ͍ͯ͠ΔςετΛ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘

Slide 54

Slide 54 text

ςετͷ੒ޭΛ֬ೝ͢Δ $ npm test > mocha --require intelli-espower-loader test LaunchRequest を起動して最初の問題を出題 ✓ handler の response 問題に正解した場合 ✓ handler の response 1回目の不正解の場合 ✓ 連続不正解数が増えていること - handler の response 3 passing (21ms) 1 pending ( SFFO

Slide 55

Slide 55 text

--- a/test/test.js +++ b/test/test.js @@ -101,6 +101,9 @@ describe('1回目の不正解の場合', () => { it('連続不正解数が増えていること', () => { assert(speechResponse.sessionAttributes.accumIncorrects === 1); }); + it('返答の音声内容が1回目の不正解に伴う内容であること', () => { + assert(speechResponse.response.outputSpeech.ssml === ' 久慈? もう 一度言ってください。岩手県の県庁所在地は? '); + }); it.skip('handler の response', () => { const expected = { version: '1.0', 1回目の不正解の場合 ✓ 連続不正解数が増えていること 1) 返答の音声内容が1回目の不正解に伴う内容であること - handler の response 3 passing (37ms) 1 pending 1 failing ·ࣦͣഊ͢ΔςετΛॻ͘ 3 FE

Slide 56

Slide 56 text

1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること - handler の response 4 passing (21ms) 1 pending --- a/index.js +++ b/index.js @@ -29,7 +29,7 @@ return { } else { // 不正解の場合 this.attributes['accumIncorrects']++; shouldRepeatSameQuestion = true; - resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; } if (shouldRepeatSameQuestion) { ੒ޭͤ͞ΔͨΊͷ࠷খݶͷίʔυΛॻ͘ ( SFFO

Slide 57

Slide 57 text

assert(speechResponse.response.outputSpeech.ssml === ' 久慈? もう一度言ってくださ い。岩手県の県庁所在地は? '); }); + it('進行状況が進んでいないこと', () => { + assert(speechResponse.sessionAttributes.advance === 2); + }); + it('得点が変わらないこと', () => { + assert(speechResponse.sessionAttributes.score === 1); + }); + it('問題番号が変わらないこと', () => { + assert(speechResponse.sessionAttributes.itemIndex === 8); + }); it.skip('handler の response', () => { const expected = { 5%%ͷճసΛ܁Γฦ͢ ( SFFO 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと - handler の response 7 passing (22ms) 1 pending

Slide 58

Slide 58 text

it('問題番号が変わらないこと', () => { assert(speechResponse.sessionAttributes.itemIndex === 8); }); - it.skip('handler の response', () => { + it('handler の response', () => { const expected = { version: '1.0', response: { shouldEndSession: false, outputSpeech: { type: 'SSML', - ssml: ' ちがいます。正解は盛岡です。 では3番。 青森県の県庁所在地は? ' + ssml: ' 久慈? もう一度言ってください。岩手県の県庁所在地は? ' }, reprompt: { outputSpeech: { type: 'SSML', - ssml: ' 3番。 青森県の県庁所在地は? ' + ssml: ' 2番。 岩手県の県庁所在地は? ' } } }, sessionAttributes: { - advance: 3, + advance: 2, score: 1, - itemIndex: 4 + accumIncorrects: 1, + itemIndex: 8 }, userAgent: 'ask-nodejs/1.0.25 Node/v8.10.0' ׬શҰகͷςετ΋εΩοϓΛ΍ΊͯάϦʔϯʹ෮ؼ ( SFFO 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 8 passing (20ms) このタイミングで完全一致のテストを復帰

Slide 59

Slide 59 text

describe('2回目の不正解の場合', () => { let speechResponse; before((done) => { const ctx = context(); const getNextItemIndex = () => 4; const handler = index.createHandler(getNextItemIndex); const req = require('./fixtures/req_incorrect.json'); Object.assign(req.session.attributes, { advance: 2, score: 1, accumIncorrects: 1, itemIndex: 8 }); handler(req, ctx); ctx.Promise.then(resp => { speechResponse = resp; done(); }).catch(err => done(err)); }); it('連続不正解数が増えていること', () => { assert(speechResponse.sessionAttributes.accumIncorrects === 2); }); it('返答の音声内容が2回目の不正解に伴う内容であること', () => { assert(speechResponse.response.outputSpeech.ssml === ' 私には「久慈」 と聞こえましたが、それは正しくありません。もう一度言ってください。岩手県の県庁所在地は? '); }); it('進行状況が進んでいないこと', () => { assert(speechResponse.sessionAttributes.advance === 2); }); it('得点が変わらないこと', () => { assert(speechResponse.sessionAttributes.score === 1); }); it('問題番号が変わらないこと', () => { assert(speechResponse.sessionAttributes.itemIndex === 8); }); }); 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること 1) 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと 12 passing (47ms) 1 failing 5%%ͷճసΛ܁Γฦ͢ 3 FE

Slide 60

Slide 60 text

--- a/index.js +++ b/index.js @@ -29,7 +29,11 @@ return { } else { // 不正解の場合 this.attributes['accumIncorrects']++; shouldRepeatSameQuestion = true; - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; + if (this.attributes['accumIncorrects'] == 1) { + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; + } else if (this.attributes['accumIncorrects'] == 2) { + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。も う一度言ってください。${currentQuestion.q}`; + } } if (shouldRepeatSameQuestion) { 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと 13 passing (22ms) 5%%ͷճసΛ܁Γฦ͢ ( SFFO

Slide 61

Slide 61 text

--- a/index.js +++ b/index.js @@ -29,11 +29,14 @@ return { } else { // 不正解の場合 this.attributes['accumIncorrects']++; shouldRepeatSameQuestion = true; - if (this.attributes['accumIncorrects'] == 1) { - resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; - } else if (this.attributes['accumIncorrects'] == 2) { - resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ てください。${currentQuestion.q}`; - } + switch (this.attributes['accumIncorrects']) { + case 1: + resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; + break; + case 2: + resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言っ てください。${currentQuestion.q}`; + break; + } } if (shouldRepeatSameQuestion) { 5%%ͷճసΛ܁Γฦ͢ 3 FGBDUPS

Slide 62

Slide 62 text

describe('3回目の不正解の場合', () => { let speechResponse; before((done) => { const ctx = context(); const getNextItemIndex = () => 4; const handler = index.createHandler(getNextItemIndex); const req = require('./fixtures/req_incorrect.json'); Object.assign(req.session.attributes, { advance: 2, score: 1, accumIncorrects: 2, itemIndex: 8 }); handler(req, ctx); ctx.Promise.then(resp => { speechResponse = resp; done(); }).catch(err => done(err)); }); it('連続不正解数が0に戻っていること', () => { assert(speechResponse.sessionAttributes.accumIncorrects === 0); }); it('返答の音声内容が3回目の不正解に伴う内容であること', () => { assert(speechResponse.response.outputSpeech.ssml === ' ちがいま す。正解は盛岡です。 では3番。 青森県の県庁所在地は? '); }); it('次の問題に進んでいるので進行状況が進んでいること', () => { assert(speechResponse.sessionAttributes.advance === 3); }); it('得点が変わらないこと', () => { assert(speechResponse.sessionAttributes.score === 1); }); it('次の問題に進んでいるので問題番号が変わっていること', () => { assert(speechResponse.sessionAttributes.itemIndex === 4); }); }); า෯Λ޿͛ͯ։ൃͷϖʔεΛ͞Βʹ଎ΊΔ 3 FE 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと 3回目の不正解の場合 1) 連続不正解数が0に戻っていること 2) 返答の音声内容が3回目の不正解に伴う内容であること 3) 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が変わらないこと 4) 次の問題に進んでいるので問題番号が変わっていること 14 passing (52ms) 4 failing

Slide 63

Slide 63 text

ҰؾʹάϦʔϯʹ͢Δ diff --git a/index.js b/index.js index 65f66d8..4498974 100644 --- a/index.js +++ b/index.js @@ -28,13 +28,18 @@ return { this.attributes['score']++; } else { // 不正解の場合 this.attributes['accumIncorrects']++; - shouldRepeatSameQuestion = true; switch (this.attributes['accumIncorrects']) { case 1: resultMessage = `${usersAnswer}? + shouldRepeatSameQuestion = true; break; case 2: resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。 もう一度言ってください。${currentQuestion.q}`; + shouldRepeatSameQuestion = true; + break; + default: + resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; + this.attributes['accumIncorrects'] = 0; break; } } 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと 3回目の不正解の場合 ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容が3回目の不正解に伴う内容であること ✓ 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が変わらないこと ✓ 次の問題に進んでいるので問題番号が変わっていること 18 passing (19ms) ( SFFO

Slide 64

Slide 64 text

LaunchRequest を起動して最初の問題を出題 ✓ 連続不正解数が0であること ✓ 返答の音声内容が初回の出題に伴う内容であること ✓ 進行状況は1であること ✓ 得点は0であること ✓ 出題された問題番号は 4 ✓ handler の response 問題に正解した場合 ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容が問題の正解に伴う内容であること ✓ 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が1増えていること ✓ 次の問題に進んでいるので問題番号が変わっていること ✓ handler の response 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題番号が変わらないこと 3回目の不正解の場合 ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容が3回目の不正解に伴う内容であること ✓ 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が変わらないこと ✓ 次の問題に進んでいるので問題番号が変わっていること 最終問題に正解した場合 ✓ shouldEndSession が true になっていること ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容がクイズ終了を知らせる内容であること ✓ 進行状況が変わらないこと ✓ 得点が1増えていること ✓ 問題番号が変わらないこと ✓ handler の response 35 passing (32ms) ( SFFO ΍͔ͬͨʂʁ ˞ϑϥάɹɹɹɹ 5%%ͷճసΛճ͠ଓ͚ɺ͖͞΄Ͳͷ ࢓༷มߋΛຬͨ͢ςετͱίʔυ͕ ΦʔϧάϦʔϯ·Ͱ΍͖ͬͯͨ 35 passing (32ms)

Slide 65

Slide 65 text

͕ͩɺͪΐͬͱ଴ͬͯ΄͍͠

Slide 66

Slide 66 text

ઃܭ͸ྑ͘ͳ͍ͬͯΔͩΖ͏͔ IUUQTUXPQBHJMFFTNDPKQXIBUEPXFOFFEGPSHSPXUIPGGVUVSFDCBGF رബԽͨ͠5%%ɺϓϩμΫτͷ੒௕ͷͨΊʹඞཁͳ΋ͷ͸ʁʙʰ݈શͳϏδωεͷܧଓత੒௕ͷͨΊʹ͸݈શͳίʔυ͕ඞཁͩʱରஊʢ̒ʣΑΓ

Slide 67

Slide 67 text

͜͜·Ͱॻ͍͖ͯͨίʔυΛݟͯΈΔͱɺ͜Ε͸ʜʜ ͜ɺ͜Ε͸ʜʜ😨 QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化 var random = getNextItemIndex(); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; var shouldRepeatSameQuestion = false; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; this.attributes['accumIncorrects'] = 0; } else { // 不正解の場合 this.attributes['accumIncorrects']++; switch (this.attributes['accumIncorrects']) { case 1: resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; shouldRepeatSameQuestion = true; break; case 2: resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`; shouldRepeatSameQuestion = true; break; default: resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; this.attributes['accumIncorrects'] = 0; break; } } if (shouldRepeatSameQuestion) { var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`; this.emit(':ask', resultMessage, reprompt); } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; var random = getNextItemIndex(); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける } else { // 全ての問題が終了した場合 var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`; this.emit(':tell', resultMessage + endMessage); // 会話を終える } },

Slide 68

Slide 68 text

ϓϩμΫτίʔυͷ࣭͸ Ή͠Ζ௿Լ͍ͯ͠ΔͷͰ͸ʜʜʁ

Slide 69

Slide 69 text

֏͞Μᐌ͘ ςετͰ͸඼࣭͸্͕Βͳ͍Ͱ͢Αɻςετ͸͋͘·Ͱ΋ ඼࣭Λ͋͛Δ͖͔͚ͬɻ඼࣭Λ͋͛Δͷ͸ϓϩάϥϛϯ άͰ͢ɻ͜Ε͸େੲ͔Βͦ͏ɻ

Slide 70

Slide 70 text

͜͜Ͱ௥͍ଧͪΛ͔͚ΔΑ͏ʹ࢓༷มߋ ͑ͬɺ͍·ʜʜʁ😨 w ໰ΛϒϩοΫʢ  ʣʹ۠੾ΓɺલͷϒϩοΫͰؒҧ͑ͨ δϟϯϧʢݝிॴࡏ஍౳ʣΛ༏ઌͯ͠ग़୊͍ͨ͠ w ϒϩοΫ಺Ͱಉ͡δϟϯϧ͸ग़͞ͳ͍ w ࠷ॳͷϒϩοΫͰ͸δϟϯϧ͕ॏͳΒͳ͍Α͏ʹϥϯμϜʹ໰ ग़͍ͨ͠

Slide 71

Slide 71 text

͞Βʹ௥͍ଧͪΛ͔͚ΔΑ͏ʹج൫ϥΠϒϥϦͷαϙʔτऴྃ ͑ͬɺ͍·ʜʜʁ😭 npm WARN deprecated [email protected]: This version of the Alexa Skills Kit SDK is no longer supported. Please use the v2 release found here: https://github.com/alexa/alexa-skills- kit-sdk-for-nodejs

Slide 72

Slide 72 text

͜Μͳ͸ͣͰ͸ͳ͔ͬͨ

Slide 73

Slide 73 text

IUUQTXXXBNB[PODPKQEQ ʰ$MFBO"SDIJUFDUVSFʱQ ʮ͋ͱͰΫϦʔϯʹ͢Ε͹͍͍Αɻઌʹࢢ৔ʹग़͞ͳ͚Ε͹ʂʯ ։ൃऀ͸ͦ͏΍͍ͬͯͭ΋͝·͔͢ɻ͕ͩɺ͋ͱͰΫϦʔϯʹ͢Δ͜ͱ͸ͳ͍ɻ ࢢ৔͔ΒͷϓϨογϟʔ͸ࢭ·Βͳ͍͔Βͩɻʮઌʹࢢ৔ʹग़͞ͳ͚Ε͹ʯͱ͍ ͏͜ͱ͸ɺޙΖʹڝ߹ଞ͕ࣾେ੎͍Δͱ͍͏͜ͱͰ͋Δɻڝ߹ଞࣾʹ௥͍ൈ͔Ε ͳ͍ͨΊʹ͸ɺ͜Ε͔Β΋૸Γଓ͚Δ͔͠ͳ͍ɻ ͦͷ݁Ռɺ։ൃऀ͸ϞʔυΛ੾Γସ͑Δ͜ͱ͕Ͱ͖ͳ͍ɻ࣍ͷػೳɺ·ͨ࣍ͷػ ೳɺ·ͨ·ͨ࣍ͷػೳΛ௥Ճ͢Δ͜ͱʹͳΓɺίʔυΛΫϦʔϯʹ͢Δ͜ͱ·Ͱ ख͕ճΒͳ͍ɻ ͦͯ͠ɺ่յ͕࢝·Δɻੜ࢈ੑ͕θϩʹ͍͍ۙͮͯ͘ɻ 6ODMF#PCᐌ͘

Slide 74

Slide 74 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 74/118

Slide 75

Slide 75 text

ݱঢ়ͷͭΒ͞Λ෼ੳ͢Δ w ͭͷมߋཁҼʢج൫ͷࣄ৘ɺ࢓༷ͷมߋʣ͕୯ҰͷϓϩμΫτίʔυʹ͢΂ ͯ߱Γ͔͔ͬͯ͘Δ w ୯Ұ੹೚ͷݪଇʢ431ʣʹ൓͍ͯ͠Δͱ͍͑Δ w ج൫ͷΞοϓσʔτͱ࢓༷มߋɺ΋ͪΖΜͲͪΒ΋΍Βͳ͚Ε͹ͳΒͳ͍ w ݱঢ়͸ΠϕϯτϋϯυϥʹશͯͷυϝΠϯϩδοΫ͕ॻ͔Ε͍ͯΔঢ়ଶʹ౳͠ ͍ɻ͜ͷ··Ͱ͸ະདྷ͕ͳ͍ w υϝΠϯϞσϧΛ"MFYBͱ-BNCEB͔ΒҾ͖͸͕͍ͨ͠

Slide 76

Slide 76 text

ઓज़&YUSBDUPS4QSPVU w &YUSBDU w طଘͷίʔυʹςετΛॻ͍ͯอޢ͠ͳ͕Βɺ৽͘͠ίʔυΛநग़͍ͯ͘͠ w 4QSPVU w طଘͷίʔυʹςετΛॻ͘͜ͱ͸͖͋ΒΊΔͱͯ͠΋ɺͤΊͯ৽͘͠ॻ͘ ίʔυ͚ͩ͸ςετΛॻ͖ͳ͕Β։ൃ͢Δ IUUQTXXXTIPFJTIBDPKQCPPLEFUBJM

Slide 77

Slide 77 text

&YUSBDUઓज़ͱ4QSPVUઓज़ͷςετൣғ 4QSPVU &YUSBDU

Slide 78

Slide 78 text

࡞ઓυϝΠϯϞσϧΛநग़ʢ&YUSBDUʣ͢Δઓज़Λબ୒ w ͜͜·Ͱॻ͍͖ͯͨૈཻ౓ͷ-BNCEBϨϕϧͷςετΛάϦʔϯʹอͪ ͳ͕ΒɺϩδοΫΛυϝΠϯϞσϧΫϥεʹҾ͖͸͕͍ͯ͘͠ w υϝΠϯϞσϧ͸"MFYB΍-BNCEBʹґଘͤ͞ͳ͍ w υϝΠϯϞσϧ͸ςετۦಈ։ൃͰ৽ن։ൃ͢Δ w ࢓༷͕ݻ·Βͳ͍͔Βςετ͕ॻ͚ͳ͍ͷͰ͸ͳ͘ɺ࢓༷͕ݻ·Βͳ͍͔ Βͦ͜ςετΛॻ͍ͯมԽΛࢧ͍͑ͯ͘

Slide 79

Slide 79 text

ςετϐϥϛουͷԼஈΛ෼ް͘͢Δઓज़ Ϣχοτ ΠϯςάϨʔγϣϯ && ίετ ஧࣮ੑ ଎౓ ςετέʔε਺ ߴ ௿ ௿ ߴ ܾఆੑ

Slide 80

Slide 80 text

ɹ1MBJO0MEͳϞσϧΫϥεΛॻ͚͹"MFYB΍-BNCEBʹґଘͤͣςετ͕ॻ͚Δ const assert = require('assert').strict; const {Session} = require('../models'); describe('Session#start(item)', () => { let session; beforeEach(() => { session = new Session(); session.start({ q: '広島県の県の花は?', a: 'モミジ', g: 'PrefectureFlower' }); }); it('#message', () => { assert(session.message() === '簡単なクイズをしましょう。1番。 広島県の県の花は?'); }); it('#reprompt', () => { assert(session.reprompt() === '1番。 広島県の県の花は?'); }); }); 3 FE Plain Old なオブジェクトのテストは非常に高速に 動作するので beforeEach を気軽に使える

Slide 81

Slide 81 text

-BNCEB͔ΒϩδοΫΛίϐʔͯ͠ςετΛ௨͢ class Session { start (item) { this.advance = 1; this.score = 0; this.accumIncorrects = 0; this.item = item; } message () { return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`; } reprompt () { return `${this.advance}番。 ${this.item.q}`; } } module.exports = { Session }; ( SFFO

Slide 82

Slide 82 text

'use strict'; var Alexa = require('alexa-sdk'); var questions = require('./questions.json'); +const {Session} = require('./models'); var createHandlers = function (getNextItemIndex) { return { QuizIntent: function () { // 初期状態 - this.attributes['advance'] = 1; // 進行状況を初期化 - this.attributes['score'] = 0; // 得点を初期化 - this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化 var random = getNextItemIndex(); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 - var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; - var reprompt = `1番。 ${questions[random].q}`; - this.emit(':ask', message, reprompt); // 相手の回答を待つ + const session = new Session(); + session.start(questions[random]); + this.attributes['advance'] = session.advance; + this.attributes['score'] = session.score; + this.attributes['accumIncorrects'] = session.accumIncorrects; + this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ }, -BNCEB͕ϞσϧΛ࢖͏Α͏ʹมߋ ( SFFO

Slide 83

Slide 83 text

࣍͸ճ౴डཧॲཧΛςετϑΝʔετͰॻ͘ describe('Session#receive(answer)', () => { let session; beforeEach(() => { session = new Session(); session.start({ q: '広島県の県の花は?', a: 'モミジ', g: 'PrefectureFlower' }); }); describe('正解した場合', () => { beforeEach(() => { session.receive('モミジ'); }); it('score が加点されていること', () => { assert(session.score === 1); }); }); }); 3 FE

Slide 84

Slide 84 text

γϯϓϧʹςετΛάϦʔϯʹ͢Δ class Session { start (item) { this.advance = 1; this.score = 0; this.accumIncorrects = 0; this.item = item; } message () { return `簡単なクイズをしましょう。${this.advance}番。 ${this.item.q}`; } reprompt () { return `${this.advance}番。 ${this.item.q}`; } receive (answer) { if (this.item.a === answer) { this.score += 1; } } } module.exports = { Session }; ( SFFO

Slide 85

Slide 85 text

$ npm run test:models 初回の出題時 ✓ 問題番号は 1 ✓ 得点は 0 ✓ 連続不正解数は 0 ✓ 次の問題に進むこと ✓ メッセージ ✓ reprompt 3問目に初挑戦する状態 正解した場合 ✓ 問題番号がひとつ進むこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進むこと ✓ 次の問題を設定した後のメッセージ 不正解の場合 ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 3問目に既に1回不正解の状態 不正解の場合 ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 3問目に既に2回不正解の状態 不正解の場合 ✓ 問題番号がひとつ進むこと ✓ 得点が変わらないこと ✓ 連続不正解数がリセットされていること ✓ 次の問題に進むこと ✓ 次の問題を設定した後のメッセージ 最終問題に初挑戦する状態 正解した場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ 不正解の場合 ✓ isFinished は false ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 最終問題に2回不正解している状態 正解した場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ 不正解の場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ 50 passing (59ms) Ҏ߱5%%ͷճసΛଓ͚ɺϩδοΫΛ-BNCEB͔Β͋Δఔ౓Ҡಈͨ͠ class Session { start (item) { this.advance = 1; this.score = 0; this.accumIncorrects = 0; this.item = item; } restore (attrs) { Object.assign(this, attrs); } setNextItem (item) { this.item = item; } message () { if (this.shouldRepeatSameQuestion()) { return this.resultMessage; } if (this.advance === 7) { return `${this.resultMessage}終わりです。あなたは${this.score}点でした。`; } if (this.advance === 1) { return `簡単なクイズをしましょう。${this.reprompt()}`; } return `${this.resultMessage}${this.reprompt()}`; } reprompt () { return `${this.advance}番。 ${this.item.q}`; } receive (answer) { this.shouldRepeat = false; if (this.item.a === answer) { this.resultMessage = 'そうです。 では'; this.score += 1; this.accumIncorrects = 0; if (this.advance < 7) { this.advance += 1; } } else { this.accumIncorrects += 1; switch (this.accumIncorrects) { case 1: this.resultMessage = `${answer}? もう一度言ってください。${this.item.q}`; this.shouldRepeat = true; break; case 2: this.resultMessage = `私には「${answer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${this.item.q}`; this.shouldRepeat = true; break; default: this.resultMessage = `ちがいます。正解は${this.item.a}です。 では`; this.accumIncorrects = 0; if (this.advance < 7) { this.advance += 1; } break; } } } isFinished () { return this.advance === 7 && !this.shouldRepeat; } shouldRepeatSameQuestion () { return this.shouldRepeat === true; } }

Slide 86

Slide 86 text

ॳճىಈϋϯυϥͷݱঢ় QuizIntent: function () { // 初期状態 var random = getNextItemIndex(); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 const session = new Session(); session.start(questions[random]); this.attributes['advance'] = session.advance; this.attributes['score'] = session.score; this.attributes['accumIncorrects'] = session.accumIncorrects; this.emit(':ask', session.message(), session.reprompt()); }, まだロジックを切り出しただけ。 Lambda レベルのテストのグリーンを保つため、 リクエストをまたがるデータの持ち方は変えない

Slide 87

Slide 87 text

ճ౴डཧϋϯυϥͷݱঢ় AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; const session = new Session(); session.restore({ advance: this.attributes['advance'], score: this.attributes['score'], accumIncorrects: this.attributes['accumIncorrects'], item: currentQuestion }); session.receive(usersAnswer); this.attributes['advance'] = session.advance; this.attributes['score'] = session.score; this.attributes['accumIncorrects'] = session.accumIncorrects; if (session.shouldRepeatSameQuestion()) { this.emit(':ask', session.message(), session.reprompt()); } else if (session.isFinished()) { // 全ての問題が終了した場合 this.emit(':tell', session.message()); // 会話を終える } else { // 続きの問題がある場合 var random = getNextItemIndex(); this.attributes['itemIndex'] = random; session.setNextItem(questions[random]); this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける } }, 現在の状態や正答/誤答にもとづく メッセージがLambdaから消えた

Slide 88

Slide 88 text

·ͩ·ͩߦͧ͘

Slide 89

Slide 89 text

࡞ઓৄࡉ΁ͷґଘΛݮΒ͍ͯ͘͠ w Ϟσϧͷ͍࣋ͬͯΔ৘ใΛϦΫΤετΛ·͍ͨͰμϯϓϦετ ΞͰ͖ΔΑ͏ʹͯ͠ɺ"MFYBݻ༗ͷػೳʢBUUSJCVUFTʣ΁ͷґଘ ΛݮΒ͍ͯ͘͠ w -BNCEBϨϕϧͷςετ͸Ұ࣌తʹ੺͘ͳΔ͕ɺظ଴஋ʹμϯ ϓσʔλΛՃ͑ΔܗͰ྘ʹ໭͍ͯ͘͠ w ϞσϧΫϥε͸Ҿ͖ଓ͖ςετۦಈ։ൃͰ։ൃ͢Δ

Slide 90

Slide 90 text

·ͣEVNQػೳͷςετΛॻ͍ͯ੺͘͢Δ 3 FE describe('Session#dump()', () => { let session; beforeEach(() => { session = new Session(); session.start({ q: '広島県の県の花は?', a: 'モミジ', g: 'PrefectureFlower' }); }); it('dump', () => { assert.deepEqual(session.dump(), { advance: 1, score: 0, accumIncorrects: 0, item: { q: '広島県の県の花は?', a: 'モミジ', g: 'PrefectureFlower' } }); }); });

Slide 91

Slide 91 text

γϯϓϧʹάϦʔϯʹ͢Δ class Session { start (item) { this.advance = 1; this.score = 0; this.accumIncorrects = 0; this.item = item; } dump () { return { advance: this.advance, score: this.score, accumIncorrects: this.accumIncorrects, item: this.item }; } restore (attrs) { Object.assign(this, attrs); } } ( SFFO

Slide 92

Slide 92 text

Ҏ߱5%%ͷճసΛଓ͚ɺμϯϓ͚ͩͰϦΫΤετΛ·ͨ͛ΔΑ͏ʹͳͬͨ 初回の出題時 ✓ 問題番号は 1 ✓ 得点は 0 ✓ 連続不正解数は 0 ✓ 次の問題に進むこと ✓ メッセージ ✓ reprompt ✓ dump 3問目に初挑戦する状態 正解した場合 ✓ 問題番号がひとつ進むこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進むこと 正解後に次の問題を設定した状態 ✓ dump ✓ メッセージ 不正解の場合 ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 3問目に既に1回不正解の状態 不正解の場合 ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 3問目に既に2回不正解の状態 不正解の場合 ✓ 問題番号がひとつ進むこと ✓ 得点が変わらないこと ✓ 連続不正解数がリセットされていること ✓ 次の問題に進むこと 不正解後に次の問題を設定した状態 ✓ dump ✓ メッセージ 最終問題に初挑戦する状態 正解した場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ 不正解の場合 ✓ isFinished は false ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数が増えていること ✓ 同じ問題を繰り返すこと ✓ メッセージ 最終問題に2回不正解している状態 正解した場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が加点されていること ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ 不正解の場合 ✓ isFinished は true ✓ 問題番号が変わらないこと ✓ 得点が変わらないこと ✓ 連続不正解数がリセットされていること ✓ 次の問題に進まないこと ✓ メッセージ LaunchRequest を起動して最初の問題を出題 ✓ 連続不正解数が0であること ✓ 返答の音声内容が初回の出題に伴う内容であること ✓ 進行状況は1であること ✓ 得点は0であること ✓ 出題された問題 ✓ handler の response 問題に正解した場合 ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容が問題の正解に伴う内容であること ✓ 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が1増えていること ✓ 次の問題に進んでいるので問題が変わっていること ✓ handler の response 1回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が1回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題が変わらないこと ✓ handler の response 2回目の不正解の場合 ✓ 連続不正解数が増えていること ✓ 返答の音声内容が2回目の不正解に伴う内容であること ✓ 進行状況が進んでいないこと ✓ 得点が変わらないこと ✓ 問題が変わらないこと 3回目の不正解の場合 ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容が3回目の不正解に伴う内容であること ✓ 次の問題に進んでいるので進行状況が進んでいること ✓ 得点が変わらないこと ✓ 次の問題に進んでいるので問題が変わっていること 最終問題に正解した場合 ✓ shouldEndSession が true になっていること ✓ 連続不正解数が0に戻っていること ✓ 返答の音声内容がクイズ終了を知らせる内容であること ✓ 進行状況が変わらないこと ✓ 得点が1増えていること ✓ 問題が変わらないこと ✓ handler の response 88 passing (82ms) QuizIntent: function () { // 初期状態 const session = new Session(); session.start(questions[getNextItemIndex()]); this.attributes['dump'] = session.dump(); this.emit(':ask', session.message(), session.reprompt()); // 相手の回答を待つ }, AnswerIntent: function () { // ユーザからの返答により起動される const usersAnswer = this.event.request.intent.slots.Answer.value; const session = new Session(); session.restore(this.attributes['dump']); session.receive(usersAnswer); if (session.shouldRepeatSameQuestion()) { this.attributes['dump'] = session.dump(); this.emit(':ask', session.message(), session.reprompt()); } else if (session.isFinished()) { // 全ての問題が終了した場合 this.attributes['dump'] = session.dump(); this.emit(':tell', session.message()); // 会話を終える } else { // 続きの問題がある場合 session.setNextItem(questions[getNextItemIndex()]); this.attributes['dump'] = session.dump(); this.emit(':ask', session.message(), session.reprompt()); // 会話を続ける } },

Slide 93

Slide 93 text

·ͩ·ͩߦͧ͘

Slide 94

Slide 94 text

࡞ઓঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அͰ͖ΔΑ͏ʹ͢Δ w ঢ়ଶભҠ΋Ϟσϧࣗ਎͕൑அͰ͖ΔΑ͏ʹͯ͠ɺ-BNCEB΍ "MFYBͱͷ઀஍໘ʢ݁߹౓ʣΛߋʹݮΒ͢ w -BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··૸Γଓ͚Δ w ϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

Slide 95

Slide 95 text

࣍ͷ໰୊ʹਐΉ͔Ͳ͏͔΋Ϟσϧࣗ਎͕൑அ͢Δ class Session { constructor ({env}) { this.env = env; } start () { this.advance = 1; this.score = 0; this.accumIncorrects = 0; this.item = this.env.nextItem(); } command () { return this.isFinished() ? ':tell' : ':ask'; } receive (answer) { // 省略 if (!this.shouldRepeatSameQuestion() && !this.isFinished()) { // 続きの問題がある場合 this.item = this.env.nextItem(); } } isFinished () { return this.advance === 7 && !this.shouldRepeat; } }

Slide 96

Slide 96 text

-BNCEBଆͷ੹຿͕ݮΓɺ͔ͳΓεοΩϦ͖ͯͨ͠ QuizIntent: function () { // 初期状態 const session = new Session({env}); session.start(); this.attributes['dump'] = session.dump(); this.emit(session.command(), session.message(), session.reprompt()); }, AnswerIntent: function () { // ユーザからの返答により起動される const usersAnswer = this.event.request.intent.slots.Answer.value; const session = new Session({env}); session.restore(this.attributes['dump']); session.receive(usersAnswer); this.attributes['dump'] = session.dump(); this.emit(session.command(), session.message(), session.reprompt()); }, 現在の状態に基づく 条件分岐がなくなった

Slide 97

Slide 97 text

΋͏গ͠ߦͧ͘

Slide 98

Slide 98 text

࡞ઓ݁߹౓Λ͞ΒʹݮΒ͢ w ΫϥεΛެ։ͤͣɺϑΝΫτϦʔؔ਺͚ͩΛެ։͢Δ͜ͱͰ಺ ෦ߏ଄Λ͞ΒʹӅṭ͠ɺ-BNCEB΍"MFYBͱͷ઀஍໘ʢ݁߹ ౓ʣΛ࠷খʹ͢Δ w -BNCEBϨϕϧͷςετ͸ΦʔϧάϦʔϯͷ··૸Γଓ͚Δ w ϞσϧΫϥε͸ςετۦಈ։ൃͰܧଓ։ൃ͢Δ

Slide 99

Slide 99 text

ந৅౓ͷߴ͍ؔ਺ͷΈΛެ։͢Δ const startSession = ({env}) => { const session = new Session({env}); session.start(); return session; }; const restoreSession = ({env, dump}) => { const session = new Session({env}); session.restore(dump); return session; }; module.exports = { startSession, restoreSession }; Session クラスの export もやめる

Slide 100

Slide 100 text

'use strict'; var Alexa = require('alexa-sdk'); var questions = require('./questions.json'); -const {Session} = require('./models'); +const {startSession, restoreSession} = require('./models'); var createHandlers = function (getNextItemIndex) { const env = { @@ -13,24 +13,21 @@ var createHandlers = function (getNextItemIndex) { return { QuizIntent: function () { // 初期状態 - const session = new Session({env}); - session.start(); + const session = startSession({env}); this.attributes['dump'] = session.dump(); this.emit(session.command(), session.message(), session.reprompt()); }, AnswerIntent: function () { // ユーザからの返答により起動される const usersAnswer = this.event.request.intent.slots.Answer.value; - const session = new Session({env}); - session.restore(this.attributes['dump']); + const session = restoreSession({env, dump: this.attributes['dump']}); session.receive(usersAnswer); this.attributes['dump'] = session.dump(); ৘ใӅṭ͕ਐΉ

Slide 101

Slide 101 text

࣮૷ৄࡉͱͯ͠ͷ"MFYB-BNCEBͱϏδωεϩδοΫΛ෼཭Ͱ͖ͨ QuizIntent: function () { // 初期状態 const session = startSession({env}); this.attributes['dump'] = session.dump(); this.emit(session.command(), session.message(), session.reprompt()); }, AnswerIntent: function () { // ユーザからの返答により起動される const usersAnswer = this.event.request.intent.slots.Answer.value; const session = restoreSession({env, dump: this.attributes['dump']}); session.receive(usersAnswer); this.attributes['dump'] = session.dump(); this.emit(session.command(), session.message(), session.reprompt()); }, 👍

Slide 102

Slide 102 text

ͻͲ͔ͬͨͱ͖ͱൺ΂ͯΈΔ QuizIntent: function () { // 初期状態 this.attributes['advance'] = 1; // 進行状況を初期化 this.attributes['score'] = 0; // 得点を初期化 this.attributes['accumIncorrects'] = 0; // 連続不正解数を初期化 var random = getNextItemIndex(); this.attributes['itemIndex'] = random; // 配列番号をitemIndexに保存 var message = `簡単なクイズをしましょう。1番。 ${questions[random].q}`; var reprompt = `1番。 ${questions[random].q}`; this.emit(':ask', message, reprompt); // 相手の回答を待つ }, AnswerIntent: function () { // ユーザからの返答により起動される // スロットから回答を取得 var usersAnswer = this.event.request.intent.slots.Answer.value; var currentQuestion = questions[this.attributes['itemIndex']]; var shouldRepeatSameQuestion = false; var resultMessage; if (currentQuestion.a === usersAnswer) { // 正解の場合 resultMessage = 'そうです。 では'; this.attributes['score']++; this.attributes['accumIncorrects'] = 0; } else { // 不正解の場合 this.attributes['accumIncorrects']++; switch (this.attributes['accumIncorrects']) { case 1: resultMessage = `${usersAnswer}? もう一度言ってください。${currentQuestion.q}`; shouldRepeatSameQuestion = true; break; case 2: resultMessage = `私には「${usersAnswer}」と聞こえましたが、それは正しくありません。もう一度言ってください。${currentQuestion.q}`; shouldRepeatSameQuestion = true; break; default: resultMessage = `ちがいます。正解は${currentQuestion.a}です。 では`; this.attributes['accumIncorrects'] = 0; break; } } if (shouldRepeatSameQuestion) { var reprompt = `${this.attributes['advance']}番。 ${currentQuestion.q}`; this.emit(':ask', resultMessage, reprompt); } else if (this.attributes['advance'] < 7) { // 続きの問題がある場合 this.attributes['advance']++; var random = getNextItemIndex(); this.attributes['itemIndex'] = random; var reprompt = `${this.attributes['advance']}番。 ${questions[random].q}`; this.emit(':ask', resultMessage + reprompt, reprompt); // 会話を続ける } else { // 全ての問題が終了した場合 var endMessage = `終わりです。あなたは${this.attributes['score']}点でした。`; this.emit(':tell', resultMessage + endMessage); // 会話を終える } }, ͜ɺ͜Ε͸ʜʜ😨

Slide 103

Slide 103 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 103/118

Slide 104

Slide 104 text

͜͜Ͱ࢓༷௥ՃͰ͢ w ऴྃͨ͠ϒϩοΫͷޡ౴܏޲Λݟͯ࣍ͷϒϩοΫͷ࣭໰άϧʔϓΛܾΊ͍ͨ w ޡ౴܏޲ͰۤखάϧʔϓΛ൑ఆ͠ɺΫΠζऴྃ࣌ʹϝοηʔδʹ൓ө͍ͨ͠ w ޡ౴਺ͰϨϕϧ൑ఆ͍ͨ͠ w ฏۉճ౴ॴཁ࣌ؒͰϨϕϧ൑ఆ͍ͨ͠ w ޡ౴਺ɺฏۉճ౴ॴཁ࣌ؒΛڞʹධՁ͠ɺ௿͍ํͷϨϕϧΛ࠾༻͍ͨ͠ υυοͱདྷ·ͨ͠ͳ &&Ͱ͸ςετ͠ʹ͍͘ཁ݅ͳͷͰɺ͜͜·ͰͰ ϢχοτςετՄೳͳͭ͘Γʹ͓͍ͯͯ͠Α͔ͬͨ

Slide 105

Slide 105 text

৘ใͱσʔλͷҧ͍Λҙࣝ͢Δ IUUQTXXXPSFJMMZDPKQCPPLT ͯ͞ɺօ͞Μ͸ʮ৘ใʯͱʮσʔλʯͷҧ͍Λ͝ଘ஌Ͱ͠ΐ͏͔ɻ զʑ͕ཉ͍͠ͷ͸ɺҙຯͷ͋Δʢ໨తΛ࣋ͬͨʣਖ਼͍͠৘ใͳͷͰ͢ɻҰํɺσʔλ ͸୯ͳΔ֤छͷࣄ࣮ͷ஋ʢԿΒ͔ͷɺ໊শͱ͔೔෇ͱֹ͔ۚͱ͔ʣͰ͋ͬͯͦΕࣗମ ʹ໨త͸͋Γ·ͤΜɻ ໨తΛ࣋ͬͨ৘ใ͸ɺແ໨తͳࣄ࣮Λूੵͨ͠σʔλΛछʑՃ޻ͯ͠ಘΒΕ·͢ɻ σʔλ͸།Ұແೋͷࣄ࣮஋Ͱ͔͢ΒɺͦΕ͔Β࡞Γग़͞ΕΔ৘ใ͸ͲΕ΋͕ਖ਼͘͠ɺ ޓ͍ʹ੔߹ੑ͕ͱΕ͍ͯ·͢ɻ ᴷ࿨ాলೋ ʰ42-Ξϯνύλʔϯʱ؂༁ऀલॻ͖

Slide 106

Slide 106 text

͜Ε͸ࣄ࣮ͩΖ͏͔ɺ৘ใͩΖ͏͔ɺͦΕΒ͕͍ࠞͬͯ͟ΔͩΖ͏͔ dump: { advance: 7, score: 5, accumIncorrects: 0, item: { a: '盛岡', g: 'PrefecturalOfficeLocation', q: '岩手県の県庁所在地は?' } }

Slide 107

Slide 107 text

ࣄ࣮ΛϞσϦϯά͠ɺߏ଄ʹ൓ө͢Δ Block Session Question Attempt Attempt Attempt Attempt Attempt Attempt Attempt Attempt Attempt Question Question Block Block

Slide 108

Slide 108 text

"dump": { "startedAt": 1540553739000, "finishedAt": null, "blocks": [ { "startedAt": 1540553739000, "finishedAt": 1540553825000, "genres": [ "PrefectureFlower", "PrefecturalOfficeLocation", "Romanization" ], "questions": [ { "item": { "q": "山形県の県の花は?", "a": "べにばな", "g": "PrefectureFlower" }, "startedAt": 1540553752000, "finishedAt": 1540553778000, "result": "correct", "attempts": [ { "startedAt": 1540553752000, "finishedAt": 1540553767000, "userAnswer": "サクラ", "result": "incorrect" }, { "startedAt": 1540553767000, "finishedAt": 1540553778000, "userAnswer": "べにばな", "result": "correct" } ࣄ࣮Λ֨ೲ͠ɺ৘ใ͸͔ͦ͜Βܭࢉ͢Δํ͕҆ఆ͢Δ w ޡ౴܏޲ͰۤखάϧʔϓΛ൑ఆ͍ͨ͠ w ޡ౴਺ͰϨϕϧ൑ఆ͍ͨ͠ w ฏۉճ౴ॴཁ࣌ؒͰϨϕϧ൑ఆ͍ͨ͠ ޡ౴܏޲ɺޡ౴਺ɺฏۉճ౴ॴཁ࣌ؒ͸ɺ ͢΂ͯʮࣄ࣮ʢσʔλʣʯ͔Βࢉग़Ͱ͖Δʮ৘ใʯ

Slide 109

Slide 109 text

Agenda  ݱঢ়֬ೝ  ಆ͏४උΛ੔͑Δ  ػೳ௥Ճͱͦͷ݁Ռ  ϞσϧΛ෼཭͢Δ  ࣄ࣮ͱ৘ใΛ෼͚Δ  ΞʔΩςΫνϟΛఆΊΔ 👉 109/118

Slide 110

Slide 110 text

͜͜Ͱ͞ΒͳΔཁ๬Ͱ͢ ਖ਼௚དྷΔͱࢥ͍ͬͯͨ͜Ε͸ w "ࣾͷεϚʔτεϐʔΧʔ͚ͩͰͳ͘ɺ(ࣾͷεϚʔτεϐʔ ΧʔͰ΋ݕূΛߦ͍͍ͨ

Slide 111

Slide 111 text

ઃܭͷݪଇ IUUQTXXXBNB[PODPKQEQ w ୯Ұ੹೚ͷݪଇʢΫϥεϨϕϧʣɺด࠯ੑڞ௨ͷݪଇʢίϯϙʔωϯτϨϕϧʣ w ಉ͡λΠϛϯάɺಉ͡ཧ༝Ͱมߋ͢Δ΋ͷ͸ͻͱ·ͱΊʹ͢Δ͜ͱɻมߋͷ λΠϛϯά΍ཧ༝͕ҟͳΔ΋ͷ͸ผʑʹ෼͚Δ͜ͱ w ҆ఆґଘͷݪଇ w ҆ఆ౓ͷߴ͍ํ޲ʹґଘ͢Δ͜ͱ

Slide 112

Slide 112 text

ॻ੶ʰ$MFBO"SDIJUFDUVSFʱʹग़ͯ͘Δ ʢޡղ͞Ε͕ͪͳʣಉ৺ԁΛྫʹͯ͠ߟ͑ͯΈΔ

Slide 113

Slide 113 text

֤ࣾεϚʔτεϐʔΧʔ޲͚ͷ࣮૷͸ ʮৄࡉʯͩͱ͍͑Δ

Slide 114

Slide 114 text

ΫΠζͷ ίΞυϝΠϯ ֤εϚʔτεϐʔΧʔ ͱͷΞμϓλ૚ ΫΠζͷίΞυϝΠϯ͸ εϚʔτεϐʔΧʔͷৄࡉʹඇґଘ Ξμϓλ૚͸֤ࣾεϚʔτεϐʔΧʔͱ ίΞυϝΠϯͱͷ઀ଓʹͱͲΊΔ

Slide 115

Slide 115 text

事実を扱う 情報を扱う 基盤を扱う 永続化を扱う ࣄ࣮͔ΒܭࢉͰ͖Δಘ఺౳΍ 76*ͱͯ͠ͷϝοηʔδͳͲΛѻ͏ ࠷ऴతͳΞʔΩςΫνϟ

Slide 116

Slide 116 text

͓ΘΓʹ

Slide 117

Slide 117 text

IUUQTXXXPINTIBDPKQCPPL 5%%͸ɺઃܭͷͻΒΊ͖͕ਖ਼͍͠ॠؒʹ๚ΕΔ͜ͱΛอূ͢Δ΋ͷͰ͸ͳ͍ɻ ͔͠͠ɺࣗ৴Λ༩͑ͯ͘ΕΔςετͱ͖ͪΜͱखೖΕ͞Εͨίʔυ͸ɺͻΒΊ ͖΁ͷඋ͑Ͱ͋Γɺ͍͟ͻΒΊ͍ͨͱ͖ʹɺͦΕΛ۩ݱԽ͢ΔͨΊͷඋ͑Ͱ΋ ͋Δɻ ᴷ,FOU#FDL ʰςετۦಈ։ൃʱୈষ ,FOU#FDLᐌ͘

Slide 118

Slide 118 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ w ٕज़ͷຊபʢόʔδϣϯ؅ཧɺςεςΟϯάɺࣗಈԽʣʹ͸༏ઌ౓͕͋Δ w ࣗಈςετΛॻ͍͚ͨͩͰ͸࣭͸্͕Βͳ͍ɻ࣭Λ্͛Δͷ͸ϓϩάϥϛϯά w ʢՄೳͰ͋Ε͹ʣϦϑΝΫλϦϯά΁ͷ଱ੑΛ࣋ͪͭͭ҆ఆͯ͠ςετͰ͖ΔϙΠϯτΛ୳͠ɺͦΕΛલઢج஍ͱ͢Δ w طଘίʔυΛςετͰे෼อޢͰ͖Δ͔Ͳ͏͔Ͱ&YUSBDUઓज़͔4QSPVUઓज़͔ΛܾΊΔ w ࢓༷͕ݻ·Βͳ͍͔Βςετ͕ॻ͚ͳ͍ͷͰ͸ͳ͘ɺ࢓༷͕ݻ·Βͳ͍͔Βͦ͜ςετΛॻ͍ͯมԽΛࢧ͍͑ͯ͘ w ࣗಈςετΛॻ͖ͳ͕ΒυϝΠϯϞσϧΛநग़͢Δ w ίΞυϝΠϯΛಠཱͯࣗ͠ಈςετͰ͖ΔΑ͏ʹɺٕज़తͳৄࡉͱͷ݁߹౓Λஈ֊తʹݮΒ͍ͯ͘͠ w ࣄ࣮ΛϞσϦϯά͠ɺ৘ใΛ͔ͦ͜ΒऔΓग़͢ w ίΞυϝΠϯͱٕज़ৄࡉΛ෼཭͠ɺίΞυϝΠϯΛҭ͍͚ͯͯΔΞʔΩςΫνϟΛఆΊΔ w ʮΫϦʔϯΞʔΩςΫνϟΈ͍ͨͳ΍ͭʯ͸࠷ॳ͔Β໨ࢦ͢ͷͰ͸ͳͯ͘ɺݪཧݪଇϕʔεͰϦϑΝΫλϦϯά͍ͯ͘͠ͱ࣍ ୈʹ͍͍ۙͮͯ͘΋ͷ

Slide 119

Slide 119 text

ࢀߟจݙ