Skip to content

Commit 76718fd

Browse files
committed
Implement sensing_askandwait block
1 parent 71ff44a commit 76718fd

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

src/blocks/sensingblocks.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void SensingBlocks::registerBlocks(IEngine *engine)
2828
{
2929
// Blocks
3030
engine->addCompileFunction(this, "sensing_distanceto", &compileDistanceTo);
31+
engine->addCompileFunction(this, "sensing_askandwait", &compileAskAndWait);
3132
engine->addCompileFunction(this, "sensing_keypressed", &compileKeyPressed);
3233
engine->addCompileFunction(this, "sensing_mousedown", &compileMouseDown);
3334
engine->addCompileFunction(this, "sensing_mousex", &compileMouseX);
@@ -77,6 +78,9 @@ void SensingBlocks::registerBlocks(IEngine *engine)
7778
engine->addFieldValue(this, "background #", BackdropNumber); // Scratch 1.4 support
7879
engine->addFieldValue(this, "backdrop #", BackdropNumber);
7980
engine->addFieldValue(this, "backdrop name", BackdropName);
81+
82+
// Callbacks
83+
engine->setQuestionAnswered(&onAnswer);
8084
}
8185

8286
void SensingBlocks::compileDistanceTo(Compiler *compiler)
@@ -100,6 +104,12 @@ void SensingBlocks::compileDistanceTo(Compiler *compiler)
100104
}
101105
}
102106

107+
void SensingBlocks::compileAskAndWait(Compiler *compiler)
108+
{
109+
compiler->addInput(QUESTION);
110+
compiler->addFunctionCall(&askAndWait);
111+
}
112+
103113
void SensingBlocks::compileKeyPressed(Compiler *compiler)
104114
{
105115
compiler->addInput(KEY_OPTION);
@@ -488,6 +498,39 @@ unsigned int SensingBlocks::distanceToMousePointer(VirtualMachine *vm)
488498
return 0;
489499
}
490500

501+
void SensingBlocks::onAnswer(const std::string &answer)
502+
{
503+
// https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L99-L115
504+
m_answer = answer;
505+
506+
if (!m_questionList.empty()) {
507+
Question *question = m_questionList.front().get();
508+
VirtualMachine *vm = question->vm;
509+
assert(vm);
510+
assert(vm->target());
511+
512+
// If the target was visible when asked, hide the say bubble unless the target was the stage
513+
if (question->wasVisible && !question->wasStage)
514+
vm->target()->setBubbleText("");
515+
516+
m_questionList.erase(m_questionList.begin());
517+
vm->resolvePromise();
518+
askNextQuestion();
519+
}
520+
}
521+
522+
unsigned int SensingBlocks::askAndWait(VirtualMachine *vm)
523+
{
524+
const bool isQuestionAsked = !m_questionList.empty();
525+
enqueueAsk(vm->getInput(0, 1)->toString(), vm);
526+
527+
if (!isQuestionAsked)
528+
askNextQuestion();
529+
530+
vm->promise();
531+
return 1;
532+
}
533+
491534
unsigned int SensingBlocks::timer(VirtualMachine *vm)
492535
{
493536
vm->addReturnValue(vm->engine()->timer()->value());
@@ -822,3 +865,44 @@ unsigned int SensingBlocks::daysSince2000(VirtualMachine *vm)
822865

823866
return 0;
824867
}
868+
869+
void SensingBlocks::enqueueAsk(const std::string &question, VirtualMachine *vm)
870+
{
871+
// https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L117-L119
872+
assert(vm);
873+
Target *target = vm->target();
874+
assert(target);
875+
bool visible = true;
876+
bool isStage = target->isStage();
877+
878+
if (!isStage) {
879+
Sprite *sprite = static_cast<Sprite *>(target);
880+
visible = sprite->visible();
881+
}
882+
883+
m_questionList.push_back(std::make_unique<Question>(question, vm, visible, isStage));
884+
}
885+
886+
void SensingBlocks::askNextQuestion()
887+
{
888+
// https://github.com/scratchfoundation/scratch-vm/blob/6055823f203a696165084b873e661713806583ec/src/blocks/scratch3_sensing.js#L121-L133
889+
if (m_questionList.empty())
890+
return;
891+
892+
Question *question = m_questionList.front().get();
893+
Target *target = question->vm->target();
894+
auto ask = question->vm->engine()->questionAsked();
895+
896+
// If the target is visible, emit a blank question and show
897+
// a bubble unless the target was the stage
898+
if (question->wasVisible && !question->wasStage) {
899+
target->setBubbleType(Target::BubbleType::Say);
900+
target->setBubbleText(question->question);
901+
902+
if (ask)
903+
ask("");
904+
} else {
905+
if (ask)
906+
ask(question->question);
907+
}
908+
}

src/blocks/sensingblocks.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@
33
#pragma once
44

55
#include <scratchcpp/iblocksection.h>
6+
#include <scratchcpp/value.h>
7+
#include <vector>
8+
69
#include "../engine/internal/clock.h"
710

811
namespace libscratchcpp
912
{
1013

14+
class Target;
15+
1116
/*! \brief The SensingBlocks class contains the implementation of sensing blocks. */
1217
class SensingBlocks : public IBlockSection
1318
{
1419
public:
1520
enum Inputs
1621
{
1722
DISTANCETOMENU,
23+
QUESTION,
1824
KEY_OPTION,
1925
OBJECT
2026
};
@@ -53,6 +59,7 @@ class SensingBlocks : public IBlockSection
5359
void registerBlocks(IEngine *engine) override;
5460

5561
static void compileDistanceTo(Compiler *compiler);
62+
static void compileAskAndWait(Compiler *compiler);
5663
static void compileKeyPressed(Compiler *compiler);
5764
static void compileMouseDown(Compiler *compiler);
5865
static void compileMouseX(Compiler *compiler);
@@ -83,6 +90,9 @@ class SensingBlocks : public IBlockSection
8390
static unsigned int distanceToByIndex(VirtualMachine *vm);
8491
static unsigned int distanceToMousePointer(VirtualMachine *vm);
8592

93+
static void onAnswer(const std::string &answer);
94+
static unsigned int askAndWait(VirtualMachine *vm);
95+
8696
static unsigned int timer(VirtualMachine *vm);
8797
static unsigned int resetTimer(VirtualMachine *vm);
8898

@@ -116,6 +126,29 @@ class SensingBlocks : public IBlockSection
116126
static unsigned int daysSince2000(VirtualMachine *vm);
117127

118128
static IClock *clock;
129+
130+
private:
131+
struct Question
132+
{
133+
Question(const std::string &question, VirtualMachine *vm, bool wasVisible, bool wasStage) :
134+
question(question),
135+
vm(vm),
136+
wasVisible(wasVisible),
137+
wasStage(wasStage)
138+
{
139+
}
140+
141+
std::string question;
142+
VirtualMachine *vm = nullptr;
143+
bool wasVisible = false;
144+
bool wasStage = false;
145+
};
146+
147+
static void enqueueAsk(const std::string &question, VirtualMachine *vm);
148+
static void askNextQuestion();
149+
150+
static inline std::vector<std::unique_ptr<Question>> m_questionList;
151+
static inline Value m_answer;
119152
};
120153

121154
} // namespace libscratchcpp

test/blocks/sensing_blocks_test.cpp

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
using namespace libscratchcpp;
1919

2020
using ::testing::Return;
21+
using ::testing::ReturnRef;
22+
using ::testing::SaveArg;
23+
using ::testing::_;
2124

2225
class SensingBlocksTest : public testing::Test
2326
{
@@ -92,6 +95,19 @@ class SensingBlocksTest : public testing::Test
9295
ClockMock m_clockMock;
9396
};
9497

98+
struct QuestionSpy
99+
{
100+
MOCK_METHOD(void, asked, (const std::string &), ());
101+
};
102+
103+
template<typename T, typename... U>
104+
size_t getAddress(std::function<T(U...)> f)
105+
{
106+
typedef T(fnType)(U...);
107+
fnType **fnPointer = f.template target<fnType *>();
108+
return (size_t)*fnPointer;
109+
}
110+
95111
TEST_F(SensingBlocksTest, Name)
96112
{
97113
ASSERT_EQ(m_section->name(), "Sensing");
@@ -106,6 +122,7 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
106122
{
107123
// Blocks
108124
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_distanceto", &SensingBlocks::compileDistanceTo));
125+
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_askandwait", &SensingBlocks::compileAskAndWait));
109126
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_keypressed", &SensingBlocks::compileKeyPressed));
110127
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousedown", &SensingBlocks::compileMouseDown));
111128
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "sensing_mousex", &SensingBlocks::compileMouseX));
@@ -156,7 +173,14 @@ TEST_F(SensingBlocksTest, RegisterBlocks)
156173
EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop #", SensingBlocks::BackdropNumber));
157174
EXPECT_CALL(m_engineMock, addFieldValue(m_section.get(), "backdrop name", SensingBlocks::BackdropName));
158175

176+
// Callbacks
177+
std::function<void(const std::string &)> questionAnsweredRef = &SensingBlocks::onAnswer;
178+
std::function<void(const std::string &)> questionAnswered;
179+
EXPECT_CALL(m_engineMock, setQuestionAnswered(_)).WillOnce(SaveArg<0>(&questionAnswered));
180+
159181
m_section->registerBlocks(&m_engineMock);
182+
ASSERT_TRUE(questionAnswered);
183+
ASSERT_EQ(getAddress(questionAnsweredRef), getAddress(questionAnswered));
160184
}
161185

162186
TEST_F(SensingBlocksTest, DistanceTo)
@@ -290,6 +314,122 @@ TEST_F(SensingBlocksTest, DistanceToImpl)
290314
ASSERT_EQ(std::round(vm.getInput(0, 1)->toDouble() * 10000) / 10000, 261.0096);
291315
}
292316

317+
TEST_F(SensingBlocksTest, AskAndWait)
318+
{
319+
Compiler compiler(&m_engineMock);
320+
321+
// ask "test" and wait
322+
auto block1 = std::make_shared<Block>("a", "sensing_askandwait");
323+
addDropdownInput(block1, "QUESTION", SensingBlocks::QUESTION, "test");
324+
325+
// ask (null block) and wait
326+
auto block2 = std::make_shared<Block>("b", "sensing_askandwait");
327+
addDropdownInput(block2, "QUESTION", SensingBlocks::QUESTION, "", createNullBlock("c"));
328+
329+
compiler.init();
330+
331+
EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::askAndWait)).WillOnce(Return(0));
332+
compiler.setBlock(block1);
333+
SensingBlocks::compileAskAndWait(&compiler);
334+
335+
EXPECT_CALL(m_engineMock, functionIndex(&SensingBlocks::askAndWait)).WillOnce(Return(0));
336+
compiler.setBlock(block2);
337+
SensingBlocks::compileAskAndWait(&compiler);
338+
339+
compiler.end();
340+
341+
ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_NULL, vm::OP_EXEC, 0, vm::OP_HALT }));
342+
ASSERT_EQ(compiler.constValues().size(), 1);
343+
ASSERT_EQ(compiler.constValues()[0].toString(), "test");
344+
}
345+
346+
TEST_F(SensingBlocksTest, AskAndWaitImpl)
347+
{
348+
static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT };
349+
static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT };
350+
static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT };
351+
static BlockFunc functions[] = { &SensingBlocks::askAndWait };
352+
static Value constValues[] = { "test1", "test2", "test3" };
353+
354+
Sprite sprite;
355+
sprite.setBubbleType(Target::BubbleType::Think);
356+
Stage stage;
357+
QuestionSpy spy;
358+
std::function<void(const std::string &)> asked = std::bind(&QuestionSpy::asked, &spy, std::placeholders::_1);
359+
360+
VirtualMachine vm1(&sprite, &m_engineMock, nullptr);
361+
vm1.setFunctions(functions);
362+
vm1.setConstValues(constValues);
363+
364+
// Ask 3 questions (2 where the sprite is visible and 1 where it's invisible)
365+
EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
366+
EXPECT_CALL(spy, asked(""));
367+
sprite.setVisible(true);
368+
vm1.setBytecode(bytecode1);
369+
vm1.run();
370+
371+
ASSERT_EQ(vm1.registerCount(), 0);
372+
ASSERT_FALSE(vm1.atEnd());
373+
ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
374+
ASSERT_EQ(sprite.bubbleText(), "test1");
375+
376+
EXPECT_CALL(m_engineMock, questionAsked).Times(0);
377+
vm1.reset();
378+
vm1.setBytecode(bytecode2);
379+
vm1.run();
380+
381+
ASSERT_EQ(vm1.registerCount(), 0);
382+
ASSERT_FALSE(vm1.atEnd());
383+
ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
384+
ASSERT_EQ(sprite.bubbleText(), "test1");
385+
386+
EXPECT_CALL(m_engineMock, questionAsked).Times(0);
387+
sprite.setVisible(false);
388+
vm1.reset();
389+
vm1.setBytecode(bytecode3);
390+
vm1.run();
391+
sprite.setVisible(true);
392+
393+
ASSERT_EQ(vm1.registerCount(), 0);
394+
ASSERT_FALSE(vm1.atEnd());
395+
ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
396+
ASSERT_EQ(sprite.bubbleText(), "test1");
397+
398+
// Ask a question from the stage
399+
VirtualMachine vm2(&stage, &m_engineMock, nullptr);
400+
vm2.setFunctions(functions);
401+
vm2.setConstValues(constValues);
402+
403+
EXPECT_CALL(m_engineMock, questionAsked).Times(0);
404+
vm2.setBytecode(bytecode2);
405+
vm2.run();
406+
ASSERT_EQ(vm2.registerCount(), 0);
407+
ASSERT_FALSE(vm2.atEnd());
408+
ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
409+
ASSERT_EQ(sprite.bubbleText(), "test1");
410+
411+
// Answer the questions
412+
EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
413+
EXPECT_CALL(spy, asked(""));
414+
SensingBlocks::onAnswer("hi");
415+
ASSERT_EQ(sprite.bubbleType(), Target::BubbleType::Say);
416+
ASSERT_EQ(sprite.bubbleText(), "test2");
417+
418+
EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
419+
EXPECT_CALL(spy, asked("test3"));
420+
SensingBlocks::onAnswer("hello");
421+
ASSERT_TRUE(sprite.bubbleText().empty());
422+
423+
EXPECT_CALL(m_engineMock, questionAsked()).WillOnce(ReturnRef(asked));
424+
EXPECT_CALL(spy, asked("test2"));
425+
SensingBlocks::onAnswer("world");
426+
ASSERT_TRUE(sprite.bubbleText().empty());
427+
ASSERT_TRUE(stage.bubbleText().empty());
428+
429+
EXPECT_CALL(m_engineMock, questionAsked).Times(0);
430+
SensingBlocks::onAnswer("test");
431+
}
432+
293433
TEST_F(SensingBlocksTest, KeyPressed)
294434
{
295435
Compiler compiler(&m_engineMock);

0 commit comments

Comments
 (0)