Skip to content

Commit cb25360

Browse files
Merge pull request #334 from Program-AR/showScenarios
Boton multiples escenarios
2 parents 48dff23 + 406f3d6 commit cb25360

9 files changed

Lines changed: 185 additions & 90 deletions

File tree

locales/en-us/challenge.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
"label": "RESTART",
2222
"tooltip": "Restart execution"
2323
},
24+
"showScenarios": {
25+
"label": "CHANGE SCENARIO",
26+
"tooltip": "Show another scenario"
27+
},
2428
"multipleScenarios": "There are multiple scenarios!",
2529
"close": "Close",
2630
"solutionButtons": {

locales/es-ar/challenge.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
"label": "REINICIAR",
2222
"tooltip": "Reiniciar la ejecución"
2323
},
24+
"showScenarios": {
25+
"label": "CAMBIAR ESCENARIO",
26+
"tooltip": "Mostrar otro escenario"
27+
},
2428
"multipleScenarios": "¡Hay varios escenarios!",
2529
"close": "Cerrar",
2630
"solutionButtons": {

locales/pt-br/challenge.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
"label": "REINICIAR",
2222
"tooltip": "Reiniciar execução"
2323
},
24+
"showScenarios": {
25+
"label": "TROCAR CENÁRIO",
26+
"tooltip": "Mostrar outro cenário"
27+
},
2428
"multipleScenarios": "Existem vários cenários!",
2529
"close": "Fechar",
2630
"solutionButtons": {

src/components/challengeView/ChallengeView.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,12 @@ type EditableBlocklyWorkspaceProps = {
120120
const HorizontalChallengeWorkspace = ({ challenge, blocklyWorkspaceProps }: ChallengeWorkspaceDistributionProps) => {
121121
const blocklyWorkspace = useMemo<JSX.Element>(() => {
122122
return <EditableBlocklyWorkspace blockIds={blocklyWorkspaceProps.blockIds} categorized={blocklyWorkspaceProps.categorized} initialXml={blocklyWorkspaceProps.initialXml} isVertical={false} zoomScale={1.0} />
123+
// eslint-disable-next-line react-hooks/exhaustive-deps
123124
}, [])
124125

125126
return <Stack direction="row" >
126127
<Stack direction="row" position="relative" flexWrap={"wrap"} flexGrow={1}>
127-
<SolutionButtons/>
128+
<SolutionButtons />
128129
{blocklyWorkspace}
129130
</Stack>
130131
<Stack>
@@ -140,6 +141,7 @@ const VerticalChallengeWorkspace = ({ challenge, blocklyWorkspaceProps }: Challe
140141

141142
const blocklyWorkspace = useMemo<JSX.Element>(() => {
142143
return <EditableBlocklyWorkspace blockIds={blocklyWorkspaceProps.blockIds} categorized={blocklyWorkspaceProps.categorized} initialXml={blocklyWorkspaceProps.initialXml} isVertical={true} zoomScale={0.7} />
144+
// eslint-disable-next-line react-hooks/exhaustive-deps
143145
}, [])
144146

145147
return <Stack flexWrap={"wrap"} flexGrow={1} >

src/components/challengeView/SceneButtons/Execute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const ExecuteButton = ({ challenge }: ExecuteButtonProps) => {
6666
</Stack>
6767
</IconButton >
6868
:
69-
<Button className={styles['scene-button']} startIcon={<PlayArrow/>} variant="contained" color="success" onClick={handleExcecute} data-testid='execute-button' data-finishedexecution={finishedExecution}>{t("run.label")} </Button>
69+
<Button className={styles['scene-button']} startIcon={<PlayArrow />} variant="contained" color="success" onClick={handleExcecute} data-testid='execute-button' data-finishedexecution={finishedExecution}>{t("run.label")} </Button>
7070
}
7171
</Tooltip>
7272
<EndDialog showModal={showModal} setShowModal={setShowModal} />
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Button, IconButton, Stack, Tooltip } from "@mui/material"
2+
import { scene } from "../scene"
3+
import { useThemeContext } from "../../../theme/ThemeContext"
4+
import styles from './sceneButtons.module.css'
5+
import { Circle, SwapHorizOutlined } from "@mui/icons-material"
6+
import { Challenge } from "../../../staticData/challenges"
7+
import { useTranslation } from "react-i18next"
8+
import { useEffect, useState } from "react"
9+
10+
type MultipleScenariosButtonProps = {
11+
challenge: Challenge
12+
}
13+
14+
export const MultipleScenariosButton = ({ challenge }: MultipleScenariosButtonProps) => {
15+
16+
const { isSmallScreen, theme } = useThemeContext()
17+
const { t } = useTranslation('challenge')
18+
const [currentScene, setCurrentScene] = useState<string | null>(null)
19+
20+
21+
useEffect(() => {
22+
const initScene = async () => {
23+
await scene.waitUntilReady()
24+
const initialScene = scene.currentScene()
25+
setCurrentScene(initialScene)
26+
}
27+
initScene()
28+
}, [])
29+
30+
const handleShowScenarios = async () => {
31+
if (currentScene == null) {
32+
await scene.restartScene(challenge.sceneDescriptor)
33+
setCurrentScene(scene.currentScene())
34+
return
35+
}
36+
37+
let attempts = 0
38+
const maxAttempts = 10
39+
let newScene
40+
41+
do {
42+
await scene.restartScene(challenge.sceneDescriptor)
43+
newScene = scene.currentScene()
44+
attempts++
45+
} while (currentScene.toString() === newScene.toString() && attempts < maxAttempts)
46+
47+
if (attempts === maxAttempts) {
48+
console.warn('No se pudo obtener una escena diferente luego de varios intentos')
49+
} else {
50+
setCurrentScene(newScene)
51+
}
52+
}
53+
54+
return <Tooltip title={t('showScenarios.tooltip')}>
55+
{isSmallScreen ?
56+
<IconButton className={styles['icon-button']} style={{backgroundColor: theme.palette.secondary.main}} onClick={handleShowScenarios} data-testid='showScenarios-button' data-finishedexecution={false}>
57+
<Stack>
58+
<Circle className={styles['circle-icon']} style={{color: theme.palette.secondary.main}} />
59+
<SwapHorizOutlined className={styles['icon']}/>
60+
</Stack>
61+
</IconButton >
62+
:
63+
<Button className={styles['scene-button']} startIcon={<SwapHorizOutlined />} variant="contained" style={{backgroundColor: theme.palette.secondary.main}} onClick={handleShowScenarios} data-testid='showScenarios-button' data-finishedexecution={false}>{t("showScenarios.label")} </Button>
64+
}
65+
</Tooltip>
66+
}

src/components/challengeView/SceneButtons/SceneButtons.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useThemeContext } from "../../../theme/ThemeContext";
88
import { ExecuteButton } from "./Execute"
99
import { Challenge } from "../../../staticData/challenges"
1010
import { useTranslation } from "react-i18next"
11+
import { MultipleScenariosButton } from "./MultipleScenarios"
1112

1213
type SceneButtonsProps = {
1314
challenge: Challenge
@@ -20,6 +21,7 @@ export const SceneButtons = ({ challenge }: SceneButtonsProps) => {
2021
<Stack direction='row' justifyContent='flex-start' flexGrow={2} spacing={2} marginRight='7px'>
2122
{shouldShow && <NextStepButton />}
2223
<ExecuteButton challenge={challenge} />
24+
{ challenge.shouldShowMultipleScenarioHelp && <MultipleScenariosButton challenge={challenge} /> }
2325
</Stack>
2426
{shouldShow && <TurboModeSwitch />}
2527
</PBCard>
@@ -29,6 +31,7 @@ export const SceneButtonsVertical = ({ challenge }: SceneButtonsProps) => {
2931
return <Stack gap={2} alignItems='center'>
3032
{shouldShow && <NextStepButton />}
3133
<ExecuteButton challenge={challenge} />
34+
{ challenge.shouldShowMultipleScenarioHelp && <MultipleScenariosButton challenge={challenge} /> }
3235
{shouldShow && <TurboModeSwitch />}
3336
</Stack>
3437
}

src/components/challengeView/scene.ts

Lines changed: 100 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,42 @@ import { Challenge } from "../../staticData/challenges";
33
import { Actor, Behaviour } from "./SceneButtons/interpreterFactory";
44

55
class Scene {
6-
iframe(): HTMLIFrameElement {
7-
return document.getElementById("sceneIframe") as HTMLIFrameElement
8-
}
9-
10-
/**
11-
* Instantiates and runs pilas-engine (pilasweb) main framework.
12-
* Preloads needed challenge's images.
13-
* Sets up messaging events from iframe. Iframe should exist before calling this method.
14-
* @param descriptor The scene descriptor
15-
*/
16-
async load(descriptor: Challenge["sceneDescriptor"]) {
17-
await this.initializePilasWeb(descriptor)
18-
this.setChallenge(descriptor)
19-
}
6+
private isReady = false;
207

21-
setChallenge(descriptor: Challenge["sceneDescriptor"]) {
22-
const initializer = descriptor === this.sceneName(descriptor) ? `new ${descriptor}()` : descriptor
23-
this.eval(`pilas.mundo.gestor_escenas.cambiar_escena(${initializer})`)
8+
async waitUntilReady(): Promise<void> {
9+
while (!this.isReady) {
10+
await new Promise(res => setTimeout(res, 50)) // espera activa
2411
}
25-
26-
initializePilasWeb(descriptor: string) {
27-
return new Promise<void>((resolve) => {
28-
const pilasweb = this.eval(`
12+
}
13+
14+
currentScene(): Challenge["sceneDescriptor"] {
15+
return this.isReady ? this.eval('pilas.mundo.gestor_escenas.escena.mapaEscena') : ''
16+
}
17+
18+
iframe(): HTMLIFrameElement {
19+
return document.getElementById("sceneIframe") as HTMLIFrameElement
20+
}
21+
22+
/**
23+
* Instantiates and runs pilas-engine (pilasweb) main framework.
24+
* Preloads needed challenge's images.
25+
* Sets up messaging events from iframe. Iframe should exist before calling this method.
26+
* @param descriptor The scene descriptor
27+
*/
28+
async load(descriptor: Challenge["sceneDescriptor"]) {
29+
await this.initializePilasWeb(descriptor)
30+
this.isReady = true
31+
this.setChallenge(descriptor)
32+
}
33+
34+
setChallenge(descriptor: Challenge["sceneDescriptor"]) {
35+
const initializer = descriptor === this.sceneName(descriptor) ? `new ${descriptor}()` : descriptor
36+
this.eval(`pilas.mundo.gestor_escenas.cambiar_escena(${initializer})`)
37+
}
38+
39+
initializePilasWeb(descriptor: string) {
40+
return new Promise<void>((resolve) => {
41+
const pilasweb = this.eval(`
2942
pilasengine.iniciar({
3043
ancho: 420,
3144
alto: 480,
@@ -35,68 +48,68 @@ class Scene {
3548
cargar_imagenes_estandar: false,
3649
silenciar_advertencia_de_multiples_ejecutar: true
3750
});`)
38-
pilasweb.ejecutar()
39-
pilasweb.setFPS(100)
40-
pilasweb.onready = resolve
41-
42-
this.listenToIframeMessages()
43-
})
44-
}
45-
46-
listenToIframeMessages() {
47-
window.addEventListener("message", (event) => {
48-
// exercises post error messages in the form { tipo: "error", error: object }
49-
// where object can be any error or { name: "ActividadError", message: "description"}
50-
if (event.data.tipo === "error")
51-
console.log(`Pilasweb execution ended with error: ${JSON.stringify(event.data.error)}`)
52-
})
53-
}
54-
55-
/**
56-
* Evals code on the iframe. Shouldn't be called outside the class.
57-
* The idea is that the responsability of managing the scene remains on this object.
58-
* @param code string with js code to run on the iframe
59-
*/
60-
private eval(code: string): any {
61-
return (this.iframe().contentWindow as any).eval(code)
62-
}
63-
64-
private imagesToPreload(descriptor: Challenge["sceneDescriptor"]) {
65-
//Responsibiliy of the exercise's scene class
66-
var images = this.eval(`${this.sceneName(descriptor)}.imagenesPreCarga()`)
67-
//TODO: Some scenes (like EscapeEnYacare) don't have images to preload. They should.
68-
images = images.length ? images : this.eval(`imageList`)
69-
70-
return JSON.stringify(images)
71-
}
72-
73-
74-
sceneName(sceneDescriptor: string): string {
75-
// if descriptor is of the form new ClassName(...). The regex (\w+) captures the classname.
76-
// The [1] access the first capture group
77-
const name = sceneDescriptor.match(/new\s+(\w+)\s*\(/)
78-
return name ? name[1] : sceneDescriptor
79-
}
80-
81-
restartScene(descriptor: Challenge["sceneDescriptor"]) {
82-
this.eval('pilas.reiniciar()')
83-
this.setChallenge(descriptor)
84-
}
85-
86-
sceneActor(): Actor {
87-
return this.eval('pilas.escena_actual().automata')
88-
}
89-
90-
sceneReceptor(receptor: string): Actor {
91-
return this.eval(`pilas.escena_actual().${receptor}`)
92-
}
93-
94-
isTheProblemSolved() {
95-
return this.eval(`pilas.escena_actual().estaResueltoElProblema();`);
96-
}
97-
98-
behaviourClass(behaviour: string): Behaviour {
99-
return this.eval(`
51+
pilasweb.ejecutar()
52+
pilasweb.setFPS(100)
53+
pilasweb.onready = resolve
54+
55+
this.listenToIframeMessages()
56+
})
57+
}
58+
59+
listenToIframeMessages() {
60+
window.addEventListener("message", (event) => {
61+
// exercises post error messages in the form { tipo: "error", error: object }
62+
// where object can be any error or { name: "ActividadError", message: "description"}
63+
if (event.data.tipo === "error")
64+
console.log(`Pilasweb execution ended with error: ${JSON.stringify(event.data.error)}`)
65+
})
66+
}
67+
68+
/**
69+
* Evals code on the iframe. Shouldn't be called outside the class.
70+
* The idea is that the responsability of managing the scene remains on this object.
71+
* @param code string with js code to run on the iframe
72+
*/
73+
private eval(code: string): any {
74+
return (this.iframe().contentWindow as any).eval(code)
75+
}
76+
77+
private imagesToPreload(descriptor: Challenge["sceneDescriptor"]) {
78+
//Responsibiliy of the exercise's scene class
79+
var images = this.eval(`${this.sceneName(descriptor)}.imagenesPreCarga()`)
80+
//TODO: Some scenes (like EscapeEnYacare) don't have images to preload. They should.
81+
images = images.length ? images : this.eval(`imageList`)
82+
83+
return JSON.stringify(images)
84+
}
85+
86+
87+
sceneName(sceneDescriptor: string): string {
88+
// if descriptor is of the form new ClassName(...). The regex (\w+) captures the classname.
89+
// The [1] access the first capture group
90+
const name = sceneDescriptor.match(/new\s+(\w+)\s*\(/)
91+
return name ? name[1] : sceneDescriptor
92+
}
93+
94+
restartScene(descriptor: Challenge["sceneDescriptor"]) {
95+
this.eval('pilas.reiniciar()')
96+
this.setChallenge(descriptor)
97+
}
98+
99+
sceneActor(): Actor {
100+
return this.eval('pilas.escena_actual().automata')
101+
}
102+
103+
sceneReceptor(receptor: string): Actor {
104+
return this.eval(`pilas.escena_actual().${receptor}`)
105+
}
106+
107+
isTheProblemSolved() {
108+
return this.eval(`pilas.escena_actual().estaResueltoElProblema();`);
109+
}
110+
111+
behaviourClass(behaviour: string): Behaviour {
112+
return this.eval(`
100113
var comportamiento = null;
101114
102115
if (window['${behaviour}']) {
@@ -111,18 +124,18 @@ class Scene {
111124
112125
comportamiento;
113126
`)
114-
}
127+
}
115128

116-
evaluateExpression(expression: string): boolean {
117-
return this.eval(`
129+
evaluateExpression(expression: string): boolean {
130+
return this.eval(`
118131
try {
119132
var value = pilas.escena_actual().automata.${expression}
120133
} catch (e) {
121134
pilas.escena_actual().errorHandler.handle(e);
122135
}
123136
124137
value`)
125-
}
138+
}
126139
}
127140

128141
export const scene = new Scene()

src/components/creator/Editor/CreatorViewMode.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Header, HeaderText } from "../../header/Header"
55
import { SerializedChallenge } from "../../serializedChallenge"
66
import { useTranslation } from "react-i18next"
77
import { ReturnToEditionButton } from "./ActionButtons/ReturnToEditButton"
8-
import { BetaBadge } from "../BetaBadge"
98
import { PBreadcrumbs } from "../../PBreadcrumbs"
109
import { EditorSubHeader } from "./Editor"
1110
import { useEffect } from "react"

0 commit comments

Comments
 (0)