From 1d7372e1eedd9c706563d7576202fa43866001e6 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Thu, 27 Feb 2025 11:10:55 +0800 Subject: [PATCH 01/46] new module for source academy minigame --- package.json | 3 +- src/bundles/robot_minigame/functions.ts | 183 +++++++++++++++++++++++ src/bundles/robot_minigame/index.ts | 23 +++ src/tabs/RobotMaze/canvas.tsx | 187 ++++++++++++++++++++++++ src/tabs/RobotMaze/index.tsx | 73 +++++++++ 5 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 src/bundles/robot_minigame/functions.ts create mode 100644 src/bundles/robot_minigame/index.ts create mode 100644 src/tabs/RobotMaze/canvas.tsx create mode 100644 src/tabs/RobotMaze/index.tsx diff --git a/package.json b/package.json index b6e7eb821a..dd9afbddd3 100644 --- a/package.json +++ b/package.json @@ -137,5 +137,6 @@ "resolutions": { "esbuild": "^0.18.20", "**/gl": "^6.0.2" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts new file mode 100644 index 0000000000..dff6185e41 --- /dev/null +++ b/src/bundles/robot_minigame/functions.ts @@ -0,0 +1,183 @@ +/* +* Internally, the robot uses a grid based movement and collision system +*/ + +import context from 'js-slang/context'; + +let robotPos: Point = {x: 0, y: 0}; + +let movePoints: Point[]; + +const DIRECTIONS = { + UP: 0, + RIGHT: 1, + DOWN: 2, + LEFT: 3 +}; + +let robotRotation = 1; + +// default grid width and height is 25 +context.moduleContexts.robot_minigame.state = { + isInit: false, + width: 25, + height: 25, + walls: [], + movePoints: [], + message: "moved successfully", + success: true +} + +type Point = {x: number, y: number} +type Wall = {p1: Point, p2: Point} + +export function set_pos(x: number, y: number): void { + robotPos.x = x; + robotPos.y = y; +} + +export function set_grid_width(width: number) { + context.moduleContexts.robot_minigame.state.width = width; +} + +export function set_grid_height(height: number) { + context.moduleContexts.robot_minigame.state.height = height; +} + +export function init(gridWidth: number, gridHeight: number, posX: number, posY: number) { + set_grid_width(gridWidth); + set_grid_height(gridHeight); + set_pos(posX, posY); + context.moduleContexts.robot_minigame.state.movePoints.push({x: posX, y: posY}); + context.moduleContexts.robot_minigame.state.isInit = true; +} + +export function turn_left() { + if (alrCollided()) return; + + robotRotation -= 1; + if (robotRotation < 0) { + robotRotation = 3; + } +} + +export function turn_right() { + if (alrCollided()) return; + + robotRotation = (robotRotation + 1) % 4; +} + +// takes the top left and bottom right corners of walls +// in terms of grid boxes +// grid starts from (0, 0) at the top left corner btw +export function set_wall(x1: number, y1: number, x2: number, y2: number) { + let wall: Wall = {p1: {x: x1, y: y1}, p2: {x: x2, y: y2}}; + context.moduleContexts.robot_minigame.state.walls.push(wall); +} + +export function move_forward(dist: number): void { + if (alrCollided()) return; + + simulate(dist); +} + +export function getX():number { + return robotPos.x; +} + +export function getY():number { + return robotPos.y; +} + +function simulate(moveDist: number) { + let dx: number = 0; + let dy: number = 0; + switch (robotRotation) { + case DIRECTIONS.UP: + dy = -1; + break; + case DIRECTIONS.RIGHT: + dx = 1; + break; + case DIRECTIONS.DOWN: + dy = 1; + break; + case DIRECTIONS.LEFT: + dx = -1; + break; + } + + // moves robot by one grid box and checks collision + for (var i = 0; i < moveDist; i++) { + robotPos.x += dx; + robotPos.y += dy; + + var walls = context.moduleContexts.robot_minigame.state.walls; + for (let j = 0; j < walls.length; j++) { + if (checkWallCollision(walls[j], robotPos)) { + context.moduleContexts.robot_minigame.state.success = false; + context.moduleContexts.robot_minigame.state.message = "collided"; + context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); + return; + } + } + } + + context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); + + // OLD CODE + // let destX = robotPos.x + moveDist * Math.cos(robotAngle); + // let destY = robotPos.y + moveDist * Math.sin(robotAngle); + // let destPoint: Point = {x: destX, y: destY} + + // for (let i = 0; i < steps; i++) { + // let distX: number = moveSpeed * Math.cos(robotAngle); + // let distY: number = moveSpeed * Math.sin(robotAngle); + + // robotPos.x += distX; + // robotPos.y += distY; + + // for (let j = 0; j < walls.length; j++) { + // if(checkWallCollision(walls[j], robotPos)) { + // addMessage("Collided with wall!!"); + // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); + // return; + // } + // } + + // if (distanceBetween(destPoint, robotPos) < robotRadius) { + // robotPos = destPoint; + // addMessage(`Robot moved forward by ${moveDist} at angle ${robotAngle} radians\n`); + // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); + // return; + // } + // } + + +} + + +function checkWallCollision(wall: Wall, pos: Point): boolean { + // // Apply the distance formula + // const p1 = wall.p1; + // const p2 = wall.p2; + + // const numerator = Math.abs((p2.y - p1.y) * pos.x - (p2.x - p1.x) * pos.y + p2.x * p1.y - p2.y * p1.x); + // const denominator = Math.sqrt((p2.y - p1.y) ** 2 + (p2.x - p1.x) ** 2); + + // return numerator / denominator < robotRadius; + + const p1 = wall.p1; + const p2 = wall.p2; + + const minX = Math.min(p1.x, p2.x); + const maxX = Math.max(p1.x, p2.x); + const minY = Math.min(p1.y, p2.y); + const maxY = Math.max(p1.y, p2.y); + + return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY; +} + +function alrCollided() { + return !context.moduleContexts.robot_minigame.state.success; +} diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts new file mode 100644 index 0000000000..4612e43b23 --- /dev/null +++ b/src/bundles/robot_minigame/index.ts @@ -0,0 +1,23 @@ +/** + * A single sentence summarising the module (this sentence is displayed larger). + * + * Sentences describing the module. More sentences about the module. + * + * @module robot_minigame + * @author Koh Wai Kei + * @author Author Name + */ + +export { + init, + set_pos, + set_wall, + move_forward, + turn_left, + turn_right, + getX, + getY +} from "./functions"; + + + diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx new file mode 100644 index 0000000000..1072150cb7 --- /dev/null +++ b/src/tabs/RobotMaze/canvas.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { type DebuggerContext } from '../../typings/type_helpers'; + +/** + * React Component props for the Tab. + */ +type Props = { + children?: never; + className?: never; + state?: any; + }; + +/** + * React Component state for the Tab. + */ +type State = { + isAnimationEnded: boolean; +}; + +type Point = { + x: number; + y: number; +} + +type Wall = { + p1: Point; + p2: Point +} + +export default class Canvas extends React.Component { + private canvasRef: React.RefObject; + private animationFrameId: number | null = null; + private speed: number = 2; // Speed of the movement + private points: Point[]; + private xPos: number; + private yPos: number; + private pointIndex: number = 1; + private walls: Wall[]; + + private CANVAS_WIDTH: number = 500; + private CANVAS_HEIGHT: number = 500; + private GRID_SIZE: number = 20; + + constructor(props) { + super(props); + this.state = { + isAnimationEnded: false + } + + // setting some variables in what may or may not be good practice + this.CANVAS_WIDTH = this.props.state.width * this.GRID_SIZE; + this.CANVAS_HEIGHT = this.props.state.height * this.GRID_SIZE; + this.points = this.props.state.movePoints; // a series of points is passed back from the modules which determines the path of robot + this.walls = this.props.state.walls; + this.xPos = this.points[0].x; + this.yPos = this.points[0].y; + + this.canvasRef = React.createRef(); + } + + componentDidMount() { + this.setupCanvas(); + } + + componentWillUnmount() { + this.stopAnimation(); + } + + setupCanvas = () => { + const canvas = this.canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + canvas.width = this.CANVAS_WIDTH; + canvas.height = this.CANVAS_HEIGHT; + + ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); + this.drawWalls(ctx); + this.drawGrid(ctx); + ctx.fillStyle = "black"; + ctx.fillRect(this.xPos, this.yPos, 20, 20); + } + + startAnimation = () => { + this.animationFrameId = requestAnimationFrame(this.animate); + }; + + stopAnimation = () => { + if (this.animationFrameId) { + this.setState({isAnimationEnded: true}) + cancelAnimationFrame(this.animationFrameId); + } + } + + animate = () => { + const canvas = this.canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + if (this.pointIndex >= this.points.length) return; + + // Draw the moving square + ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); + this.drawWalls(ctx); + this.drawGrid(ctx); + + // Update position + const targetPoint = this.points[this.pointIndex]; + const dx = targetPoint.x * this.GRID_SIZE - this.xPos; + const dy = targetPoint.y * this.GRID_SIZE - this.yPos; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 1) { + this.xPos += (dx / distance) * this.speed; + this.yPos += (dy / distance) * this.speed; + } + + // if distance to target point is small + if (distance <= 1) { + // snap to the target point + this.xPos = targetPoint.x * this.GRID_SIZE; + this.yPos = targetPoint.y * this.GRID_SIZE; + + // set target to the next point in the array + this.pointIndex+= 1; + + if (this.pointIndex >= this.points.length) { + this.stopAnimation(); + } + } + ctx.fillStyle = "black"; + ctx.fillRect(this.xPos, this.yPos, 20, 20); + + // Request the next frame + this.animationFrameId = requestAnimationFrame(this.animate); + }; + + drawWalls(ctx: CanvasRenderingContext2D) { + for (let i = 0; i < this.walls.length; i++) { + // assumption is made that p1 is going to be the top left corner, might make some error checks for that later on + const p1 = this.walls[i].p1; + const p2 = this.walls[i].p2; + const width = (p2.x - p1.x) * this.GRID_SIZE + this.GRID_SIZE; + const height = (p2.y - p1.y) * this.GRID_SIZE + this.GRID_SIZE; + + ctx.fillStyle = "rgb(128, 128, 128)"; + ctx.fillRect(p1.x * this.GRID_SIZE, p1.y * this.GRID_SIZE, width, height); + } + } + + drawGrid(ctx: CanvasRenderingContext2D) { + // Draw grid + ctx.strokeStyle = "gray"; + ctx.lineWidth = 1; + + // Draw vertical lines + for (let x = 0; x <= this.CANVAS_WIDTH; x += this.GRID_SIZE) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, this.CANVAS_HEIGHT); + ctx.stroke(); + } + + // Draw horizontal lines + for (let y = 0; y <= this.CANVAS_HEIGHT; y += this.GRID_SIZE) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(this.CANVAS_WIDTH, y); + ctx.stroke(); + } + } + + public render() { + return ( + <> + +

{this.state.isAnimationEnded ? this.props.state.message : <>}

+
+ +
+ + + + ); + } +} \ No newline at end of file diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx new file mode 100644 index 0000000000..3b66575a67 --- /dev/null +++ b/src/tabs/RobotMaze/index.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { type DebuggerContext } from '../../typings/type_helpers'; +import Canvas from "./canvas" + +/** + * + * @author + * @author + */ + +/** + * React Component props for the Tab. + */ +type Props = { + children?: never; + className?: never; + context?: any; +}; + +/** + * React Component state for the Tab. + */ +type State = { + counter: number; +}; + +/** + * The main React Component of the Tab. + */ +class RobotMaze extends React.Component { + constructor(props) { + super(props); + } + + public render() { + const { context: { moduleContexts: { robot_minigame } } } = this.props.context; + + return ( + + ); + } +} + +export default { + /** + * This function will be called to determine if the component will be + * rendered. Currently spawns when the result in the REPL is "test". + * @param {DebuggerContext} context + * @returns {boolean} + */ + toSpawn(context: DebuggerContext) { + return context.context?.moduleContexts?.robot_minigame.state.isInit; + }, + + /** + * This function will be called to render the module tab in the side contents + * on Source Academy frontend. + * @param {DebuggerContext} context + */ + body: (context: any) => , + + /** + * The Tab's icon tooltip in the side contents on Source Academy frontend. + */ + label: 'Robot Maze', + + /** + * BlueprintJS IconName element's name, used to render the icon which will be + * displayed in the side contents panel. + * @see https://blueprintjs.com/docs/#icons + */ + iconName: 'build', +}; \ No newline at end of file From 04766d2c25b064b2e98ea3eaba2db8c8a3ab3925 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Thu, 27 Feb 2025 12:00:55 +0800 Subject: [PATCH 02/46] formatting code --- modules.json | 5 + src/bundles/robot_minigame/functions.ts | 235 ++++++++++++------------ src/bundles/robot_minigame/index.ts | 5 +- src/tabs/RobotMaze/canvas.tsx | 84 +++++---- src/tabs/RobotMaze/index.tsx | 17 +- 5 files changed, 168 insertions(+), 178 deletions(-) diff --git a/modules.json b/modules.json index 914bd7bc80..4dbead98b8 100644 --- a/modules.json +++ b/modules.json @@ -116,5 +116,10 @@ "tabs": [ "Nbody" ] + }, + "robot_minigame": { + "tabs": [ + "RobotMaze" + ] } } \ No newline at end of file diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index dff6185e41..979d3efbae 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -1,35 +1,34 @@ -/* -* Internally, the robot uses a grid based movement and collision system +/* +* Currently uses a grid based system, will upgrade to more fancy stuff later +* The movement is simulated, then a series of movement points is passed to the module context, which the frontend then uses to render */ import context from 'js-slang/context'; -let robotPos: Point = {x: 0, y: 0}; - -let movePoints: Point[]; +const robotPos: Point = {x: 0, y: 0}; const DIRECTIONS = { - UP: 0, - RIGHT: 1, - DOWN: 2, - LEFT: 3 + UP: 0, + RIGHT: 1, + DOWN: 2, + LEFT: 3 }; let robotRotation = 1; // default grid width and height is 25 context.moduleContexts.robot_minigame.state = { - isInit: false, - width: 25, - height: 25, - walls: [], - movePoints: [], - message: "moved successfully", - success: true -} + isInit: false, + width: 25, + height: 25, + walls: [], + movePoints: [], + message: 'moved successfully', + success: true +}; -type Point = {x: number, y: number} -type Wall = {p1: Point, p2: Point} +type Point = {x: number, y: number}; +type Wall = {p1: Point, p2: Point}; export function set_pos(x: number, y: number): void { robotPos.x = x; @@ -37,147 +36,145 @@ export function set_pos(x: number, y: number): void { } export function set_grid_width(width: number) { - context.moduleContexts.robot_minigame.state.width = width; + context.moduleContexts.robot_minigame.state.width = width; } export function set_grid_height(height: number) { - context.moduleContexts.robot_minigame.state.height = height; + context.moduleContexts.robot_minigame.state.height = height; } export function init(gridWidth: number, gridHeight: number, posX: number, posY: number) { - set_grid_width(gridWidth); - set_grid_height(gridHeight); - set_pos(posX, posY); - context.moduleContexts.robot_minigame.state.movePoints.push({x: posX, y: posY}); - context.moduleContexts.robot_minigame.state.isInit = true; + set_grid_width(gridWidth); + set_grid_height(gridHeight); + set_pos(posX, posY); + context.moduleContexts.robot_minigame.state.movePoints.push({x: posX, y: posY}); + context.moduleContexts.robot_minigame.state.isInit = true; } export function turn_left() { - if (alrCollided()) return; + if (alrCollided()) return; - robotRotation -= 1; - if (robotRotation < 0) { - robotRotation = 3; - } + robotRotation -= 1; + if (robotRotation < 0) { + robotRotation = 3; + } } export function turn_right() { - if (alrCollided()) return; + if (alrCollided()) return; - robotRotation = (robotRotation + 1) % 4; + robotRotation = (robotRotation + 1) % 4; } -// takes the top left and bottom right corners of walls +// takes the top left and bottom right corners of walls // in terms of grid boxes // grid starts from (0, 0) at the top left corner btw export function set_wall(x1: number, y1: number, x2: number, y2: number) { - let wall: Wall = {p1: {x: x1, y: y1}, p2: {x: x2, y: y2}}; - context.moduleContexts.robot_minigame.state.walls.push(wall); + const wall: Wall = {p1: {x: x1, y: y1}, p2: {x: x2, y: y2}}; + context.moduleContexts.robot_minigame.state.walls.push(wall); } export function move_forward(dist: number): void { - if (alrCollided()) return; - - simulate(dist); + if (alrCollided()) return; + + simulate(dist); } export function getX():number { - return robotPos.x; + return robotPos.x; } export function getY():number { - return robotPos.y; + return robotPos.y; } function simulate(moveDist: number) { - let dx: number = 0; - let dy: number = 0; - switch (robotRotation) { - case DIRECTIONS.UP: - dy = -1; - break; - case DIRECTIONS.RIGHT: - dx = 1; - break; - case DIRECTIONS.DOWN: - dy = 1; - break; - case DIRECTIONS.LEFT: - dx = -1; - break; - } - - // moves robot by one grid box and checks collision - for (var i = 0; i < moveDist; i++) { - robotPos.x += dx; - robotPos.y += dy; - - var walls = context.moduleContexts.robot_minigame.state.walls; - for (let j = 0; j < walls.length; j++) { - if (checkWallCollision(walls[j], robotPos)) { - context.moduleContexts.robot_minigame.state.success = false; - context.moduleContexts.robot_minigame.state.message = "collided"; - context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); - return; - } - } + let dx: number = 0; + let dy: number = 0; + switch (robotRotation) { + case DIRECTIONS.UP: + dy = -1; + break; + case DIRECTIONS.RIGHT: + dx = 1; + break; + case DIRECTIONS.DOWN: + dy = 1; + break; + case DIRECTIONS.LEFT: + dx = -1; + break; + } + + // moves robot by one grid box and checks collision + for (let i = 0; i < moveDist; i++) { + robotPos.x += dx; + robotPos.y += dy; + + const walls = context.moduleContexts.robot_minigame.state.walls; + for (let j = 0; j < walls.length; j++) { + if (checkWallCollision(walls[j], robotPos)) { + context.moduleContexts.robot_minigame.state.success = false; + context.moduleContexts.robot_minigame.state.message = 'collided'; + context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); + return; + } } - - context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); - - // OLD CODE - // let destX = robotPos.x + moveDist * Math.cos(robotAngle); - // let destY = robotPos.y + moveDist * Math.sin(robotAngle); - // let destPoint: Point = {x: destX, y: destY} - - // for (let i = 0; i < steps; i++) { - // let distX: number = moveSpeed * Math.cos(robotAngle); - // let distY: number = moveSpeed * Math.sin(robotAngle); - - // robotPos.x += distX; - // robotPos.y += distY; - - // for (let j = 0; j < walls.length; j++) { - // if(checkWallCollision(walls[j], robotPos)) { - // addMessage("Collided with wall!!"); - // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); - // return; - // } - // } - - // if (distanceBetween(destPoint, robotPos) < robotRadius) { - // robotPos = destPoint; - // addMessage(`Robot moved forward by ${moveDist} at angle ${robotAngle} radians\n`); - // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); - // return; - // } - // } - + } + + context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); + + // OLD CODE + // let destX = robotPos.x + moveDist * Math.cos(robotAngle); + // let destY = robotPos.y + moveDist * Math.sin(robotAngle); + // let destPoint: Point = {x: destX, y: destY} + + // for (let i = 0; i < steps; i++) { + // let distX: number = moveSpeed * Math.cos(robotAngle); + // let distY: number = moveSpeed * Math.sin(robotAngle); + + // robotPos.x += distX; + // robotPos.y += distY; + + // for (let j = 0; j < walls.length; j++) { + // if(checkWallCollision(walls[j], robotPos)) { + // addMessage("Collided with wall!!"); + // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); + // return; + // } + // } + + // if (distanceBetween(destPoint, robotPos) < robotRadius) { + // robotPos = destPoint; + // addMessage(`Robot moved forward by ${moveDist} at angle ${robotAngle} radians\n`); + // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); + // return; + // } + // } } - function checkWallCollision(wall: Wall, pos: Point): boolean { - // // Apply the distance formula - // const p1 = wall.p1; - // const p2 = wall.p2; + // // Apply the distance formula + // const p1 = wall.p1; + // const p2 = wall.p2; + + // const numerator = Math.abs((p2.y - p1.y) * pos.x - (p2.x - p1.x) * pos.y + p2.x * p1.y - p2.y * p1.x); + // const denominator = Math.sqrt((p2.y - p1.y) ** 2 + (p2.x - p1.x) ** 2); - // const numerator = Math.abs((p2.y - p1.y) * pos.x - (p2.x - p1.x) * pos.y + p2.x * p1.y - p2.y * p1.x); - // const denominator = Math.sqrt((p2.y - p1.y) ** 2 + (p2.x - p1.x) ** 2); - - // return numerator / denominator < robotRadius; + // return numerator / denominator < robotRadius; - const p1 = wall.p1; - const p2 = wall.p2; + const p1 = wall.p1; + const p2 = wall.p2; - const minX = Math.min(p1.x, p2.x); - const maxX = Math.max(p1.x, p2.x); - const minY = Math.min(p1.y, p2.y); - const maxY = Math.max(p1.y, p2.y); + const minX = Math.min(p1.x, p2.x); + const maxX = Math.max(p1.x, p2.x); + const minY = Math.min(p1.y, p2.y); + const maxY = Math.max(p1.y, p2.y); - return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY; + return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY; } function alrCollided() { - return !context.moduleContexts.robot_minigame.state.success; + return !context.moduleContexts.robot_minigame.state.success; } diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 4612e43b23..070011a51f 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -17,7 +17,4 @@ export { turn_right, getX, getY -} from "./functions"; - - - +} from './functions'; diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx index 1072150cb7..64f3350c64 100644 --- a/src/tabs/RobotMaze/canvas.tsx +++ b/src/tabs/RobotMaze/canvas.tsx @@ -1,31 +1,30 @@ import React from 'react'; -import { type DebuggerContext } from '../../typings/type_helpers'; /** * React Component props for the Tab. */ type Props = { - children?: never; - className?: never; - state?: any; - }; - + children?: never; + className?: never; + state?: any; +}; + /** * React Component state for the Tab. */ type State = { - isAnimationEnded: boolean; + isAnimationEnded: boolean; }; type Point = { - x: number; - y: number; -} + x: number; + y: number; +}; type Wall = { p1: Point; p2: Point -} +}; export default class Canvas extends React.Component { private canvasRef: React.RefObject; @@ -45,7 +44,7 @@ export default class Canvas extends React.Component { super(props); this.state = { isAnimationEnded: false - } + }; // setting some variables in what may or may not be good practice this.CANVAS_WIDTH = this.props.state.width * this.GRID_SIZE; @@ -69,7 +68,7 @@ export default class Canvas extends React.Component { setupCanvas = () => { const canvas = this.canvasRef.current; if (!canvas) return; - const ctx = canvas.getContext("2d"); + const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = this.CANVAS_WIDTH; @@ -78,9 +77,9 @@ export default class Canvas extends React.Component { ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); - ctx.fillStyle = "black"; + ctx.fillStyle = 'black'; ctx.fillRect(this.xPos, this.yPos, 20, 20); - } + }; startAnimation = () => { this.animationFrameId = requestAnimationFrame(this.animate); @@ -88,16 +87,16 @@ export default class Canvas extends React.Component { stopAnimation = () => { if (this.animationFrameId) { - this.setState({isAnimationEnded: true}) + this.setState({isAnimationEnded: true}); cancelAnimationFrame(this.animationFrameId); } - } + }; animate = () => { const canvas = this.canvasRef.current; if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; if (this.pointIndex >= this.points.length) return; // Draw the moving square @@ -110,7 +109,7 @@ export default class Canvas extends React.Component { const dx = targetPoint.x * this.GRID_SIZE - this.xPos; const dy = targetPoint.y * this.GRID_SIZE - this.yPos; const distance = Math.sqrt(dx * dx + dy * dy); - + if (distance > 1) { this.xPos += (dx / distance) * this.speed; this.yPos += (dy / distance) * this.speed; @@ -118,18 +117,18 @@ export default class Canvas extends React.Component { // if distance to target point is small if (distance <= 1) { - // snap to the target point - this.xPos = targetPoint.x * this.GRID_SIZE; - this.yPos = targetPoint.y * this.GRID_SIZE; + // snap to the target point + this.xPos = targetPoint.x * this.GRID_SIZE; + this.yPos = targetPoint.y * this.GRID_SIZE; - // set target to the next point in the array - this.pointIndex+= 1; + // set target to the next point in the array + this.pointIndex+= 1; - if (this.pointIndex >= this.points.length) { - this.stopAnimation(); - } + if (this.pointIndex >= this.points.length) { + this.stopAnimation(); + } } - ctx.fillStyle = "black"; + ctx.fillStyle = 'black'; ctx.fillRect(this.xPos, this.yPos, 20, 20); // Request the next frame @@ -143,15 +142,15 @@ export default class Canvas extends React.Component { const p2 = this.walls[i].p2; const width = (p2.x - p1.x) * this.GRID_SIZE + this.GRID_SIZE; const height = (p2.y - p1.y) * this.GRID_SIZE + this.GRID_SIZE; - - ctx.fillStyle = "rgb(128, 128, 128)"; - ctx.fillRect(p1.x * this.GRID_SIZE, p1.y * this.GRID_SIZE, width, height); + + ctx.fillStyle = 'rgb(128, 128, 128)'; + ctx.fillRect(p1.x * this.GRID_SIZE, p1.y * this.GRID_SIZE, width, height); } } drawGrid(ctx: CanvasRenderingContext2D) { // Draw grid - ctx.strokeStyle = "gray"; + ctx.strokeStyle = 'gray'; ctx.lineWidth = 1; // Draw vertical lines @@ -173,15 +172,14 @@ export default class Canvas extends React.Component { public render() { return ( - <> - -

{this.state.isAnimationEnded ? this.props.state.message : <>}

-
- -
- - - + <> + +

{this.state.isAnimationEnded ? this.props.state.message : <>}

+
+ +
+ + ); } -} \ No newline at end of file +} diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 3b66575a67..83c72d0c98 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { type DebuggerContext } from '../../typings/type_helpers'; -import Canvas from "./canvas" +import Canvas from './canvas'; /** * @@ -17,13 +17,6 @@ type Props = { context?: any; }; -/** - * React Component state for the Tab. - */ -type State = { - counter: number; -}; - /** * The main React Component of the Tab. */ @@ -48,9 +41,9 @@ export default { * @param {DebuggerContext} context * @returns {boolean} */ - toSpawn(context: DebuggerContext) { - return context.context?.moduleContexts?.robot_minigame.state.isInit; - }, + toSpawn(context: DebuggerContext) { + return context.context?.moduleContexts?.robot_minigame.state.isInit; + }, /** * This function will be called to render the module tab in the side contents @@ -70,4 +63,4 @@ export default { * @see https://blueprintjs.com/docs/#icons */ iconName: 'build', -}; \ No newline at end of file +}; From 1eabde26b9e5f218c49dad2dad45f3a18a881bc1 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Sat, 1 Mar 2025 23:18:45 +0800 Subject: [PATCH 03/46] raycasting implementedgit status --- src/bundles/robot_minigame/functions.ts | 277 +++++++++++++----------- src/bundles/robot_minigame/index.ts | 2 +- src/tabs/RobotMaze/canvas.tsx | 79 ++++--- src/tabs/RobotMaze/index.tsx | 5 +- 4 files changed, 203 insertions(+), 160 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 979d3efbae..da6496009c 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -5,176 +5,211 @@ import context from 'js-slang/context'; -const robotPos: Point = {x: 0, y: 0}; +type Point = {x: number, y: number}; +type Wall = {p1: Point, p2: Point}; -const DIRECTIONS = { - UP: 0, - RIGHT: 1, - DOWN: 2, - LEFT: 3 -}; +type Polygon = Point[]; + +type StateData = { + isInit: boolean, + width: number, + height: number, + walls: Polygon[], + movePoints: Point[], + message: String, + success: boolean, + messages: String[] +} -let robotRotation = 1; +type Robot = { + x: number; // the top left corner + y: number; + dx: number; + dy: number; + radius: number +} -// default grid width and height is 25 -context.moduleContexts.robot_minigame.state = { +let stateData: StateData = { isInit: false, - width: 25, - height: 25, + width: 500, + height: 500, walls: [], movePoints: [], message: 'moved successfully', - success: true -}; + success: true, + messages: [] +} -type Point = {x: number, y: number}; -type Wall = {p1: Point, p2: Point}; +let robot: Robot = { + x: 25, // default start pos, puts it at the top left corner of canvas without colliding with the walls + y: 25, + dx: 1, + dy: 0, + radius: 20 //give the robot a circular hitbox +} + +let bounds: Point[] = [] + +// default grid width and height is 25 +context.moduleContexts.robot_minigame.state = stateData; export function set_pos(x: number, y: number): void { - robotPos.x = x; - robotPos.y = y; + robot.x = x; + robot.y = y; } -export function set_grid_width(width: number) { - context.moduleContexts.robot_minigame.state.width = width; +export function set_width(width: number) { + stateData.width = width; } -export function set_grid_height(height: number) { - context.moduleContexts.robot_minigame.state.height = height; +export function set_height(height: number) { + stateData.height = height; } -export function init(gridWidth: number, gridHeight: number, posX: number, posY: number) { - set_grid_width(gridWidth); - set_grid_height(gridHeight); +export function init(width: number, height: number, posX: number, posY: number) { + set_width(width); + set_height(height); set_pos(posX, posY); - context.moduleContexts.robot_minigame.state.movePoints.push({x: posX, y: posY}); - context.moduleContexts.robot_minigame.state.isInit = true; + stateData.movePoints.push({x: posX, y: posY}); + stateData.isInit = true; + + bounds = [ + {x: 0, y: 0}, + {x: width, y: 0}, + {x: width, y: height}, + {x: 0, y: height} + ] + } export function turn_left() { if (alrCollided()) return; - robotRotation -= 1; - if (robotRotation < 0) { - robotRotation = 3; - } + let currentAngle = Math.tan(robot.dy / robot.dx); + currentAngle -= Math.PI / 2; + + robot.dx = Math.cos(currentAngle); + robot.dy = Math.sin(currentAngle); } export function turn_right() { if (alrCollided()) return; - robotRotation = (robotRotation + 1) % 4; + let currentAngle = Math.tan(robot.dy / robot.dx); + currentAngle += Math.PI / 2; + + robot.dx = Math.cos(currentAngle); + robot.dy = Math.sin(currentAngle); } -// takes the top left and bottom right corners of walls -// in terms of grid boxes -// grid starts from (0, 0) at the top left corner btw -export function set_wall(x1: number, y1: number, x2: number, y2: number) { - const wall: Wall = {p1: {x: x1, y: y1}, p2: {x: x2, y: y2}}; - context.moduleContexts.robot_minigame.state.walls.push(wall); +export function set_rect_wall(x: number, y: number, width: number, height: number) { + const polygon: Polygon = [ + {x: x, y: y}, + {x: x + width, y: y}, + {x: x+width, y: y+height}, + {x: x, y:y+height} + ] + + stateData.walls.push(polygon); } -export function move_forward(dist: number): void { +export function move_forward(): void { if (alrCollided()) return; - simulate(dist); + const collisionPoint: Point | null = raycast(stateData.walls) + if (collisionPoint !== null) { + let nextPoint: Point = { + x: collisionPoint.x - robot.dx * (robot.radius + 5), + y: collisionPoint.y - robot.dy * (robot.radius + 5) + } + + robot.x = nextPoint.x; + robot.y = nextPoint.y; + stateData.movePoints.push(nextPoint); + } } export function getX():number { - return robotPos.x; + return robot.x; } export function getY():number { - return robotPos.y; -} - -function simulate(moveDist: number) { - let dx: number = 0; - let dy: number = 0; - switch (robotRotation) { - case DIRECTIONS.UP: - dy = -1; - break; - case DIRECTIONS.RIGHT: - dx = 1; - break; - case DIRECTIONS.DOWN: - dy = 1; - break; - case DIRECTIONS.LEFT: - dx = -1; - break; - } + return robot.y; +} - // moves robot by one grid box and checks collision - for (let i = 0; i < moveDist; i++) { - robotPos.x += dx; - robotPos.y += dy; - - const walls = context.moduleContexts.robot_minigame.state.walls; - for (let j = 0; j < walls.length; j++) { - if (checkWallCollision(walls[j], robotPos)) { - context.moduleContexts.robot_minigame.state.success = false; - context.moduleContexts.robot_minigame.state.message = 'collided'; - context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); - return; +function raycast(polygons: Polygon[]): Point | null { + let nearest: Point | null = null; + let minDist = Infinity; + + for (const polygon of polygons) { + stateData.messages.push("checking polygon"); + const numVertices = polygon.length; + + for (let i = 0; i < numVertices; i++) { + const x1 = polygon[i].x, y1 = polygon[i].y; + const x2 = polygon[(i + 1) % numVertices].x, y2 = polygon[(i + 1) % numVertices].y; + + const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); + + if (intersection.collided && intersection.dist < minDist) { + minDist = intersection.dist + nearest = {x: intersection.x, y: intersection.y}; + } } - } } - context.moduleContexts.robot_minigame.state.movePoints.push({x: robotPos.x, y: robotPos.y}); - - // OLD CODE - // let destX = robotPos.x + moveDist * Math.cos(robotAngle); - // let destY = robotPos.y + moveDist * Math.sin(robotAngle); - // let destPoint: Point = {x: destX, y: destY} + // if no collisions with obstacles, check the outer bounds of map + if (nearest === null) { + for (let i = 0; i < bounds.length; i++) { + const x1 = bounds[i].x, y1 = bounds[i].y; + const x2 = bounds[(i + 1) % bounds.length].x, y2 = bounds[(i + 1) % bounds.length].y; - // for (let i = 0; i < steps; i++) { - // let distX: number = moveSpeed * Math.cos(robotAngle); - // let distY: number = moveSpeed * Math.sin(robotAngle); + const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); - // robotPos.x += distX; - // robotPos.y += distY; - - // for (let j = 0; j < walls.length; j++) { - // if(checkWallCollision(walls[j], robotPos)) { - // addMessage("Collided with wall!!"); - // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); - // return; - // } - // } - - // if (distanceBetween(destPoint, robotPos) < robotRadius) { - // robotPos = destPoint; - // addMessage(`Robot moved forward by ${moveDist} at angle ${robotAngle} radians\n`); - // addMessage(`Position: (${robotPos.x.toFixed(3)}, ${robotPos.y.toFixed(3)}), Rotation: ${robotAngle}\n`); - // return; - // } - // } + if (intersection.collided && intersection.dist < minDist) { + minDist = intersection.dist + nearest = {x: intersection.x, y: intersection.y}; + } + } + } + return nearest; // Closest intersection point } -function checkWallCollision(wall: Wall, pos: Point): boolean { - // // Apply the distance formula - // const p1 = wall.p1; - // const p2 = wall.p2; - - // const numerator = Math.abs((p2.y - p1.y) * pos.x - (p2.x - p1.x) * pos.y + p2.x * p1.y - p2.y * p1.x); - // const denominator = Math.sqrt((p2.y - p1.y) ** 2 + (p2.x - p1.x) ** 2); - - // return numerator / denominator < robotRadius; - - const p1 = wall.p1; - const p2 = wall.p2; - - const minX = Math.min(p1.x, p2.x); - const maxX = Math.max(p1.x, p2.x); - const minY = Math.min(p1.y, p2.y); - const maxY = Math.max(p1.y, p2.y); - - return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY; +//Determine if a ray and a line segment intersect, and if so, determine the collision point +function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4){ + var denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); + var r; + var s; + var x; + var y; + var b = false; + + //If lines not collinear or parallel + if(denom != 0){ + //Intersection in ray "local" coordinates + r = (((y1 - y3) * (x4 - x3)) - (x1 - x3) * (y4 - y3)) / denom; + + //Intersection in segment "local" coordinates + s = (((y1 - y3) * (x2 - x1)) - (x1 - x3) * (y2 - y1)) / denom; + + //The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on + if (r >= 0) + { + //If point along the line segment + if (s >= 0 && s <= 1) + { + b = true; + //Get point coordinates (offset by r local units from start of ray) + x = x1 + r * (x2 - x1); + y = y1 + r * (y2 - y1); + } + } + } + var p = {collided: b, x: x, y: y, dist: r}; + return p; } function alrCollided() { - return !context.moduleContexts.robot_minigame.state.success; + return !stateData.success; } diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 070011a51f..c1bd9a6ca4 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -11,7 +11,7 @@ export { init, set_pos, - set_wall, + set_rect_wall, move_forward, turn_left, turn_right, diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx index 64f3350c64..4f81e0126a 100644 --- a/src/tabs/RobotMaze/canvas.tsx +++ b/src/tabs/RobotMaze/canvas.tsx @@ -26,6 +26,8 @@ type Wall = { p2: Point }; +type Polygon = Point[] + export default class Canvas extends React.Component { private canvasRef: React.RefObject; private animationFrameId: number | null = null; @@ -34,11 +36,10 @@ export default class Canvas extends React.Component { private xPos: number; private yPos: number; private pointIndex: number = 1; - private walls: Wall[]; + private walls: Polygon[]; private CANVAS_WIDTH: number = 500; private CANVAS_HEIGHT: number = 500; - private GRID_SIZE: number = 20; constructor(props) { super(props); @@ -47,8 +48,8 @@ export default class Canvas extends React.Component { }; // setting some variables in what may or may not be good practice - this.CANVAS_WIDTH = this.props.state.width * this.GRID_SIZE; - this.CANVAS_HEIGHT = this.props.state.height * this.GRID_SIZE; + this.CANVAS_WIDTH = this.props.state.width; + this.CANVAS_HEIGHT = this.props.state.height; this.points = this.props.state.movePoints; // a series of points is passed back from the modules which determines the path of robot this.walls = this.props.state.walls; this.xPos = this.points[0].x; @@ -77,8 +78,7 @@ export default class Canvas extends React.Component { ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); - ctx.fillStyle = 'black'; - ctx.fillRect(this.xPos, this.yPos, 20, 20); + this.drawRobot(ctx, this.xPos, this.yPos) }; startAnimation = () => { @@ -99,15 +99,14 @@ export default class Canvas extends React.Component { if (!ctx) return; if (this.pointIndex >= this.points.length) return; - // Draw the moving square ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); // Update position const targetPoint = this.points[this.pointIndex]; - const dx = targetPoint.x * this.GRID_SIZE - this.xPos; - const dy = targetPoint.y * this.GRID_SIZE - this.yPos; + const dx = targetPoint.x - this.xPos; + const dy = targetPoint.y - this.yPos; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 1) { @@ -118,8 +117,8 @@ export default class Canvas extends React.Component { // if distance to target point is small if (distance <= 1) { // snap to the target point - this.xPos = targetPoint.x * this.GRID_SIZE; - this.yPos = targetPoint.y * this.GRID_SIZE; + this.xPos = targetPoint.x; + this.yPos = targetPoint.y; // set target to the next point in the array this.pointIndex+= 1; @@ -128,46 +127,54 @@ export default class Canvas extends React.Component { this.stopAnimation(); } } - ctx.fillStyle = 'black'; - ctx.fillRect(this.xPos, this.yPos, 20, 20); + this.drawRobot(ctx, this.xPos, this.yPos) // Request the next frame this.animationFrameId = requestAnimationFrame(this.animate); }; + drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number) { + ctx.beginPath(); // Begin a new path + + ctx.arc(x, y, 20, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) + + ctx.fillStyle = "black"; // Set the fill color + ctx.fill(); // Fill the circle + } + drawWalls(ctx: CanvasRenderingContext2D) { for (let i = 0; i < this.walls.length; i++) { // assumption is made that p1 is going to be the top left corner, might make some error checks for that later on - const p1 = this.walls[i].p1; - const p2 = this.walls[i].p2; - const width = (p2.x - p1.x) * this.GRID_SIZE + this.GRID_SIZE; - const height = (p2.y - p1.y) * this.GRID_SIZE + this.GRID_SIZE; + const wall: Polygon = this.walls[i]; + + ctx.beginPath(); + ctx.moveTo(wall[0].x, wall[0].y) + for (let j = 1; j < wall.length; j++) { + ctx.lineTo(wall[j].x, wall[j].y); + } + ctx.closePath(); - ctx.fillStyle = 'rgb(128, 128, 128)'; - ctx.fillRect(p1.x * this.GRID_SIZE, p1.y * this.GRID_SIZE, width, height); + ctx.fillStyle = "rgba(169, 169, 169, 0.5)"; // Set the fill color + ctx.fill(); // Fill the polygon + + ctx.strokeStyle = "rgb(53, 53, 53)"; // Set the stroke color + ctx.lineWidth = 2; // Set the border width + ctx.stroke(); // Stroke the polygon } } drawGrid(ctx: CanvasRenderingContext2D) { // Draw grid - ctx.strokeStyle = 'gray'; - ctx.lineWidth = 1; - - // Draw vertical lines - for (let x = 0; x <= this.CANVAS_WIDTH; x += this.GRID_SIZE) { - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, this.CANVAS_HEIGHT); - ctx.stroke(); - } + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, this.CANVAS_HEIGHT); + ctx.lineTo(this.CANVAS_WIDTH, this.CANVAS_HEIGHT); + ctx.lineTo(this.CANVAS_WIDTH, 0) + ctx.closePath() - // Draw horizontal lines - for (let y = 0; y <= this.CANVAS_HEIGHT; y += this.GRID_SIZE) { - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(this.CANVAS_WIDTH, y); - ctx.stroke(); - } + ctx.strokeStyle = 'gray'; + ctx.lineWidth = 3; + ctx.stroke() } public render() { diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 83c72d0c98..52edb4dd6a 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -26,10 +26,10 @@ class RobotMaze extends React.Component { } public render() { - const { context: { moduleContexts: { robot_minigame } } } = this.props.context; + const { context: { moduleContexts: { robot_minigame: {state} } } } = this.props.context; return ( - + ); } } @@ -42,6 +42,7 @@ export default { * @returns {boolean} */ toSpawn(context: DebuggerContext) { + console.log(context.context?.moduleContexts?.robot_minigame.state); return context.context?.moduleContexts?.robot_minigame.state.isInit; }, From 4decbb84265048d64f53203d861f9ba24837cd32 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Sat, 1 Mar 2025 23:24:18 +0800 Subject: [PATCH 04/46] fixing some error in eslint --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 775263e5f0..9a4b0ad602 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,7 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; -import typeImportsPlugin from './scripts/dist/typeimports.js'; +import typeImportsPlugin from './scripts/src/linting/typeimports.ts'; const todoTreeKeywordsWarning = ['TODO', 'TODOS', 'TODO WIP', 'FIXME', 'WIP']; const todoTreeKeywordsAll = [...todoTreeKeywordsWarning, 'NOTE', 'NOTES', 'LIST']; From 906716ad9bc7f32f39f30f3a2c9dc644adfae57c Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Sat, 1 Mar 2025 23:26:36 +0800 Subject: [PATCH 05/46] fixing some error in eslint 2 --- eslint.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 9a4b0ad602..634cc90633 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,7 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; -import typeImportsPlugin from './scripts/src/linting/typeimports.ts'; +import typeImportsPlugin from './scripts/dist/typeimports.js'; const todoTreeKeywordsWarning = ['TODO', 'TODOS', 'TODO WIP', 'FIXME', 'WIP']; const todoTreeKeywordsAll = [...todoTreeKeywordsWarning, 'NOTE', 'NOTES', 'LIST']; @@ -188,4 +188,4 @@ export default tseslint.config( 'jest/valid-describe-callback': 'off' } } -); +); \ No newline at end of file From 0d4b236bb938138fe2d9fc7e2e69d5cb5df68f2f Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Sat, 1 Mar 2025 23:32:16 +0800 Subject: [PATCH 06/46] formatted code --- src/bundles/robot_minigame/functions.ts | 97 ++++++++++++------------- src/tabs/RobotMaze/canvas.tsx | 25 +++---- src/tabs/RobotMaze/index.tsx | 2 +- 3 files changed, 58 insertions(+), 66 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index da6496009c..b7624a3a60 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -6,7 +6,6 @@ import context from 'js-slang/context'; type Point = {x: number, y: number}; -type Wall = {p1: Point, p2: Point}; type Polygon = Point[]; @@ -16,10 +15,10 @@ type StateData = { height: number, walls: Polygon[], movePoints: Point[], - message: String, + message: string, success: boolean, - messages: String[] -} + messages: string[] +}; type Robot = { x: number; // the top left corner @@ -27,9 +26,9 @@ type Robot = { dx: number; dy: number; radius: number -} +}; -let stateData: StateData = { +const stateData: StateData = { isInit: false, width: 500, height: 500, @@ -38,17 +37,17 @@ let stateData: StateData = { message: 'moved successfully', success: true, messages: [] -} +}; -let robot: Robot = { +const robot: Robot = { x: 25, // default start pos, puts it at the top left corner of canvas without colliding with the walls y: 25, dx: 1, dy: 0, - radius: 20 //give the robot a circular hitbox -} + radius: 20 // give the robot a circular hitbox +}; -let bounds: Point[] = [] +let bounds: Point[] = []; // default grid width and height is 25 context.moduleContexts.robot_minigame.state = stateData; @@ -78,7 +77,7 @@ export function init(width: number, height: number, posX: number, posY: number) {x: width, y: 0}, {x: width, y: height}, {x: 0, y: height} - ] + ]; } @@ -108,7 +107,7 @@ export function set_rect_wall(x: number, y: number, width: number, height: numbe {x: x + width, y: y}, {x: x+width, y: y+height}, {x: x, y:y+height} - ] + ]; stateData.walls.push(polygon); } @@ -116,13 +115,13 @@ export function set_rect_wall(x: number, y: number, width: number, height: numbe export function move_forward(): void { if (alrCollided()) return; - const collisionPoint: Point | null = raycast(stateData.walls) + const collisionPoint: Point | null = raycast(stateData.walls); if (collisionPoint !== null) { - let nextPoint: Point = { + const nextPoint: Point = { x: collisionPoint.x - robot.dx * (robot.radius + 5), y: collisionPoint.y - robot.dy * (robot.radius + 5) - } - + }; + robot.x = nextPoint.x; robot.y = nextPoint.y; stateData.movePoints.push(nextPoint); @@ -142,20 +141,20 @@ function raycast(polygons: Polygon[]): Point | null { let minDist = Infinity; for (const polygon of polygons) { - stateData.messages.push("checking polygon"); - const numVertices = polygon.length; - - for (let i = 0; i < numVertices; i++) { - const x1 = polygon[i].x, y1 = polygon[i].y; - const x2 = polygon[(i + 1) % numVertices].x, y2 = polygon[(i + 1) % numVertices].y; - - const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); - - if (intersection.collided && intersection.dist < minDist) { - minDist = intersection.dist - nearest = {x: intersection.x, y: intersection.y}; - } + stateData.messages.push('checking polygon'); + const numVertices = polygon.length; + + for (let i = 0; i < numVertices; i++) { + const x1 = polygon[i].x, y1 = polygon[i].y; + const x2 = polygon[(i + 1) % numVertices].x, y2 = polygon[(i + 1) % numVertices].y; + + const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); + + if (intersection.collided && intersection.dist < minDist) { + minDist = intersection.dist; + nearest = {x: intersection.x, y: intersection.y}; } + } } // if no collisions with obstacles, check the outer bounds of map @@ -167,7 +166,7 @@ function raycast(polygons: Polygon[]): Point | null { const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); if (intersection.collided && intersection.dist < minDist) { - minDist = intersection.dist + minDist = intersection.dist; nearest = {x: intersection.x, y: intersection.y}; } } @@ -176,37 +175,35 @@ function raycast(polygons: Polygon[]): Point | null { return nearest; // Closest intersection point } -//Determine if a ray and a line segment intersect, and if so, determine the collision point +// Determine if a ray and a line segment intersect, and if so, determine the collision point function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4){ - var denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); - var r; - var s; - var x; - var y; - var b = false; - - //If lines not collinear or parallel + const denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); + let r; + let s; + let x; + let y; + let b = false; + + // If lines not collinear or parallel if(denom != 0){ - //Intersection in ray "local" coordinates + // Intersection in ray "local" coordinates r = (((y1 - y3) * (x4 - x3)) - (x1 - x3) * (y4 - y3)) / denom; - //Intersection in segment "local" coordinates + // Intersection in segment "local" coordinates s = (((y1 - y3) * (x2 - x1)) - (x1 - x3) * (y2 - y1)) / denom; - //The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on - if (r >= 0) - { - //If point along the line segment - if (s >= 0 && s <= 1) - { + // The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on + if (r >= 0) { + // If point along the line segment + if (s >= 0 && s <= 1) { b = true; - //Get point coordinates (offset by r local units from start of ray) + // Get point coordinates (offset by r local units from start of ray) x = x1 + r * (x2 - x1); y = y1 + r * (y2 - y1); } } } - var p = {collided: b, x: x, y: y, dist: r}; + const p = {collided: b, x: x, y: y, dist: r}; return p; } diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx index 4f81e0126a..e7ca29c4ae 100644 --- a/src/tabs/RobotMaze/canvas.tsx +++ b/src/tabs/RobotMaze/canvas.tsx @@ -21,12 +21,7 @@ type Point = { y: number; }; -type Wall = { - p1: Point; - p2: Point -}; - -type Polygon = Point[] +type Polygon = Point[]; export default class Canvas extends React.Component { private canvasRef: React.RefObject; @@ -78,7 +73,7 @@ export default class Canvas extends React.Component { ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); - this.drawRobot(ctx, this.xPos, this.yPos) + this.drawRobot(ctx, this.xPos, this.yPos); }; startAnimation = () => { @@ -128,7 +123,7 @@ export default class Canvas extends React.Component { } } - this.drawRobot(ctx, this.xPos, this.yPos) + this.drawRobot(ctx, this.xPos, this.yPos); // Request the next frame this.animationFrameId = requestAnimationFrame(this.animate); }; @@ -138,7 +133,7 @@ export default class Canvas extends React.Component { ctx.arc(x, y, 20, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) - ctx.fillStyle = "black"; // Set the fill color + ctx.fillStyle = 'black'; // Set the fill color ctx.fill(); // Fill the circle } @@ -148,16 +143,16 @@ export default class Canvas extends React.Component { const wall: Polygon = this.walls[i]; ctx.beginPath(); - ctx.moveTo(wall[0].x, wall[0].y) + ctx.moveTo(wall[0].x, wall[0].y); for (let j = 1; j < wall.length; j++) { ctx.lineTo(wall[j].x, wall[j].y); } ctx.closePath(); - ctx.fillStyle = "rgba(169, 169, 169, 0.5)"; // Set the fill color + ctx.fillStyle = 'rgba(169, 169, 169, 0.5)'; // Set the fill color ctx.fill(); // Fill the polygon - ctx.strokeStyle = "rgb(53, 53, 53)"; // Set the stroke color + ctx.strokeStyle = 'rgb(53, 53, 53)'; // Set the stroke color ctx.lineWidth = 2; // Set the border width ctx.stroke(); // Stroke the polygon } @@ -169,12 +164,12 @@ export default class Canvas extends React.Component { ctx.moveTo(0, 0); ctx.lineTo(0, this.CANVAS_HEIGHT); ctx.lineTo(this.CANVAS_WIDTH, this.CANVAS_HEIGHT); - ctx.lineTo(this.CANVAS_WIDTH, 0) - ctx.closePath() + ctx.lineTo(this.CANVAS_WIDTH, 0); + ctx.closePath(); ctx.strokeStyle = 'gray'; ctx.lineWidth = 3; - ctx.stroke() + ctx.stroke(); } public render() { diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 52edb4dd6a..f4b759de77 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { type DebuggerContext } from '../../typings/type_helpers'; +import type { DebuggerContext } from '../../typings/type_helpers'; import Canvas from './canvas'; /** From fd5321de8f2d8a8c2141beb486f39745120ec3d0 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Tue, 18 Mar 2025 22:04:54 +0800 Subject: [PATCH 07/46] raycast fix --- src/bundles/robot_minigame/functions.ts | 217 ++++++++++++++++-------- src/bundles/robot_minigame/index.ts | 3 + 2 files changed, 151 insertions(+), 69 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index b7624a3a60..08f64c3a62 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -4,8 +4,15 @@ */ import context from 'js-slang/context'; +import { + accumulate, + head, + tail, + type List +} from 'js-slang/dist/stdlib/list'; type Point = {x: number, y: number}; +type Intersection = {x: number, y: number, dist: number} type Polygon = Point[]; @@ -17,7 +24,8 @@ type StateData = { movePoints: Point[], message: string, success: boolean, - messages: string[] + messages: string[], + rotations: Point[] }; type Robot = { @@ -36,7 +44,8 @@ const stateData: StateData = { movePoints: [], message: 'moved successfully', success: true, - messages: [] + messages: [], + rotations: [] }; const robot: Robot = { @@ -49,7 +58,6 @@ const robot: Robot = { let bounds: Point[] = []; -// default grid width and height is 25 context.moduleContexts.robot_minigame.state = stateData; export function set_pos(x: number, y: number): void { @@ -82,23 +90,59 @@ export function init(width: number, height: number, posX: number, posY: number) } export function turn_left() { - if (alrCollided()) return; + let currentAngle = Math.atan2(-robot.dy, robot.dx); - let currentAngle = Math.tan(robot.dy / robot.dx); - currentAngle -= Math.PI / 2; + currentAngle += Math.PI / 2; robot.dx = Math.cos(currentAngle); - robot.dy = Math.sin(currentAngle); + robot.dy = -Math.sin(currentAngle); + + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; + if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + + logCoordinates(); } export function turn_right() { - if (alrCollided()) return; + let currentAngle = Math.atan2(-robot.dy, robot.dx); - let currentAngle = Math.tan(robot.dy / robot.dx); - currentAngle += Math.PI / 2; + currentAngle -= Math.PI / 2; robot.dx = Math.cos(currentAngle); - robot.dy = Math.sin(currentAngle); + robot.dy = -Math.sin(currentAngle); + + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; + if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + + logCoordinates(); +} + +export function rotate_right(angle: number) { + let currentAngle = Math.atan2(-robot.dy, robot.dx); + + currentAngle -= angle; + + robot.dx = Math.cos(currentAngle); + robot.dy = -Math.sin(currentAngle); + + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; + if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + + logCoordinates(); +} + +export function rotate_left(angle: number) { + let currentAngle = Math.atan2(-robot.dy, robot.dx); + + currentAngle += angle; + + robot.dx = Math.cos(currentAngle); + robot.dy = -Math.sin(currentAngle); + + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; + if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + + logCoordinates(); } export function set_rect_wall(x: number, y: number, width: number, height: number) { @@ -112,20 +156,15 @@ export function set_rect_wall(x: number, y: number, width: number, height: numbe stateData.walls.push(polygon); } -export function move_forward(): void { - if (alrCollided()) return; - - const collisionPoint: Point | null = raycast(stateData.walls); - if (collisionPoint !== null) { - const nextPoint: Point = { - x: collisionPoint.x - robot.dx * (robot.radius + 5), - y: collisionPoint.y - robot.dy * (robot.radius + 5) - }; - - robot.x = nextPoint.x; - robot.y = nextPoint.y; - stateData.movePoints.push(nextPoint); +export function set_polygon_wall(vertices: List) { + const polygon: Polygon = [] + while (vertices != null) { + const p = head(vertices); + polygon.push({x: head(p), y: tail(p)}); + vertices = tail(vertices); } + + stateData.walls.push(polygon); } export function getX():number { @@ -136,47 +175,80 @@ export function getY():number { return robot.y; } -function raycast(polygons: Polygon[]): Point | null { +export function move_forward(): void { + if (alrCollided()) return; + + let distance = findCollision(); + distance = Math.max(distance - robot.radius - 5, 0) + + const nextPoint: Point = { + x: robot.x + distance * robot.dx, + y: robot.y + distance * robot.dy + } + + + robot.x = nextPoint.x; + robot.y = nextPoint.y; + stateData.movePoints.push(nextPoint); + stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); +} + +function findCollision(): number { let nearest: Point | null = null; - let minDist = Infinity; + let minDist: number = Infinity; + + for (const wall of stateData.walls) { + const intersection: Intersection | null = raycast(wall); + if (intersection !== null && intersection.dist < minDist) { + minDist = intersection.dist; + nearest = {x: intersection.x, y: intersection.y}; + } + } - for (const polygon of polygons) { - stateData.messages.push('checking polygon'); - const numVertices = polygon.length; + // check outer bounds as well + const intersection: Intersection | null = raycast(bounds); + if (intersection !== null && intersection.dist < minDist) { + minDist = intersection.dist; + nearest = {x: intersection.x, y: intersection.y}; + } + + return minDist === Infinity ? 0 : minDist; // Closest intersection point +} - for (let i = 0; i < numVertices; i++) { - const x1 = polygon[i].x, y1 = polygon[i].y; - const x2 = polygon[(i + 1) % numVertices].x, y2 = polygon[(i + 1) % numVertices].y; +function raycast(polygon: Polygon): Intersection | null { + let minDist = Infinity; + let nearest: Intersection | null = null; - const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); + for (let i = 0; i < polygon.length; i++) { + const x1 = polygon[i].x, y1 = polygon[i].y; + const x2 = polygon[(i + 1) % polygon.length].x, y2 = polygon[(i + 1) % polygon.length].y; - if (intersection.collided && intersection.dist < minDist) { - minDist = intersection.dist; - nearest = {x: intersection.x, y: intersection.y}; - } - } - } + const topX = robot.x - robot.radius * robot.dy; + const topY = robot.y - robot.radius * robot.dx; - // if no collisions with obstacles, check the outer bounds of map - if (nearest === null) { - for (let i = 0; i < bounds.length; i++) { - const x1 = bounds[i].x, y1 = bounds[i].y; - const x2 = bounds[(i + 1) % bounds.length].x, y2 = bounds[(i + 1) % bounds.length].y; + const bottomX = robot.x + robot.radius * robot.dy; + const bottomY = robot.y + robot.radius * robot.dx; - const intersection = getIntersection(robot.x, robot.y, robot.dx + robot.x, robot.dy + robot.y, x1, y1, x2, y2); + const raycast_sources: Point[] = [ + {x: robot.x, y: robot.y}, + {x: topX, y: topY}, + {x: bottomX, y: bottomY} + ] - if (intersection.collided && intersection.dist < minDist) { + for (const source of raycast_sources) { + const intersection = getIntersection(source.x, source.y, robot.dx + source.x, robot.dy + source.y, x1, y1, x2, y2); + if (intersection !== null && intersection.dist < minDist) { minDist = intersection.dist; - nearest = {x: intersection.x, y: intersection.y}; + nearest = intersection; } } } - - return nearest; // Closest intersection point + + return nearest; } // Determine if a ray and a line segment intersect, and if so, determine the collision point -function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4){ +function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): Intersection | null { const denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); let r; let s; @@ -184,29 +256,36 @@ function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4){ let y; let b = false; - // If lines not collinear or parallel - if(denom != 0){ - // Intersection in ray "local" coordinates - r = (((y1 - y3) * (x4 - x3)) - (x1 - x3) * (y4 - y3)) / denom; - - // Intersection in segment "local" coordinates - s = (((y1 - y3) * (x2 - x1)) - (x1 - x3) * (y2 - y1)) / denom; - - // The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on - if (r >= 0) { - // If point along the line segment - if (s >= 0 && s <= 1) { - b = true; - // Get point coordinates (offset by r local units from start of ray) - x = x1 + r * (x2 - x1); - y = y1 + r * (y2 - y1); - } + // If lines are collinear or parallel + if (denom === 0) return null; + + // Intersection in ray "local" coordinates + r = (((y1 - y3) * (x4 - x3)) - (x1 - x3) * (y4 - y3)) / denom; + + // Intersection in segment "local" coordinates + s = (((y1 - y3) * (x2 - x1)) - (x1 - x3) * (y2 - y1)) / denom; + + // The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on + if (r >= 0) { + // If point along the line segment + if (s >= 0 && s <= 1) { + b = true; + // Get point coordinates (offset by r local units from start of ray) + x = x1 + r * (x2 - x1); + y = y1 + r * (y2 - y1); } } - const p = {collided: b, x: x, y: y, dist: r}; - return p; + + if (!b) return null + + return {x: x, y: y, dist: r} } function alrCollided() { return !stateData.success; } + +// debug +function logCoordinates() { + stateData.messages.push(`x: ${robot.x}, y: ${robot.y}, dx: ${robot.dx}, dy: ${robot.dy}`) +} diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index c1bd9a6ca4..8ec6146aa6 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -12,9 +12,12 @@ export { init, set_pos, set_rect_wall, + set_polygon_wall, move_forward, turn_left, turn_right, + rotate_right, + rotate_left, getX, getY } from './functions'; From a7630f3e5930a89bbf617dbfaf6ae3aab30b0ab4 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 19 Mar 2025 09:14:31 +0800 Subject: [PATCH 08/46] commented and cleaned up code --- src/bundles/robot_minigame/functions.ts | 119 ++++++++++++++++-------- src/bundles/robot_minigame/index.ts | 2 + 2 files changed, 83 insertions(+), 38 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 08f64c3a62..5336ccdfcd 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -1,18 +1,17 @@ -/* -* Currently uses a grid based system, will upgrade to more fancy stuff later -* The movement is simulated, then a series of movement points is passed to the module context, which the frontend then uses to render -*/ - import context from 'js-slang/context'; import { - accumulate, head, tail, type List } from 'js-slang/dist/stdlib/list'; type Point = {x: number, y: number}; -type Intersection = {x: number, y: number, dist: number} +type PointWithRotation = {x: number, y: number, angle: number} + +type CommandData = { + type: String, + location: PointWithRotation +} type Polygon = Point[]; @@ -24,8 +23,7 @@ type StateData = { movePoints: Point[], message: string, success: boolean, - messages: string[], - rotations: Point[] + messages: string[] }; type Robot = { @@ -44,8 +42,7 @@ const stateData: StateData = { movePoints: [], message: 'moved successfully', success: true, - messages: [], - rotations: [] + messages: [] }; const robot: Robot = { @@ -58,6 +55,7 @@ const robot: Robot = { let bounds: Point[] = []; +// sets the context to the statedata obj, mostly for convenience so i dont have to type context.... everytime context.moduleContexts.robot_minigame.state = stateData; export function set_pos(x: number, y: number): void { @@ -73,11 +71,15 @@ export function set_height(height: number) { stateData.height = height; } +// condenses setting the width and height of map, and the initial position of robot in one call export function init(width: number, height: number, posX: number, posY: number) { + if (stateData.isInit) return; // dont allow init more than once + set_width(width); set_height(height); set_pos(posX, posY); - stateData.movePoints.push({x: posX, y: posY}); + + stateData.movePoints.push({x: posX, y: posY}); // push starting point to movepoints data stateData.isInit = true; bounds = [ @@ -86,7 +88,6 @@ export function init(width: number, height: number, posX: number, posY: number) {x: width, y: height}, {x: 0, y: height} ]; - } export function turn_left() { @@ -97,9 +98,11 @@ export function turn_left() { robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); + // prevent floating point issues if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + // debug log logCoordinates(); } @@ -114,6 +117,7 @@ export function turn_right() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + // debug log logCoordinates(); } @@ -124,10 +128,11 @@ export function rotate_right(angle: number) { robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); - + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + // debug log logCoordinates(); } @@ -145,6 +150,7 @@ export function rotate_left(angle: number) { logCoordinates(); } +// easily set up a rectangular wall using the x and y of the top left corner, and width/height export function set_rect_wall(x: number, y: number, width: number, height: number) { const polygon: Polygon = [ {x: x, y: y}, @@ -156,8 +162,11 @@ export function set_rect_wall(x: number, y: number, width: number, height: numbe stateData.walls.push(polygon); } +// creates irregularly shaped wall +// takes in a list of vertices as its argument export function set_polygon_wall(vertices: List) { const polygon: Polygon = [] + while (vertices != null) { const p = head(vertices); polygon.push({x: head(p), y: tail(p)}); @@ -175,10 +184,13 @@ export function getY():number { return robot.y; } -export function move_forward(): void { +// moves robot to the nearest wall +export function move_forward_to_wall(): void { if (alrCollided()) return; - let distance = findCollision(); + let distance = findMoveDistance(); // do the raycast, figure out how far the robot is from the nearest wall + + // a lil extra offset from wall distance = Math.max(distance - robot.radius - 5, 0) const nextPoint: Point = { @@ -186,49 +198,80 @@ export function move_forward(): void { y: robot.y + distance * robot.dy } - robot.x = nextPoint.x; robot.y = nextPoint.y; stateData.movePoints.push(nextPoint); + + // for debug stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); } -function findCollision(): number { - let nearest: Point | null = null; +// Moves forward by a small amount +export function move_forward(moveDist: number): void { + const nextPoint: Point = { + x: robot.x + moveDist * robot.dx, + y: robot.y + moveDist + robot.dy + } + + // need to check for collision with wall + + robot.x = nextPoint.x; + robot.y = nextPoint.y; + stateData.movePoints.push(nextPoint); + + logCoordinates(); +} + +export function sensor(): boolean { + return false; +} + +// returns the distance from the nearest wall +function findMoveDistance(): number { let minDist: number = Infinity; + // loop through all the walls for (const wall of stateData.walls) { - const intersection: Intersection | null = raycast(wall); - if (intersection !== null && intersection.dist < minDist) { - minDist = intersection.dist; - nearest = {x: intersection.x, y: intersection.y}; + const intersectionDist = raycast(wall); // do the raycast + + // if intersection is closer, update minDist + if (intersectionDist !== null && intersectionDist < minDist) { + minDist = intersectionDist; } } // check outer bounds as well - const intersection: Intersection | null = raycast(bounds); - if (intersection !== null && intersection.dist < minDist) { - minDist = intersection.dist; - nearest = {x: intersection.x, y: intersection.y}; + const intersectionDist = raycast(bounds); + if (intersectionDist !== null && intersectionDist < minDist) { + minDist = intersectionDist; } - return minDist === Infinity ? 0 : minDist; // Closest intersection point + // Closest intersection point + // By all rights, there should always be an intersection point since the robot is always within the bounds + // and the bounds should be a collision + // but something goes wrong, will just return 0 + return minDist === Infinity ? 0 : minDist; } -function raycast(polygon: Polygon): Intersection | null { +// does the raycast logic for one particular wall +// three rays are cast: one from the center, one from the top and one from the bottom. the minimum dist is returned +// return null if no collision +function raycast(polygon: Polygon): number | null { let minDist = Infinity; - let nearest: Intersection | null = null; for (let i = 0; i < polygon.length; i++) { + // wall line segment const x1 = polygon[i].x, y1 = polygon[i].y; const x2 = polygon[(i + 1) % polygon.length].x, y2 = polygon[(i + 1) % polygon.length].y; + // calculate the top and bottom coordinates of the robot const topX = robot.x - robot.radius * robot.dy; const topY = robot.y - robot.radius * robot.dx; const bottomX = robot.x + robot.radius * robot.dy; const bottomY = robot.y + robot.radius * robot.dx; + // raycast from 3 sources: top, middle, bottom const raycast_sources: Point[] = [ {x: robot.x, y: robot.y}, {x: topX, y: topY}, @@ -236,19 +279,19 @@ function raycast(polygon: Polygon): Intersection | null { ] for (const source of raycast_sources) { - const intersection = getIntersection(source.x, source.y, robot.dx + source.x, robot.dy + source.y, x1, y1, x2, y2); - if (intersection !== null && intersection.dist < minDist) { - minDist = intersection.dist; - nearest = intersection; + const intersectionDist = getIntersection(source.x, source.y, robot.dx + source.x, robot.dy + source.y, x1, y1, x2, y2); + if (intersectionDist !== null && intersectionDist < minDist) { + minDist = intersectionDist; } } } - return nearest; + return minDist === Infinity ? null : minDist; } // Determine if a ray and a line segment intersect, and if so, determine the collision point -function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): Intersection | null { +// returns null if there's no collision, or the distance to the line segment if collides +function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): number | null { const denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); let r; let s; @@ -276,9 +319,9 @@ function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): Intersection | null { } } - if (!b) return null + if (!b) return null; - return {x: x, y: y, dist: r} + return r; } function alrCollided() { diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 8ec6146aa6..f932028ba7 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -14,6 +14,8 @@ export { set_rect_wall, set_polygon_wall, move_forward, + sensor, + move_forward_to_wall, turn_left, turn_right, rotate_right, From d149702acebf965632387771ddaf7bcb5f06a07a Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 19 Mar 2025 09:16:49 +0800 Subject: [PATCH 09/46] commenting code --- src/bundles/robot_minigame/functions.ts | 50 ++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 5336ccdfcd..d1c31b7b34 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -6,12 +6,12 @@ import { } from 'js-slang/dist/stdlib/list'; type Point = {x: number, y: number}; -type PointWithRotation = {x: number, y: number, angle: number} +type PointWithRotation = {x: number, y: number, angle: number}; type CommandData = { - type: String, + type: string, location: PointWithRotation -} +}; type Polygon = Point[]; @@ -128,7 +128,7 @@ export function rotate_right(angle: number) { robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); - + if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; @@ -163,9 +163,9 @@ export function set_rect_wall(x: number, y: number, width: number, height: numbe } // creates irregularly shaped wall -// takes in a list of vertices as its argument +// takes in a list of vertices as its argument export function set_polygon_wall(vertices: List) { - const polygon: Polygon = [] + const polygon: Polygon = []; while (vertices != null) { const p = head(vertices); @@ -184,20 +184,20 @@ export function getY():number { return robot.y; } -// moves robot to the nearest wall +// moves robot to the nearest wall export function move_forward_to_wall(): void { if (alrCollided()) return; let distance = findMoveDistance(); // do the raycast, figure out how far the robot is from the nearest wall // a lil extra offset from wall - distance = Math.max(distance - robot.radius - 5, 0) - + distance = Math.max(distance - robot.radius - 5, 0); + const nextPoint: Point = { x: robot.x + distance * robot.dx, y: robot.y + distance * robot.dy - } - + }; + robot.x = nextPoint.x; robot.y = nextPoint.y; stateData.movePoints.push(nextPoint); @@ -206,12 +206,12 @@ export function move_forward_to_wall(): void { stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); } -// Moves forward by a small amount +// Moves forward by a small amount export function move_forward(moveDist: number): void { const nextPoint: Point = { x: robot.x + moveDist * robot.dx, y: robot.y + moveDist + robot.dy - } + }; // need to check for collision with wall @@ -226,31 +226,31 @@ export function sensor(): boolean { return false; } -// returns the distance from the nearest wall +// returns the distance from the nearest wall function findMoveDistance(): number { let minDist: number = Infinity; // loop through all the walls for (const wall of stateData.walls) { - const intersectionDist = raycast(wall); // do the raycast + const intersectionDist = raycast(wall); // do the raycast // if intersection is closer, update minDist if (intersectionDist !== null && intersectionDist < minDist) { minDist = intersectionDist; - } + } } // check outer bounds as well const intersectionDist = raycast(bounds); if (intersectionDist !== null && intersectionDist < minDist) { minDist = intersectionDist; - } - + } + // Closest intersection point - // By all rights, there should always be an intersection point since the robot is always within the bounds + // By all rights, there should always be an intersection point since the robot is always within the bounds // and the bounds should be a collision // but something goes wrong, will just return 0 - return minDist === Infinity ? 0 : minDist; + return minDist === Infinity ? 0 : minDist; } // does the raycast logic for one particular wall @@ -264,7 +264,7 @@ function raycast(polygon: Polygon): number | null { const x1 = polygon[i].x, y1 = polygon[i].y; const x2 = polygon[(i + 1) % polygon.length].x, y2 = polygon[(i + 1) % polygon.length].y; - // calculate the top and bottom coordinates of the robot + // calculate the top and bottom coordinates of the robot const topX = robot.x - robot.radius * robot.dy; const topY = robot.y - robot.radius * robot.dx; @@ -276,7 +276,7 @@ function raycast(polygon: Polygon): number | null { {x: robot.x, y: robot.y}, {x: topX, y: topY}, {x: bottomX, y: bottomY} - ] + ]; for (const source of raycast_sources) { const intersectionDist = getIntersection(source.x, source.y, robot.dx + source.x, robot.dy + source.y, x1, y1, x2, y2); @@ -285,7 +285,7 @@ function raycast(polygon: Polygon): number | null { } } } - + return minDist === Infinity ? null : minDist; } @@ -320,7 +320,7 @@ function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): number | null { } if (!b) return null; - + return r; } @@ -330,5 +330,5 @@ function alrCollided() { // debug function logCoordinates() { - stateData.messages.push(`x: ${robot.x}, y: ${robot.y}, dx: ${robot.dx}, dy: ${robot.dy}`) + stateData.messages.push(`x: ${robot.x}, y: ${robot.y}, dx: ${robot.dx}, dy: ${robot.dy}`); } From aef74d509cdc5e665aef043faf02b17e579bf49f Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 19 Mar 2025 16:06:41 +0800 Subject: [PATCH 10/46] updated to command system, includes move, rotate and sensor --- src/bundles/robot_minigame/functions.ts | 74 ++++++++++--- src/bundles/robot_minigame/index.ts | 1 + src/tabs/RobotMaze/canvas.tsx | 141 +++++++++++++++++++----- 3 files changed, 168 insertions(+), 48 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index d1c31b7b34..cdbab53620 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -6,11 +6,11 @@ import { } from 'js-slang/dist/stdlib/list'; type Point = {x: number, y: number}; -type PointWithRotation = {x: number, y: number, angle: number}; +type PointWithRotation = {x: number, y: number, rotation: number}; -type CommandData = { +type Command = { type: string, - location: PointWithRotation + position: PointWithRotation }; type Polygon = Point[]; @@ -20,10 +20,11 @@ type StateData = { width: number, height: number, walls: Polygon[], - movePoints: Point[], + moveCommands: Command[], message: string, success: boolean, - messages: string[] + messages: string[], + robotSize: number }; type Robot = { @@ -39,10 +40,11 @@ const stateData: StateData = { width: 500, height: 500, walls: [], - movePoints: [], + moveCommands: [], message: 'moved successfully', success: true, - messages: [] + messages: [], + robotSize: 15 }; const robot: Robot = { @@ -50,7 +52,7 @@ const robot: Robot = { y: 25, dx: 1, dy: 0, - radius: 20 // give the robot a circular hitbox + radius: 15 // give the robot a circular hitbox }; let bounds: Point[] = []; @@ -63,6 +65,12 @@ export function set_pos(x: number, y: number): void { robot.y = y; } +export function set_rotation(rotation: number) { + robot.dx = Math.cos(rotation); + robot.dy = -Math.sin(rotation); +} + + export function set_width(width: number) { stateData.width = width; } @@ -72,14 +80,15 @@ export function set_height(height: number) { } // condenses setting the width and height of map, and the initial position of robot in one call -export function init(width: number, height: number, posX: number, posY: number) { +export function init(width: number, height: number, posX: number, posY: number, rotation: number) { if (stateData.isInit) return; // dont allow init more than once set_width(width); set_height(height); set_pos(posX, posY); + set_rotation(rotation); - stateData.movePoints.push({x: posX, y: posY}); // push starting point to movepoints data + stateData.moveCommands.push({type: "begin", position: getPositionWithRotation()}); // push starting point to movepoints data stateData.isInit = true; bounds = [ @@ -102,10 +111,13 @@ export function turn_left() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + stateData.moveCommands.push({type: "rotateLeft", position: getPositionWithRotation()}); + // debug log logCoordinates(); } + export function turn_right() { let currentAngle = Math.atan2(-robot.dy, robot.dx); @@ -117,6 +129,8 @@ export function turn_right() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + stateData.moveCommands.push({type: "rotateRight", position: getPositionWithRotation()}); + // debug log logCoordinates(); } @@ -132,6 +146,8 @@ export function rotate_right(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + stateData.moveCommands.push({type: "rotateRight", position: getPositionWithRotation()}); + // debug log logCoordinates(); } @@ -147,6 +163,8 @@ export function rotate_left(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + stateData.moveCommands.push({type: "rotateLeft", position: getPositionWithRotation()}); + logCoordinates(); } @@ -188,7 +206,7 @@ export function getY():number { export function move_forward_to_wall(): void { if (alrCollided()) return; - let distance = findMoveDistance(); // do the raycast, figure out how far the robot is from the nearest wall + let distance = findDistanceToWall(); // do the raycast, figure out how far the robot is from the nearest wall // a lil extra offset from wall distance = Math.max(distance - robot.radius - 5, 0); @@ -200,34 +218,49 @@ export function move_forward_to_wall(): void { robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.movePoints.push(nextPoint); + stateData.moveCommands.push({type: "move", position: getPositionWithRotation()}); // for debug stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); } -// Moves forward by a small amount +// Moves forward by a specified amount export function move_forward(moveDist: number): void { + // need to check for collision with wall + const dist = findDistanceToWall(); + stateData.messages.push(`${dist}`); + + if (dist < moveDist + robot.radius) { + stateData.message = "collided"; + stateData.success = false; + moveDist = dist - robot.radius + 1; // move only until the wall + } + const nextPoint: Point = { x: robot.x + moveDist * robot.dx, - y: robot.y + moveDist + robot.dy + y: robot.y + moveDist * robot.dy }; - // need to check for collision with wall - robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.movePoints.push(nextPoint); + stateData.moveCommands.push({type: "move", position: getPositionWithRotation()}); logCoordinates(); } +// detects if there are walls 10 units ahead of the robot +// add as a command later export function sensor(): boolean { + const dist = findDistanceToWall(); + stateData.moveCommands.push({type: "sensor", position: getPositionWithRotation()}) + if (dist <= 10 + robot.radius) { + return true; + } return false; } // returns the distance from the nearest wall -function findMoveDistance(): number { +function findDistanceToWall(): number { let minDist: number = Infinity; // loop through all the walls @@ -328,6 +361,11 @@ function alrCollided() { return !stateData.success; } +function getPositionWithRotation(): PointWithRotation { + const angle = Math.atan2(-robot.dy, robot.dx); + return {x: robot.x, y: robot.y, rotation: angle} +} + // debug function logCoordinates() { stateData.messages.push(`x: ${robot.x}, y: ${robot.y}, dx: ${robot.dx}, dy: ${robot.dy}`); diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index f932028ba7..559eea0f56 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -11,6 +11,7 @@ export { init, set_pos, + set_rotation, set_rect_wall, set_polygon_wall, move_forward, diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx index e7ca29c4ae..c92ccab12b 100644 --- a/src/tabs/RobotMaze/canvas.tsx +++ b/src/tabs/RobotMaze/canvas.tsx @@ -21,21 +21,35 @@ type Point = { y: number; }; +type PointWithRotation = {x: number, y: number, rotation: number}; + +type Command = { + type: string, + position: PointWithRotation +}; + type Polygon = Point[]; export default class Canvas extends React.Component { private canvasRef: React.RefObject; private animationFrameId: number | null = null; private speed: number = 2; // Speed of the movement - private points: Point[]; + private commands: Command[]; + private xPos: number; private yPos: number; - private pointIndex: number = 1; + private rotation: number; + + private robotSize: number; + private commandIndex: number = 1; private walls: Polygon[]; private CANVAS_WIDTH: number = 500; private CANVAS_HEIGHT: number = 500; + private isPaused: boolean = false; + private unpauseTIme: number = 0; + constructor(props) { super(props); this.state = { @@ -45,10 +59,13 @@ export default class Canvas extends React.Component { // setting some variables in what may or may not be good practice this.CANVAS_WIDTH = this.props.state.width; this.CANVAS_HEIGHT = this.props.state.height; - this.points = this.props.state.movePoints; // a series of points is passed back from the modules which determines the path of robot + this.commands = this.props.state.moveCommands; // a series of points is passed back from the modules which determines the path of robot this.walls = this.props.state.walls; - this.xPos = this.points[0].x; - this.yPos = this.points[0].y; + + this.xPos = this.commands[0].position.x; + this.yPos = this.commands[0].position.y; + this.rotation = this.commands[0].position.rotation; + this.robotSize = this.props.state.robotSize; this.canvasRef = React.createRef(); } @@ -73,7 +90,7 @@ export default class Canvas extends React.Component { ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); - this.drawRobot(ctx, this.xPos, this.yPos); + this.drawRobot(ctx, this.xPos, this.yPos, this.rotation); }; startAnimation = () => { @@ -92,49 +109,114 @@ export default class Canvas extends React.Component { if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; - if (this.pointIndex >= this.points.length) return; + + if (this.commandIndex >= this.commands.length) return; + + // temporary pausing thing for the sensor + if (this.isPaused) { + if (Date.now() > this.unpauseTIme) { + this.isPaused = false; + this.commandIndex += 1; + } + this.animationFrameId = requestAnimationFrame(this.animate); + return; + } ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); this.drawWalls(ctx); this.drawGrid(ctx); - // Update position - const targetPoint = this.points[this.pointIndex]; - const dx = targetPoint.x - this.xPos; - const dy = targetPoint.y - this.yPos; - const distance = Math.sqrt(dx * dx + dy * dy); + // get command + const currentCommand: Command = this.commands[this.commandIndex]; - if (distance > 1) { - this.xPos += (dx / distance) * this.speed; - this.yPos += (dy / distance) * this.speed; - } + // checking for the 3 types of commands so far: move, rotate and sensor + if (currentCommand.type == "move") { + const targetPoint = {x: currentCommand.position.x, y: currentCommand.position.y}; + const dx = targetPoint.x - this.xPos; + const dy = targetPoint.y - this.yPos; + const distance = Math.sqrt(dx * dx + dy * dy); - // if distance to target point is small - if (distance <= 1) { - // snap to the target point - this.xPos = targetPoint.x; - this.yPos = targetPoint.y; + if (distance > 1) { + this.xPos += (dx / distance) * this.speed; + this.yPos += (dy / distance) * this.speed; + } - // set target to the next point in the array - this.pointIndex+= 1; + // if distance to target point is small + if (distance <= 1) { + // snap to the target point + this.xPos = targetPoint.x; + this.yPos = targetPoint.y; - if (this.pointIndex >= this.points.length) { - this.stopAnimation(); + // set target to the next point in the array + this.commandIndex+= 1; } + } + else if (currentCommand.type == "rotateLeft" || currentCommand.type == "rotateRight") { + const targetRotation = currentCommand.position.rotation; + if (currentCommand.type == "rotateLeft") { + this.rotation += 0.1; + } else { + this.rotation -= 0.1; + } + + if (Math.abs(this.rotation - targetRotation) <= 0.1) { + this.rotation = targetRotation; + this.commandIndex+= 1; + } else { + if (this.rotation > Math.PI) { + this.rotation -= 2 * Math.PI; + } + + if (this.rotation < -Math.PI) { + this.rotation += 2 * Math.PI; + } + + if (Math.abs(this.rotation - targetRotation) <= 0.1) { + this.rotation = targetRotation; + this.commandIndex+= 1; + } + } + } else if (currentCommand.type === "sensor") { + this.isPaused = true; + this.unpauseTIme = Date.now() + 500; + } + + if (this.commandIndex >= this.commands.length) { + this.stopAnimation(); } - this.drawRobot(ctx, this.xPos, this.yPos); + this.drawRobot(ctx, this.xPos, this.yPos, this.rotation); // Request the next frame this.animationFrameId = requestAnimationFrame(this.animate); }; - drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number) { - ctx.beginPath(); // Begin a new path + drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number, rotation: number) { + const centerX = x; + const centerY = y; + + ctx.save(); + + // translates the origin of the canvas to the center of the robot, then rotate + ctx.translate(centerX, centerY); + ctx.rotate(-rotation); + + ctx.beginPath(); // Begin drawing robot - ctx.arc(x, y, 20, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) + ctx.arc(0, 0, this.robotSize, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) ctx.fillStyle = 'black'; // Set the fill color ctx.fill(); // Fill the circle + ctx.closePath(); + + ctx.strokeStyle = "white"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(this.robotSize, 0); + ctx.lineTo(0, 0); + ctx.stroke(); + + // restore state of the background + ctx.restore(); } drawWalls(ctx: CanvasRenderingContext2D) { @@ -181,7 +263,6 @@ export default class Canvas extends React.Component { - ); } } From ae6e98c0b7194a71cb0f85ba8b5a5164455353f4 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 19 Mar 2025 16:08:37 +0800 Subject: [PATCH 11/46] formatting fix --- src/bundles/robot_minigame/functions.ts | 24 ++++++++++------------ src/tabs/RobotMaze/canvas.tsx | 27 ++++++++++++------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index cdbab53620..a432966476 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -70,7 +70,6 @@ export function set_rotation(rotation: number) { robot.dy = -Math.sin(rotation); } - export function set_width(width: number) { stateData.width = width; } @@ -88,7 +87,7 @@ export function init(width: number, height: number, posX: number, posY: number, set_pos(posX, posY); set_rotation(rotation); - stateData.moveCommands.push({type: "begin", position: getPositionWithRotation()}); // push starting point to movepoints data + stateData.moveCommands.push({type: 'begin', position: getPositionWithRotation()}); // push starting point to movepoints data stateData.isInit = true; bounds = [ @@ -111,13 +110,12 @@ export function turn_left() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: "rotateLeft", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); // debug log logCoordinates(); } - export function turn_right() { let currentAngle = Math.atan2(-robot.dy, robot.dx); @@ -129,7 +127,7 @@ export function turn_right() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: "rotateRight", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); // debug log logCoordinates(); @@ -146,7 +144,7 @@ export function rotate_right(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: "rotateRight", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); // debug log logCoordinates(); @@ -163,7 +161,7 @@ export function rotate_left(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: "rotateLeft", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); logCoordinates(); } @@ -218,7 +216,7 @@ export function move_forward_to_wall(): void { robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.moveCommands.push({type: "move", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); // for debug stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); @@ -229,9 +227,9 @@ export function move_forward(moveDist: number): void { // need to check for collision with wall const dist = findDistanceToWall(); stateData.messages.push(`${dist}`); - + if (dist < moveDist + robot.radius) { - stateData.message = "collided"; + stateData.message = 'collided'; stateData.success = false; moveDist = dist - robot.radius + 1; // move only until the wall } @@ -243,7 +241,7 @@ export function move_forward(moveDist: number): void { robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.moveCommands.push({type: "move", position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); logCoordinates(); } @@ -252,7 +250,7 @@ export function move_forward(moveDist: number): void { // add as a command later export function sensor(): boolean { const dist = findDistanceToWall(); - stateData.moveCommands.push({type: "sensor", position: getPositionWithRotation()}) + stateData.moveCommands.push({type: 'sensor', position: getPositionWithRotation()}); if (dist <= 10 + robot.radius) { return true; } @@ -363,7 +361,7 @@ function alrCollided() { function getPositionWithRotation(): PointWithRotation { const angle = Math.atan2(-robot.dy, robot.dx); - return {x: robot.x, y: robot.y, rotation: angle} + return {x: robot.x, y: robot.y, rotation: angle}; } // debug diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx index c92ccab12b..8be6e49438 100644 --- a/src/tabs/RobotMaze/canvas.tsx +++ b/src/tabs/RobotMaze/canvas.tsx @@ -130,7 +130,7 @@ export default class Canvas extends React.Component { const currentCommand: Command = this.commands[this.commandIndex]; // checking for the 3 types of commands so far: move, rotate and sensor - if (currentCommand.type == "move") { + if (currentCommand.type == 'move') { const targetPoint = {x: currentCommand.position.x, y: currentCommand.position.y}; const dx = targetPoint.x - this.xPos; const dy = targetPoint.y - this.yPos; @@ -148,15 +148,14 @@ export default class Canvas extends React.Component { this.yPos = targetPoint.y; // set target to the next point in the array - this.commandIndex+= 1; + this.commandIndex+= 1; } - } - else if (currentCommand.type == "rotateLeft" || currentCommand.type == "rotateRight") { + } else if (currentCommand.type == 'rotateLeft' || currentCommand.type == 'rotateRight') { const targetRotation = currentCommand.position.rotation; - if (currentCommand.type == "rotateLeft") { - this.rotation += 0.1; + if (currentCommand.type == 'rotateLeft') { + this.rotation += 0.1; } else { - this.rotation -= 0.1; + this.rotation -= 0.1; } if (Math.abs(this.rotation - targetRotation) <= 0.1) { @@ -166,21 +165,21 @@ export default class Canvas extends React.Component { if (this.rotation > Math.PI) { this.rotation -= 2 * Math.PI; } - + if (this.rotation < -Math.PI) { this.rotation += 2 * Math.PI; } - + if (Math.abs(this.rotation - targetRotation) <= 0.1) { this.rotation = targetRotation; this.commandIndex+= 1; } } - } else if (currentCommand.type === "sensor") { + } else if (currentCommand.type === 'sensor') { this.isPaused = true; this.unpauseTIme = Date.now() + 500; } - + if (this.commandIndex >= this.commands.length) { this.stopAnimation(); } @@ -193,7 +192,7 @@ export default class Canvas extends React.Component { drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number, rotation: number) { const centerX = x; const centerY = y; - + ctx.save(); // translates the origin of the canvas to the center of the robot, then rotate @@ -208,8 +207,8 @@ export default class Canvas extends React.Component { ctx.fill(); // Fill the circle ctx.closePath(); - ctx.strokeStyle = "white"; - ctx.lineWidth = 2; + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.robotSize, 0); ctx.lineTo(0, 0); From b532ff497e8b5431935cbbbda3fcd70297c499b5 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 20 Mar 2025 14:14:23 +0800 Subject: [PATCH 12/46] Add skeletons for required functions --- src/bundles/robot_minigame/functions.ts | 366 +++++++++++++++++------- 1 file changed, 255 insertions(+), 111 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index a432966476..0877f9282e 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -5,33 +5,48 @@ import { type List } from 'js-slang/dist/stdlib/list'; -type Point = {x: number, y: number}; -type PointWithRotation = {x: number, y: number, rotation: number}; +interface Point { + x: number + y: number +} + +interface PointWithRotation extends Point { + rotation: number +} -type Command = { - type: string, +interface Command { + type: string position: PointWithRotation -}; +} + +interface AreaFlags { + [name: string]: boolean +} -type Polygon = Point[]; +interface Area { + vertices: Point[] + isCollidable: boolean + flags: AreaFlags +} -type StateData = { - isInit: boolean, - width: number, - height: number, - walls: Polygon[], - moveCommands: Command[], - message: string, - success: boolean, - messages: string[], +interface StateData { + isInit: boolean + width: number + height: number + areas: Area[] + areasEntered: number[] + moveCommands: Command[] + message: string + success: boolean + messages: string[] robotSize: number }; -type Robot = { - x: number; // the top left corner - y: number; - dx: number; - dy: number; +interface Robot { + x: number // the top left corner + y: number + dx: number + dy: number radius: number }; @@ -39,7 +54,8 @@ const stateData: StateData = { isInit: false, width: 500, height: 500, - walls: [], + areas: [], + areasEntered: [], moveCommands: [], message: 'moved successfully', success: true, @@ -78,8 +94,27 @@ export function set_height(height: number) { stateData.height = height; } -// condenses setting the width and height of map, and the initial position of robot in one call -export function init(width: number, height: number, posX: number, posY: number, rotation: number) { +// ===== // +// SETUP // +// ===== // + +/** + * Initializes a new simulation with a map of size width * height + * Also sets the initial position an rotation of the robot + * + * @param width of the map + * @param height of the map + * @param posX initial X coordinate of the robot + * @param posY initial Y coordinate of the robot + * @param rotation initial rotation of the robot + */ +export function init( + width: number, + height: number, + posX: number, + posY: number, + rotation: number +) { if (stateData.isInit) return; // dont allow init more than once set_width(width); @@ -98,45 +133,204 @@ export function init(width: number, height: number, posX: number, posY: number, ]; } -export function turn_left() { +/** + * Creates a new area with the given vertices and flags + * + * @param vertices of the area + * @param isCollidable a boolean indicating if the area is a collidable obstacle or not + * @param flags any additional flags the area may have + */ +export function create_wall( + vertices: List, + isCollidable: boolean, + flags: AreaFlags +) { + // TO BE IMPLEMENTED +} + + + +/* REFACTOR / REIMPLEMENT >>> + +// easily set up a rectangular wall using the x and y of the top left corner, and width/height +export function set_rect_wall(x: number, y: number, width: number, height: number) { + const polygon: Polygon = [ + {x: x, y: y}, + {x: x + width, y: y}, + {x: x+width, y: y+height}, + {x: x, y:y+height} + ]; + + stateData.walls.push(polygon); +} + +// creates irregularly shaped wall +// takes in a list of vertices as its argument +export function set_polygon_wall(vertices: List) { + const polygon: Polygon = []; + + while (vertices != null) { + const p = head(vertices); + polygon.push({x: head(p), y: tail(p)}); + vertices = tail(vertices); + } + + stateData.walls.push(polygon); +} + +<<< REFACTOR / REIMPLEMENT */ + + + +// ======= // +// SENSORS // +// ======= // + +/** + * Get the distance to the closest collidable area + * + * @returns the distance to the closest obstacle + */ +export function get_distance() : number { + // TO BE IMPLEMENTED + return 0; +} + +/** + * Gets the flags of the area containing the point (x, y) + * + * @param x coordinate + * @param y coordinate + * @returns the flags of the area containing (x, y) + */ +export function get_flags( + x: number, + y: number +) : AreaFlags { + // TO BE IMPLEMENTED + return {}; +} + +/** + * Gets the color of the area under the robot + * + * @returns the color of the area under the robot + */ +export function get_color() : string { + // TO BE IMPLEMENTED + return ""; +} + + + +// ======= // +// ACTIONS // +// ======= // + +/** + * Move the robot forward by the specified distance + * + * @param distance to move forward + */ +export function move_forward(distance: number) { + // need to check for collision with wall + const dist = findDistanceToWall(); + stateData.messages.push(`${dist}`); + + if (dist < distance + robot.radius) { + stateData.message = 'collided'; + stateData.success = false; + distance = dist - robot.radius + 1; // move only until the wall + } + + const nextPoint: Point = { + x: robot.x + distance * robot.dx, + y: robot.y + distance * robot.dy + }; + + robot.x = nextPoint.x; + robot.y = nextPoint.y; + stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); + + logCoordinates(); +} + +// The distance from a wall a move_forward_to_wall() command will stop +const SAFE_DISTANCE_FROM_WALL : number = 5; + +/** + * Move the robot forward to within a predefined distance of the wall + */ +export function move_forward_to_wall() { + if (alrCollided()) return; + + let distance = findDistanceToWall(); // do the raycast, figure out how far the robot is from the nearest wall + + // a lil extra offset from wall + distance = Math.max(distance - robot.radius - SAFE_DISTANCE_FROM_WALL, 0); + + const nextPoint: Point = { + x: robot.x + distance * robot.dx, + y: robot.y + distance * robot.dy + }; + + robot.x = nextPoint.x; + robot.y = nextPoint.y; + stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); + + // for debug + stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); +} + +/** + * + * @param angle the angle (in radians) to rotate right + */ +export function rotate(angle: number) { let currentAngle = Math.atan2(-robot.dy, robot.dx); - currentAngle += Math.PI / 2; + currentAngle -= angle; robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); - // prevent floating point issues if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); // debug log logCoordinates(); } -export function turn_right() { +/** + * Turns the robot 90 degrees to the left + */ +export function turn_left() { let currentAngle = Math.atan2(-robot.dy, robot.dx); - currentAngle -= Math.PI / 2; + currentAngle += Math.PI / 2; robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); + // prevent floating point issues if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); + stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); // debug log logCoordinates(); } -export function rotate_right(angle: number) { +/** + * Turns the robot 90 degrees to the right + */ +export function turn_right() { let currentAngle = Math.atan2(-robot.dy, robot.dx); - currentAngle -= angle; + currentAngle -= Math.PI / 2; robot.dx = Math.cos(currentAngle); robot.dy = -Math.sin(currentAngle); @@ -150,6 +344,26 @@ export function rotate_right(angle: number) { logCoordinates(); } +// ======= // +// TESTING // +// ======= // + +/** + * + */ +export function areasEntered( + check : (area : Area[]) => void +) : boolean { + // TO BE IMPLEMENTED + return false; +} + + + +// ==================== // +// Unassigned / Helpers // +// ==================== // + export function rotate_left(angle: number) { let currentAngle = Math.atan2(-robot.dy, robot.dx); @@ -166,32 +380,6 @@ export function rotate_left(angle: number) { logCoordinates(); } -// easily set up a rectangular wall using the x and y of the top left corner, and width/height -export function set_rect_wall(x: number, y: number, width: number, height: number) { - const polygon: Polygon = [ - {x: x, y: y}, - {x: x + width, y: y}, - {x: x+width, y: y+height}, - {x: x, y:y+height} - ]; - - stateData.walls.push(polygon); -} - -// creates irregularly shaped wall -// takes in a list of vertices as its argument -export function set_polygon_wall(vertices: List) { - const polygon: Polygon = []; - - while (vertices != null) { - const p = head(vertices); - polygon.push({x: head(p), y: tail(p)}); - vertices = tail(vertices); - } - - stateData.walls.push(polygon); -} - export function getX():number { return robot.x; } @@ -200,52 +388,6 @@ export function getY():number { return robot.y; } -// moves robot to the nearest wall -export function move_forward_to_wall(): void { - if (alrCollided()) return; - - let distance = findDistanceToWall(); // do the raycast, figure out how far the robot is from the nearest wall - - // a lil extra offset from wall - distance = Math.max(distance - robot.radius - 5, 0); - - const nextPoint: Point = { - x: robot.x + distance * robot.dx, - y: robot.y + distance * robot.dy - }; - - robot.x = nextPoint.x; - robot.y = nextPoint.y; - stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); - - // for debug - stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); -} - -// Moves forward by a specified amount -export function move_forward(moveDist: number): void { - // need to check for collision with wall - const dist = findDistanceToWall(); - stateData.messages.push(`${dist}`); - - if (dist < moveDist + robot.radius) { - stateData.message = 'collided'; - stateData.success = false; - moveDist = dist - robot.radius + 1; // move only until the wall - } - - const nextPoint: Point = { - x: robot.x + moveDist * robot.dx, - y: robot.y + moveDist * robot.dy - }; - - robot.x = nextPoint.x; - robot.y = nextPoint.y; - stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); - - logCoordinates(); -} - // detects if there are walls 10 units ahead of the robot // add as a command later export function sensor(): boolean { @@ -262,8 +404,8 @@ function findDistanceToWall(): number { let minDist: number = Infinity; // loop through all the walls - for (const wall of stateData.walls) { - const intersectionDist = raycast(wall); // do the raycast + for (const wall of stateData.areas) { + const intersectionDist = raycast(wall.vertices); // do the raycast // if intersection is closer, update minDist if (intersectionDist !== null && intersectionDist < minDist) { @@ -284,16 +426,18 @@ function findDistanceToWall(): number { return minDist === Infinity ? 0 : minDist; } -// does the raycast logic for one particular wall +// does the raycast logic for one particular area // three rays are cast: one from the center, one from the top and one from the bottom. the minimum dist is returned // return null if no collision -function raycast(polygon: Polygon): number | null { +function raycast( + vertices: Point[] +): number | null { let minDist = Infinity; - for (let i = 0; i < polygon.length; i++) { + for (let i = 0; i < vertices.length; i++) { // wall line segment - const x1 = polygon[i].x, y1 = polygon[i].y; - const x2 = polygon[(i + 1) % polygon.length].x, y2 = polygon[(i + 1) % polygon.length].y; + const x1 = vertices[i].x, y1 = vertices[i].y; + const x2 = vertices[(i + 1) % vertices.length].x, y2 = vertices[(i + 1) % vertices.length].y; // calculate the top and bottom coordinates of the robot const topX = robot.x - robot.radius * robot.dy; From d799e800fbbc139dca4b3287283807f93ca34d3a Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 20 Mar 2025 14:20:18 +0800 Subject: [PATCH 13/46] Remove semicolons :( --- src/bundles/robot_minigame/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 0877f9282e..8e57b16544 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -40,7 +40,7 @@ interface StateData { success: boolean messages: string[] robotSize: number -}; +} interface Robot { x: number // the top left corner @@ -48,7 +48,7 @@ interface Robot { dx: number dy: number radius: number -}; +} const stateData: StateData = { isInit: false, From 0f0ff3fe231292cdde7ee1ecc8e3b7c75c9929d6 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 20 Mar 2025 14:35:02 +0800 Subject: [PATCH 14/46] Update robot-minigame index exports --- src/bundles/robot_minigame/functions.ts | 4 ++-- src/bundles/robot_minigame/index.ts | 18 ++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 8e57b16544..d40652aa23 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -140,7 +140,7 @@ export function init( * @param isCollidable a boolean indicating if the area is a collidable obstacle or not * @param flags any additional flags the area may have */ -export function create_wall( +export function create_area( vertices: List, isCollidable: boolean, flags: AreaFlags @@ -351,7 +351,7 @@ export function turn_right() { /** * */ -export function areasEntered( +export function enteredAreas( check : (area : Area[]) => void ) : boolean { // TO BE IMPLEMENTED diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 559eea0f56..b1b0d81f57 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -9,18 +9,8 @@ */ export { - init, - set_pos, - set_rotation, - set_rect_wall, - set_polygon_wall, - move_forward, - sensor, - move_forward_to_wall, - turn_left, - turn_right, - rotate_right, - rotate_left, - getX, - getY + init, create_area, + get_distance, get_flags, get_color, + move_forward, move_forward_to_wall, rotate, turn_left, turn_right, + enteredAreas } from './functions'; From e84a02a078f0d8bf98d5227a093fb2e7d52b4fb0 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 20 Mar 2025 20:48:50 +0800 Subject: [PATCH 15/46] commands -> actionLog; rewrote Canvas as React FC --- src/bundles/robot_minigame/functions.ts | 75 +++-- src/bundles/robot_minigame/index.ts | 2 +- src/tabs/RobotMaze/canvas.tsx | 267 ------------------ .../RobotMaze/components/RobotSimulation.tsx | 247 ++++++++++++++++ src/tabs/RobotMaze/index.tsx | 32 +-- 5 files changed, 313 insertions(+), 310 deletions(-) delete mode 100644 src/tabs/RobotMaze/canvas.tsx create mode 100644 src/tabs/RobotMaze/components/RobotSimulation.tsx diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index d40652aa23..9e4db0977b 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -1,20 +1,23 @@ -import context from 'js-slang/context'; +// import context from 'js-slang/context'; NOT ALLOWED import { head, tail, type List } from 'js-slang/dist/stdlib/list'; +// A point (x, y) interface Point { x: number y: number } -interface PointWithRotation extends Point { +// A point (x, y) with rotation +export interface PointWithRotation extends Point { rotation: number } -interface Command { +// A prior movement +export interface Action { type: string position: PointWithRotation } @@ -23,19 +26,19 @@ interface AreaFlags { [name: string]: boolean } -interface Area { +export interface Area { vertices: Point[] isCollidable: boolean flags: AreaFlags } -interface StateData { +export interface StateData { isInit: boolean width: number height: number areas: Area[] - areasEntered: number[] - moveCommands: Command[] + areaLog: number[] + actionLog: Action[] message: string success: boolean messages: string[] @@ -55,8 +58,8 @@ const stateData: StateData = { width: 500, height: 500, areas: [], - areasEntered: [], - moveCommands: [], + areaLog: [], + actionLog: [], message: 'moved successfully', success: true, messages: [], @@ -74,7 +77,7 @@ const robot: Robot = { let bounds: Point[] = []; // sets the context to the statedata obj, mostly for convenience so i dont have to type context.... everytime -context.moduleContexts.robot_minigame.state = stateData; +// context.moduleContexts.robot_minigame.state = stateData; export function set_pos(x: number, y: number): void { robot.x = x; @@ -122,7 +125,7 @@ export function init( set_pos(posX, posY); set_rotation(rotation); - stateData.moveCommands.push({type: 'begin', position: getPositionWithRotation()}); // push starting point to movepoints data + stateData.actionLog.push({type: 'begin', position: getPositionWithRotation()}); // push starting point to movepoints data stateData.isInit = true; bounds = [ @@ -143,15 +146,43 @@ export function init( export function create_area( vertices: List, isCollidable: boolean, - flags: AreaFlags + flags: AreaFlags = {} ) { - // TO BE IMPLEMENTED -} + // Parse vertices list into a points array + const points : Point[] = []; + while (vertices != null) { + const p = head(vertices); + points.push({x: head(p), y: tail(p)}); + vertices = tail(vertices); + } + // Store the new area + stateData.areas.push({ + vertices: points, + isCollidable, + flags + }); +} -/* REFACTOR / REIMPLEMENT >>> +/** + * Creates a new obstacle + * + * @param vertices: List + */ +export function create_obstacle( + vertices: List +) { + create_area(vertices, true); +} + +/** + * Creates a new rectangular, axis-aligned obstacle + * + * @param x top left corner of the + */ +/* // easily set up a rectangular wall using the x and y of the top left corner, and width/height export function set_rect_wall(x: number, y: number, width: number, height: number) { const polygon: Polygon = [ @@ -250,7 +281,7 @@ export function move_forward(distance: number) { robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'move', position: getPositionWithRotation()}); logCoordinates(); } @@ -276,7 +307,7 @@ export function move_forward_to_wall() { robot.x = nextPoint.x; robot.y = nextPoint.y; - stateData.moveCommands.push({type: 'move', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'move', position: getPositionWithRotation()}); // for debug stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); @@ -297,7 +328,7 @@ export function rotate(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'rotate', position: getPositionWithRotation()}); // debug log logCoordinates(); @@ -318,7 +349,7 @@ export function turn_left() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'rotateLeft', position: getPositionWithRotation()}); // debug log logCoordinates(); @@ -338,7 +369,7 @@ export function turn_right() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateRight', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'rotateRight', position: getPositionWithRotation()}); // debug log logCoordinates(); @@ -375,7 +406,7 @@ export function rotate_left(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.moveCommands.push({type: 'rotateLeft', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'rotateLeft', position: getPositionWithRotation()}); logCoordinates(); } @@ -392,7 +423,7 @@ export function getY():number { // add as a command later export function sensor(): boolean { const dist = findDistanceToWall(); - stateData.moveCommands.push({type: 'sensor', position: getPositionWithRotation()}); + stateData.actionLog.push({type: 'sensor', position: getPositionWithRotation()}); if (dist <= 10 + robot.radius) { return true; } diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index b1b0d81f57..962f979f7d 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -5,7 +5,7 @@ * * @module robot_minigame * @author Koh Wai Kei - * @author Author Name + * @author Justin Cheng */ export { diff --git a/src/tabs/RobotMaze/canvas.tsx b/src/tabs/RobotMaze/canvas.tsx deleted file mode 100644 index 8be6e49438..0000000000 --- a/src/tabs/RobotMaze/canvas.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import React from 'react'; - -/** - * React Component props for the Tab. - */ -type Props = { - children?: never; - className?: never; - state?: any; -}; - -/** - * React Component state for the Tab. - */ -type State = { - isAnimationEnded: boolean; -}; - -type Point = { - x: number; - y: number; -}; - -type PointWithRotation = {x: number, y: number, rotation: number}; - -type Command = { - type: string, - position: PointWithRotation -}; - -type Polygon = Point[]; - -export default class Canvas extends React.Component { - private canvasRef: React.RefObject; - private animationFrameId: number | null = null; - private speed: number = 2; // Speed of the movement - private commands: Command[]; - - private xPos: number; - private yPos: number; - private rotation: number; - - private robotSize: number; - private commandIndex: number = 1; - private walls: Polygon[]; - - private CANVAS_WIDTH: number = 500; - private CANVAS_HEIGHT: number = 500; - - private isPaused: boolean = false; - private unpauseTIme: number = 0; - - constructor(props) { - super(props); - this.state = { - isAnimationEnded: false - }; - - // setting some variables in what may or may not be good practice - this.CANVAS_WIDTH = this.props.state.width; - this.CANVAS_HEIGHT = this.props.state.height; - this.commands = this.props.state.moveCommands; // a series of points is passed back from the modules which determines the path of robot - this.walls = this.props.state.walls; - - this.xPos = this.commands[0].position.x; - this.yPos = this.commands[0].position.y; - this.rotation = this.commands[0].position.rotation; - this.robotSize = this.props.state.robotSize; - - this.canvasRef = React.createRef(); - } - - componentDidMount() { - this.setupCanvas(); - } - - componentWillUnmount() { - this.stopAnimation(); - } - - setupCanvas = () => { - const canvas = this.canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - canvas.width = this.CANVAS_WIDTH; - canvas.height = this.CANVAS_HEIGHT; - - ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); - this.drawWalls(ctx); - this.drawGrid(ctx); - this.drawRobot(ctx, this.xPos, this.yPos, this.rotation); - }; - - startAnimation = () => { - this.animationFrameId = requestAnimationFrame(this.animate); - }; - - stopAnimation = () => { - if (this.animationFrameId) { - this.setState({isAnimationEnded: true}); - cancelAnimationFrame(this.animationFrameId); - } - }; - - animate = () => { - const canvas = this.canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - if (this.commandIndex >= this.commands.length) return; - - // temporary pausing thing for the sensor - if (this.isPaused) { - if (Date.now() > this.unpauseTIme) { - this.isPaused = false; - this.commandIndex += 1; - } - this.animationFrameId = requestAnimationFrame(this.animate); - return; - } - - ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); - this.drawWalls(ctx); - this.drawGrid(ctx); - - // get command - const currentCommand: Command = this.commands[this.commandIndex]; - - // checking for the 3 types of commands so far: move, rotate and sensor - if (currentCommand.type == 'move') { - const targetPoint = {x: currentCommand.position.x, y: currentCommand.position.y}; - const dx = targetPoint.x - this.xPos; - const dy = targetPoint.y - this.yPos; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance > 1) { - this.xPos += (dx / distance) * this.speed; - this.yPos += (dy / distance) * this.speed; - } - - // if distance to target point is small - if (distance <= 1) { - // snap to the target point - this.xPos = targetPoint.x; - this.yPos = targetPoint.y; - - // set target to the next point in the array - this.commandIndex+= 1; - } - } else if (currentCommand.type == 'rotateLeft' || currentCommand.type == 'rotateRight') { - const targetRotation = currentCommand.position.rotation; - if (currentCommand.type == 'rotateLeft') { - this.rotation += 0.1; - } else { - this.rotation -= 0.1; - } - - if (Math.abs(this.rotation - targetRotation) <= 0.1) { - this.rotation = targetRotation; - this.commandIndex+= 1; - } else { - if (this.rotation > Math.PI) { - this.rotation -= 2 * Math.PI; - } - - if (this.rotation < -Math.PI) { - this.rotation += 2 * Math.PI; - } - - if (Math.abs(this.rotation - targetRotation) <= 0.1) { - this.rotation = targetRotation; - this.commandIndex+= 1; - } - } - } else if (currentCommand.type === 'sensor') { - this.isPaused = true; - this.unpauseTIme = Date.now() + 500; - } - - if (this.commandIndex >= this.commands.length) { - this.stopAnimation(); - } - - this.drawRobot(ctx, this.xPos, this.yPos, this.rotation); - // Request the next frame - this.animationFrameId = requestAnimationFrame(this.animate); - }; - - drawRobot(ctx: CanvasRenderingContext2D, x: number, y: number, rotation: number) { - const centerX = x; - const centerY = y; - - ctx.save(); - - // translates the origin of the canvas to the center of the robot, then rotate - ctx.translate(centerX, centerY); - ctx.rotate(-rotation); - - ctx.beginPath(); // Begin drawing robot - - ctx.arc(0, 0, this.robotSize, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) - - ctx.fillStyle = 'black'; // Set the fill color - ctx.fill(); // Fill the circle - ctx.closePath(); - - ctx.strokeStyle = 'white'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(this.robotSize, 0); - ctx.lineTo(0, 0); - ctx.stroke(); - - // restore state of the background - ctx.restore(); - } - - drawWalls(ctx: CanvasRenderingContext2D) { - for (let i = 0; i < this.walls.length; i++) { - // assumption is made that p1 is going to be the top left corner, might make some error checks for that later on - const wall: Polygon = this.walls[i]; - - ctx.beginPath(); - ctx.moveTo(wall[0].x, wall[0].y); - for (let j = 1; j < wall.length; j++) { - ctx.lineTo(wall[j].x, wall[j].y); - } - ctx.closePath(); - - ctx.fillStyle = 'rgba(169, 169, 169, 0.5)'; // Set the fill color - ctx.fill(); // Fill the polygon - - ctx.strokeStyle = 'rgb(53, 53, 53)'; // Set the stroke color - ctx.lineWidth = 2; // Set the border width - ctx.stroke(); // Stroke the polygon - } - } - - drawGrid(ctx: CanvasRenderingContext2D) { - // Draw grid - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(0, this.CANVAS_HEIGHT); - ctx.lineTo(this.CANVAS_WIDTH, this.CANVAS_HEIGHT); - ctx.lineTo(this.CANVAS_WIDTH, 0); - ctx.closePath(); - - ctx.strokeStyle = 'gray'; - ctx.lineWidth = 3; - ctx.stroke(); - } - - public render() { - return ( - <> - -

{this.state.isAnimationEnded ? this.props.state.message : <>}

-
- -
- - ); - } -} diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx new file mode 100644 index 0000000000..6dce14e571 --- /dev/null +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -0,0 +1,247 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + type Area, type Action, type PointWithRotation, type StateData +} from '../../../bundles/robot_minigame/functions'; + +/** + * Draw the borders of the map + * @param ctx for the canvas to draw on + * @param width of the map + * @param height of the map + */ +const drawBorders = (ctx: CanvasRenderingContext2D, width: number, height: number) => { + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, height); + ctx.lineTo(width, height); + ctx.lineTo(width, 0); + ctx.closePath(); + + ctx.strokeStyle = 'gray'; + ctx.lineWidth = 3; + ctx.stroke(); +} + +// Draw the areas of the map +const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { + for (const { vertices } of areas) { + ctx.beginPath(); + ctx.moveTo(vertices[0].x, vertices[0].y); + for (const vertex of vertices.slice(1)) { + ctx.lineTo(vertex.x, vertex.y); + } + ctx.closePath(); + + ctx.fillStyle = 'rgba(169, 169, 169, 0.5)'; // Set the fill color + ctx.fill(); // Fill the polygon + + ctx.strokeStyle = 'rgb(53, 53, 53)'; // Set the stroke color + ctx.lineWidth = 2; // Set the border width + ctx.stroke(); // Stroke the polygon + } +} + +// Draw the robot +const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWithRotation, size: number) => { + const centerX = x; + const centerY = y; + + ctx.save(); + + // translates the origin of the canvas to the center of the robot, then rotate + ctx.translate(centerX, centerY); + ctx.rotate(-rotation); + + ctx.beginPath(); // Begin drawing robot + + ctx.arc(0, 0, size, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) + + ctx.fillStyle = 'black'; // Set the fill color + ctx.fill(); // Fill the circle + ctx.closePath(); + + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(size, 0); + ctx.lineTo(0, 0); + ctx.stroke(); + + // restore state of the background + ctx.restore(); +} + +// The speed to move at +const ANIMATION_SPEED : number = 2; + +/** + * React Component props for the Tab. + */ +interface MapProps { + children?: never + className?: never + state: StateData +}; + +const RobotSimulation : React.FC = ({ + state: { + isInit, + width, + height, + areas, + // areaLog, + actionLog, + message, + // success, + // messages, + robotSize + } +}) => { + // Store the robot status + const [x, setX] = useState(actionLog[0].position.x); + const [y, setY] = useState(actionLog[0].position.y); + const [rotation, setRotation] = useState(actionLog[0].position.rotation); + + // Whether the animation has ended + const [isAnimationEnded, setAnimationEnded] = useState(false); + + // Whether the animation is paused + const [animationUnpauseTime, setAnimationUnpauseTime] = useState(-1); + + // Store the animation frame + const [animationFrame, setAnimationFrame] = useState(); + + // Store the next animation + const [currentAction, setCurrentAction] = useState(1); + + // Select the next action + const nextAction = () => setCurrentAction(a => a + 1); + + // Animate the next frame + const nextFrame = () => setAnimationFrame(requestAnimationFrame(animate)); + + // Run the animation + const animate = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + if (currentAction >= actionLog.length) return; + + // temporary pausing thing for the sensor + if (animationUnpauseTime < 0) { + if (Date.now() > animationUnpauseTime) { + setAnimationUnpauseTime(-1); + nextAction(); + } + return nextFrame(); + } + + // Draw the map + ctx.reset(); + drawBorders(ctx, width, height); + drawAreas(ctx, areas); + + // Get current action + const { type, position }: Action = actionLog[currentAction]; + + switch(type) { + case 'move': + const targetPoint = {x: position.x, y: position.y}; + const dx = targetPoint.x - x; + const dy = targetPoint.y - y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 1) { + setX(v => v + (dx / distance) * ANIMATION_SPEED); + setY(v => v + (dy / distance) * ANIMATION_SPEED); + } + + // if distance to target point is small + if (distance <= 1) { + // snap to the target point + setX(targetPoint.x); + setY(targetPoint.y); + + // set target to the next point in the array + nextAction(); + } + break; + case 'rotate': + const targetRotation = position.rotation; + setRotation(v => v - 0.1); + + if (Math.abs(rotation - targetRotation) <= 0.1) { + setRotation(targetRotation); + nextAction(); + } else { + if (rotation > Math.PI) { + setRotation(v => v - 2 * Math.PI); + } + + if (rotation < -Math.PI) { + setRotation(v => v + 2 * Math.PI); + } + + if (Math.abs(rotation - targetRotation) <= 0.1) { + setRotation(targetRotation); + nextAction(); + } + } + break; + case 'sensor': + setAnimationUnpauseTime(Date.now() + 500); + break; + } + + if (currentAction >= actionLog.length) { + stopAnimation(); + } + + drawRobot(ctx, {x, y, rotation}, robotSize); + + // Request the next frame + nextFrame(); + }; + + // Stop the current animation + const stopAnimation = () => { + if (animationFrame) { + setAnimationEnded(true); + cancelAnimationFrame(animationFrame); + } + } + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + canvas.width = width; + canvas.height = height; + + ctx.reset(); + drawBorders(ctx, width, height); + drawAreas(ctx, areas); + drawRobot(ctx, {x, y, rotation}, robotSize); + + return stopAnimation; + }, []); + + // Store a reference to the HTML canvas + const canvasRef = useRef(null); + + return ( + <> + +

{isAnimationEnded ? message : <>}

+
+ +
+ + ); +} + +export default RobotSimulation; \ No newline at end of file diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index f4b759de77..1bdf50443e 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,37 +1,29 @@ import React from 'react'; import type { DebuggerContext } from '../../typings/type_helpers'; -import Canvas from './canvas'; +import RobotSimulation from './components/RobotSimulation'; /** - * - * @author - * @author + * Renders the robot minigame in the assessment workspace + * @author Koh Wai Kei + * @author Justin Cheng */ /** * React Component props for the Tab. */ -type Props = { - children?: never; - className?: never; - context?: any; +interface MainProps { + children?: never + className?: never + context?: DebuggerContext }; /** * The main React Component of the Tab. */ -class RobotMaze extends React.Component { - constructor(props) { - super(props); - } - - public render() { - const { context: { moduleContexts: { robot_minigame: {state} } } } = this.props.context; - - return ( - - ); - } +const RobotMaze : React.FC = ({ context }) => { + return ( + + ); } export default { From 700084097b08215566e8ea0615f185b2d35b9635 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Fri, 21 Mar 2025 10:48:36 +0800 Subject: [PATCH 16/46] Fix animation to use refs instead --- src/bundles/robot_minigame/functions.ts | 109 +++----- src/bundles/robot_minigame/index.ts | 2 +- .../RobotMaze/components/RobotSimulation.tsx | 262 ++++++++++-------- src/tabs/RobotMaze/index.tsx | 4 +- 4 files changed, 189 insertions(+), 188 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 9e4db0977b..a03d9b273f 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -1,4 +1,4 @@ -// import context from 'js-slang/context'; NOT ALLOWED +import context from 'js-slang/context'; import { head, tail, @@ -77,23 +77,23 @@ const robot: Robot = { let bounds: Point[] = []; // sets the context to the statedata obj, mostly for convenience so i dont have to type context.... everytime -// context.moduleContexts.robot_minigame.state = stateData; +context.moduleContexts.robot_minigame.state = stateData; -export function set_pos(x: number, y: number): void { +function set_pos(x: number, y: number): void { robot.x = x; robot.y = y; } -export function set_rotation(rotation: number) { +function set_rotation(rotation: number) { robot.dx = Math.cos(rotation); robot.dy = -Math.sin(rotation); } -export function set_width(width: number) { +function set_width(width: number) { stateData.width = width; } -export function set_height(height: number) { +function set_height(height: number) { stateData.height = height; } @@ -104,13 +104,13 @@ export function set_height(height: number) { /** * Initializes a new simulation with a map of size width * height * Also sets the initial position an rotation of the robot - * + * * @param width of the map * @param height of the map * @param posX initial X coordinate of the robot * @param posY initial Y coordinate of the robot * @param rotation initial rotation of the robot - */ + */ export function init( width: number, height: number, @@ -138,28 +138,28 @@ export function init( /** * Creates a new area with the given vertices and flags - * + * * @param vertices of the area * @param isCollidable a boolean indicating if the area is a collidable obstacle or not * @param flags any additional flags the area may have */ export function create_area( - vertices: List, + vertices: Point[], isCollidable: boolean, flags: AreaFlags = {} ) { - // Parse vertices list into a points array - const points : Point[] = []; + // // Parse vertices list into a points array + // const points : Point[] = []; - while (vertices != null) { - const p = head(vertices); - points.push({x: head(p), y: tail(p)}); - vertices = tail(vertices); - } + // while (vertices != null) { + // const p = head(vertices); + // points.push({x: head(p), y: tail(p)}); + // vertices = tail(vertices); + // } // Store the new area stateData.areas.push({ - vertices: points, + vertices, isCollidable, flags }); @@ -167,59 +167,44 @@ export function create_area( /** * Creates a new obstacle - * - * @param vertices: List + * + * @param vertices of the obstacle */ export function create_obstacle( - vertices: List + vertices: Point[] ) { create_area(vertices, true); } /** * Creates a new rectangular, axis-aligned obstacle - * - * @param x top left corner of the + * + * @param x top left corner of the rectangle + * @param y top right corner of the rectangle + * @param width of the rectangle + * @param height of the rectangle */ - -/* -// easily set up a rectangular wall using the x and y of the top left corner, and width/height -export function set_rect_wall(x: number, y: number, width: number, height: number) { - const polygon: Polygon = [ - {x: x, y: y}, - {x: x + width, y: y}, - {x: x+width, y: y+height}, - {x: x, y:y+height} - ]; - - stateData.walls.push(polygon); -} - -// creates irregularly shaped wall -// takes in a list of vertices as its argument -export function set_polygon_wall(vertices: List) { - const polygon: Polygon = []; - - while (vertices != null) { - const p = head(vertices); - polygon.push({x: head(p), y: tail(p)}); - vertices = tail(vertices); - } - - stateData.walls.push(polygon); +export function create_rect_obstacle( + x: number, + y: number, + width: number, + height: number +) { + create_obstacle([ + {x, y}, + {x: x + width, y}, + {x: x + width, y: y + height}, + {x, y:y + height} + ]); } -<<< REFACTOR / REIMPLEMENT */ - - - // ======= // // SENSORS // // ======= // /** * Get the distance to the closest collidable area - * + * * @returns the distance to the closest obstacle */ export function get_distance() : number { @@ -229,7 +214,7 @@ export function get_distance() : number { /** * Gets the flags of the area containing the point (x, y) - * + * * @param x coordinate * @param y coordinate * @returns the flags of the area containing (x, y) @@ -244,23 +229,21 @@ export function get_flags( /** * Gets the color of the area under the robot - * + * * @returns the color of the area under the robot */ export function get_color() : string { // TO BE IMPLEMENTED - return ""; + return ''; } - - // ======= // // ACTIONS // // ======= // /** * Move the robot forward by the specified distance - * + * * @param distance to move forward */ export function move_forward(distance: number) { @@ -314,7 +297,7 @@ export function move_forward_to_wall() { } /** - * + * * @param angle the angle (in radians) to rotate right */ export function rotate(angle: number) { @@ -380,7 +363,7 @@ export function turn_right() { // ======= // /** - * + * */ export function enteredAreas( check : (area : Area[]) => void @@ -389,8 +372,6 @@ export function enteredAreas( return false; } - - // ==================== // // Unassigned / Helpers // // ==================== // diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 962f979f7d..653142b74b 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -9,7 +9,7 @@ */ export { - init, create_area, + init, create_area, create_obstacle, create_rect_obstacle, get_distance, get_flags, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, enteredAreas diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 6dce14e571..fafdfd043b 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,7 +1,22 @@ import React, { useEffect, useRef, useState } from 'react'; -import { - type Area, type Action, type PointWithRotation, type StateData -} from '../../../bundles/robot_minigame/functions'; +import type { Area, Action, PointWithRotation, StateData } from '../../../bundles/robot_minigame/functions'; + +/** + * Calculate the acute angle between 2 angles + * + * @param target rotation + * @param current rotation + * @returns the acute angle between + */ +const smallestAngle = (target, current) => { + const dr = (target - current) % (2 * Math.PI); + + if (dr > 0 && dr > Math.PI) return dr - (2 * Math.PI); + + if (dr < 0 && dr < -Math.PI) return dr + (2 * Math.PI); + + return dr; +}; /** * Draw the borders of the map @@ -20,7 +35,7 @@ const drawBorders = (ctx: CanvasRenderingContext2D, width: number, height: numbe ctx.strokeStyle = 'gray'; ctx.lineWidth = 3; ctx.stroke(); -} +}; // Draw the areas of the map const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { @@ -39,7 +54,7 @@ const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { ctx.lineWidth = 2; // Set the border width ctx.stroke(); // Stroke the polygon } -} +}; // Draw the robot const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWithRotation, size: number) => { @@ -69,6 +84,29 @@ const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWith // restore state of the background ctx.restore(); +}; + +/** + * Render the current game state + */ +const drawAll = ( + ctx : CanvasRenderingContext2D, + width : number, + height : number, + areas: Area[], + {x, y, rotation} : Robot, + robotSize: number +) => { + ctx.reset(); + drawBorders(ctx, width, height); + drawAreas(ctx, areas); + drawRobot(ctx, {x, y, rotation}, robotSize); +}; + +interface Robot { + x: number + y: number + rotation: number } // The speed to move at @@ -81,11 +119,11 @@ interface MapProps { children?: never className?: never state: StateData -}; +} const RobotSimulation : React.FC = ({ state: { - isInit, + // isInit, width, height, areas, @@ -97,151 +135,133 @@ const RobotSimulation : React.FC = ({ robotSize } }) => { - // Store the robot status - const [x, setX] = useState(actionLog[0].position.x); - const [y, setY] = useState(actionLog[0].position.y); - const [rotation, setRotation] = useState(actionLog[0].position.rotation); - - // Whether the animation has ended - const [isAnimationEnded, setAnimationEnded] = useState(false); + // Store animation status + // 0 => Loaded / Loading + // 1 => Running + // 2 => Paused + // 3 => Finished + const [animationStatus, setAnimationStatus] = useState<0 | 1 | 2 | 3>(0); - // Whether the animation is paused - const [animationUnpauseTime, setAnimationUnpauseTime] = useState(-1); + // Store animation pause + const animationPauseUntil = useRef(null); - // Store the animation frame - const [animationFrame, setAnimationFrame] = useState(); + // Store current action id + const currentAction = useRef(1); - // Store the next animation - const [currentAction, setCurrentAction] = useState(1); + // Store robot status + const robot = useRef({x: 0, y: 0, rotation: 0}); - // Select the next action - const nextAction = () => setCurrentAction(a => a + 1); - - // Animate the next frame - const nextFrame = () => setAnimationFrame(requestAnimationFrame(animate)); + // Ensure canvas is preloaded correctly + useEffect(() => { + // Only trigger if animationStatus is 0 + if (animationStatus !== 0) return; - // Run the animation - const animate = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; - if (currentAction >= actionLog.length) return; + robot.current = actionLog[0].position; - // temporary pausing thing for the sensor - if (animationUnpauseTime < 0) { - if (Date.now() > animationUnpauseTime) { - setAnimationUnpauseTime(-1); - nextAction(); - } - return nextFrame(); - } + canvas.width = width; + canvas.height = height; - // Draw the map - ctx.reset(); - drawBorders(ctx, width, height); - drawAreas(ctx, areas); - - // Get current action - const { type, position }: Action = actionLog[currentAction]; - - switch(type) { - case 'move': - const targetPoint = {x: position.x, y: position.y}; - const dx = targetPoint.x - x; - const dy = targetPoint.y - y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance > 1) { - setX(v => v + (dx / distance) * ANIMATION_SPEED); - setY(v => v + (dy / distance) * ANIMATION_SPEED); - } - - // if distance to target point is small - if (distance <= 1) { - // snap to the target point - setX(targetPoint.x); - setY(targetPoint.y); - - // set target to the next point in the array - nextAction(); + drawAll(ctx, width, height, areas, robot.current, robotSize); + }, [animationStatus, width, height, areas, robotSize]); + + // Handle animation + useEffect(() => { + if (animationStatus !== 1) return; + + const interval = setInterval(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // End animation after action log complete + if (currentAction.current >= actionLog.length) return setAnimationStatus(3); + + // Skip animation if paused + if (animationPauseUntil.current !== null) { + if (Date.now() > animationPauseUntil.current) { + animationPauseUntil.current = null; + currentAction.current++; } - break; - case 'rotate': - const targetRotation = position.rotation; - setRotation(v => v - 0.1); - - if (Math.abs(rotation - targetRotation) <= 0.1) { - setRotation(targetRotation); - nextAction(); - } else { - if (rotation > Math.PI) { - setRotation(v => v - 2 * Math.PI); - } + return; + } - if (rotation < -Math.PI) { - setRotation(v => v + 2 * Math.PI); + // Get current action + const { type, position: {x: tx, y: ty, rotation: targetRotation} }: Action = actionLog[currentAction.current]; + + switch(type) { + case 'move': { + // Calculate the distance to target point + const dx = tx - robot.current.x; + const dy = ty - robot.current.y; + const distance = Math.sqrt( + (tx - robot.current.x) ** 2 + + (ty - robot.current.y) ** 2); + + // If distance to target point is small + if (distance <= ANIMATION_SPEED) { + // Snap to the target point + robot.current.x = tx; + robot.current.y = ty; + + // Move on to next action + currentAction.current++; + break; } - if (Math.abs(rotation - targetRotation) <= 0.1) { - setRotation(targetRotation); - nextAction(); + // Move the robot towards the target + robot.current.x += (dx / distance) * ANIMATION_SPEED; + robot.current.y += (dy / distance) * ANIMATION_SPEED; + break; + } case 'rotate': { + // If rotation is close to target rotation + if (Math.abs(targetRotation - robot.current.rotation) <= 0.1) { + // Snap to the target point + robot.current.rotation = targetRotation; + + // Move on to next action + currentAction.current++; + break; } - } - break; - case 'sensor': - setAnimationUnpauseTime(Date.now() + 500); - break; - } - if (currentAction >= actionLog.length) { - stopAnimation(); - } - - drawRobot(ctx, {x, y, rotation}, robotSize); - - // Request the next frame - nextFrame(); - }; - - // Stop the current animation - const stopAnimation = () => { - if (animationFrame) { - setAnimationEnded(true); - cancelAnimationFrame(animationFrame); - } - } + robot.current.rotation += smallestAngle(targetRotation, robot.current.rotation) > 0 ? 0.1 : -0.1; - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; + if (robot.current.rotation > Math.PI) { + robot.current.rotation -= 2 * Math.PI; + } - canvas.width = width; - canvas.height = height; + if (robot.current.rotation < Math.PI) { + robot.current.rotation += 2 * Math.PI; + } + break; + } case 'sensor': + animationPauseUntil.current = Date.now() + 1000; + break; + } - ctx.reset(); - drawBorders(ctx, width, height); - drawAreas(ctx, areas); - drawRobot(ctx, {x, y, rotation}, robotSize); + drawAll(ctx, width, height, areas, robot.current, robotSize); + }, 10); - return stopAnimation; - }, []); + return () => clearInterval(interval); + }, [animationStatus, width, height, areas, robotSize]); // Store a reference to the HTML canvas const canvasRef = useRef(null); return ( <> - -

{isAnimationEnded ? message : <>}

+ +

{animationStatus === 3 ? '' : message}

); -} +}; -export default RobotSimulation; \ No newline at end of file +export default RobotSimulation; diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 1bdf50443e..41b23c78b3 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -15,7 +15,7 @@ interface MainProps { children?: never className?: never context?: DebuggerContext -}; +} /** * The main React Component of the Tab. @@ -24,7 +24,7 @@ const RobotMaze : React.FC = ({ context }) => { return ( ); -} +}; export default { /** From 08b896c7bdff7acf96c1f8facd08af2c524d13f9 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Fri, 21 Mar 2025 17:49:05 +0800 Subject: [PATCH 17/46] Implemented pause/resume/reset for display --- .../RobotMaze/components/RobotSimulation.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index fafdfd043b..4fe7f8c1c1 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -161,8 +161,13 @@ const RobotSimulation : React.FC = ({ const ctx = canvas.getContext('2d'); if (!ctx) return; - robot.current = actionLog[0].position; + // Reset current action + currentAction.current = 1; + // Reset robot position + robot.current = Object.assign({}, actionLog[0].position); + + // Update canvas dimensions canvas.width = width; canvas.height = height; @@ -250,13 +255,21 @@ const RobotSimulation : React.FC = ({ return () => clearInterval(interval); }, [animationStatus, width, height, areas, robotSize]); + console.log(animationStatus); + // Store a reference to the HTML canvas const canvasRef = useRef(null); return ( <> - -

{animationStatus === 3 ? '' : message}

+ {animationStatus === 0 + ? + : animationStatus === 1 + ? + : animationStatus === 2 + ? + : } + {animationStatus === 3 &&

{message}

}
From 544ded9086b0661e3c298f268633fe7c85a919e1 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Mon, 24 Mar 2025 10:43:39 +0800 Subject: [PATCH 18/46] Implement all functions in primary function set --- src/bundles/robot_minigame/functions.ts | 649 +++++++++++------- src/bundles/robot_minigame/index.ts | 4 +- .../RobotMaze/components/RobotSimulation.tsx | 42 +- 3 files changed, 439 insertions(+), 256 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index a03d9b273f..d26520d5f4 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -1,9 +1,9 @@ import context from 'js-slang/context'; -import { - head, - tail, - type List -} from 'js-slang/dist/stdlib/list'; +// import { +// head, +// tail, +// type List +// } from 'js-slang/dist/stdlib/list'; // A point (x, y) interface Point { @@ -16,56 +16,64 @@ export interface PointWithRotation extends Point { rotation: number } -// A prior movement +// A line segment between p1 and p2 +interface LineSegment { + p1: Point + p2: Point +} + +// A ray from origin towards target +interface Ray { + origin: Point + target: Point +} + +// A stored action export interface Action { - type: string + type: 'begin' | 'move' | 'rotate' | 'sensor' position: PointWithRotation } interface AreaFlags { - [name: string]: boolean + [name: string]: any } export interface Area { vertices: Point[] - isCollidable: boolean + isObstacle: boolean flags: AreaFlags } -export interface StateData { +export interface RobotMap { isInit: boolean + isComplete: boolean width: number height: number + robotSize: number areas: Area[] - areaLog: number[] + areaLog: Area[] actionLog: Action[] message: string - success: boolean - messages: string[] - robotSize: number } -interface Robot { - x: number // the top left corner - y: number - dx: number - dy: number - radius: number -} - -const stateData: StateData = { +const state: RobotMap = { isInit: false, + isComplete: false, width: 500, height: 500, + robotSize: 15, areas: [], areaLog: [], actionLog: [], - message: 'moved successfully', - success: true, - messages: [], - robotSize: 15 + message: 'moved successfully' }; +interface Robot extends Point { + dx: number + dy: number + radius: number +} + const robot: Robot = { x: 25, // default start pos, puts it at the top left corner of canvas without colliding with the walls y: 25, @@ -74,27 +82,58 @@ const robot: Robot = { radius: 15 // give the robot a circular hitbox }; -let bounds: Point[] = []; +let bounds: Point[]; -// sets the context to the statedata obj, mostly for convenience so i dont have to type context.... everytime -context.moduleContexts.robot_minigame.state = stateData; +// sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime +context.moduleContexts.robot_minigame.state = state; + +/** + * Teleport the robot + * + * @param x coordinate of the robot + * @param y coordinate of the robot + */ +function set_pos( + x: number, + y: number +) { + // Init functions should not run after initialization + if (state.isInit) return; -function set_pos(x: number, y: number): void { robot.x = x; robot.y = y; } -function set_rotation(rotation: number) { +/** + * Set the rotation of the robot + * + * @param rotation in radians + */ +function set_rotation( + rotation: number +) { + // Init functions should not run after initialization + if (state.isInit) return; + robot.dx = Math.cos(rotation); robot.dy = -Math.sin(rotation); } -function set_width(width: number) { - stateData.width = width; -} +/** + * Set the width and height of the map + * + * @param width of the map + * @param height of the map + */ +function set_dimensions( + width: number, + height: number +) { + // Init functions should not run after initialization + if (state.isInit) return; -function set_height(height: number) { - stateData.height = height; + state.width = width; + state.height = height; } // ===== // @@ -102,7 +141,7 @@ function set_height(height: number) { // ===== // /** - * Initializes a new simulation with a map of size width * height + * Shorthand function that initializes a new simulation with a map of size width * height * Also sets the initial position an rotation of the robot * * @param width of the map @@ -118,15 +157,15 @@ export function init( posY: number, rotation: number ) { - if (stateData.isInit) return; // dont allow init more than once + // Init functions should not run after initialization + if (state.isInit) return; - set_width(width); - set_height(height); + set_dimensions(width, height); set_pos(posX, posY); set_rotation(rotation); - stateData.actionLog.push({type: 'begin', position: getPositionWithRotation()}); // push starting point to movepoints data - stateData.isInit = true; + // Store the starting position in the actionLog + logAction('begin', getPositionWithRotation()); bounds = [ {x: 0, y: 0}, @@ -139,28 +178,35 @@ export function init( /** * Creates a new area with the given vertices and flags * - * @param vertices of the area - * @param isCollidable a boolean indicating if the area is a collidable obstacle or not + * @param vertices of the area in alternating x-y pairs + * @param isObstacle a boolean indicating if the area is an obstacle or not * @param flags any additional flags the area may have */ export function create_area( - vertices: Point[], - isCollidable: boolean, + rawVertices: number[], + isObstacle: boolean, flags: AreaFlags = {} ) { - // // Parse vertices list into a points array - // const points : Point[] = []; + // Init functions should not run after initialization + if (state.isInit) return; + + if (rawVertices.length % 2 !== 0) throw new Error('Odd number of arguments given (expected even)'); - // while (vertices != null) { - // const p = head(vertices); - // points.push({x: head(p), y: tail(p)}); - // vertices = tail(vertices); - // } + // Store vertices as Point array + const vertices: Point[] = []; + + // Parse x-y pairs into Points + for (let i = 0; i < rawVertices.length / 2; i++) { + vertices[i] = { + x: rawVertices[i * 2], + y: rawVertices[i * 2 + 1] + }; + } // Store the new area - stateData.areas.push({ + state.areas.push({ vertices, - isCollidable, + isObstacle, flags }); } @@ -171,8 +217,11 @@ export function create_area( * @param vertices of the obstacle */ export function create_obstacle( - vertices: Point[] + vertices: number[] ) { + // Init functions should not run after initialization + if (state.isInit) return; + create_area(vertices, true); } @@ -190,14 +239,24 @@ export function create_rect_obstacle( width: number, height: number ) { + // Init functions should not run after initialization + if (state.isInit) return; + create_obstacle([ - {x, y}, - {x: x + width, y}, - {x: x + width, y: y + height}, - {x, y:y + height} + x, y, + x + width, y, + x + width, y + height, + x, y + height ]); } +/** + * Inform the simulator that the initialisation phase is complete + */ +export function complete_init() { + state.isInit = true; +} + // ======= // // SENSORS // // ======= // @@ -205,11 +264,23 @@ export function create_rect_obstacle( /** * Get the distance to the closest collidable area * - * @returns the distance to the closest obstacle + * @returns the distance to the closest obstacle, or infinity (if robot is out of bounds) */ export function get_distance() : number { - // TO BE IMPLEMENTED - return 0; + // Check for all obstacles in the robot's path + const obstacleCollisions: Collision[] = robot_raycast((area: Area) => area.isObstacle); + + // If an obstacle is found, return its distance + if (obstacleCollisions.length > 0) return obstacleCollisions[0].distance; + + // Find the distance to the bounds + const boundsCollision: Collision | null = robot_raycast_area({ + vertices: bounds, + isObstacle: true, + flags: {} + }); + + return boundsCollision === null ? Infinity : boundsCollision.distance; } /** @@ -223,8 +294,10 @@ export function get_flags( x: number, y: number ) : AreaFlags { - // TO BE IMPLEMENTED - return {}; + // Find the area containing the point + const area: Area | null = area_of_point({x, y}); + + return area === null ? {} : area.flags; } /** @@ -233,8 +306,7 @@ export function get_flags( * @returns the color of the area under the robot */ export function get_color() : string { - // TO BE IMPLEMENTED - return ''; + return get_flags(robot.x, robot.y).color; } // ======= // @@ -246,61 +318,61 @@ export function get_color() : string { * * @param distance to move forward */ -export function move_forward(distance: number) { - // need to check for collision with wall - const dist = findDistanceToWall(); - stateData.messages.push(`${dist}`); - - if (dist < distance + robot.radius) { - stateData.message = 'collided'; - stateData.success = false; - distance = dist - robot.radius + 1; // move only until the wall - } +export function move_forward( + distance: number +) { + // Check for all areas in the robot's path + const collisions: Collision[] = robot_raycast() + .filter(col => col.distance < distance + robot.radius); - const nextPoint: Point = { - x: robot.x + distance * robot.dx, - y: robot.y + distance * robot.dy - }; + for (const col of collisions) { + // Log the area + logArea(col.area); + + // Handle a collision with an obstacle + if (col.area.isObstacle) { + // Calculate find distance + const finalDistance = (col.distance - robot.radius + 1); + + // Move the robot to its final position + robot.x = robot.x + finalDistance * robot.dx; + robot.y = robot.y + finalDistance * robot.dy; - robot.x = nextPoint.x; - robot.y = nextPoint.y; - stateData.actionLog.push({type: 'move', position: getPositionWithRotation()}); + // Update the final message + state.message = `Collided with wall at (${robot.x + col.distance * robot.dx},${robot.y + col.distance * robot.dy})`; - logCoordinates(); + // Throw an error to interrupt the simulation + throw new Error('Collided with wall'); + } + } + + // Move the robot to its end position + robot.x = robot.x + distance * robot.dx; + robot.y = robot.y + distance * robot.dy; + + // Store the action in the actionLog + logAction('move', getPositionWithRotation()); } // The distance from a wall a move_forward_to_wall() command will stop -const SAFE_DISTANCE_FROM_WALL : number = 5; +const SAFE_DISTANCE_FROM_WALL : number = 10; /** * Move the robot forward to within a predefined distance of the wall */ export function move_forward_to_wall() { - if (alrCollided()) return; - - let distance = findDistanceToWall(); // do the raycast, figure out how far the robot is from the nearest wall - - // a lil extra offset from wall - distance = Math.max(distance - robot.radius - SAFE_DISTANCE_FROM_WALL, 0); - - const nextPoint: Point = { - x: robot.x + distance * robot.dx, - y: robot.y + distance * robot.dy - }; - - robot.x = nextPoint.x; - robot.y = nextPoint.y; - stateData.actionLog.push({type: 'move', position: getPositionWithRotation()}); - - // for debug - stateData.messages.push(`Distance is ${distance} Collision point at x: ${nextPoint.x}, y: ${nextPoint.y}`); + // Move forward the furthest possible safe distance + a lil extra offset + move_forward(Math.max(get_distance() - robot.radius - SAFE_DISTANCE_FROM_WALL, 0)); } /** + * Rotates the robot clockwise by the given angle * - * @param angle the angle (in radians) to rotate right + * @param angle the angle (in radians) to rotate clockwise */ -export function rotate(angle: number) { +export function rotate( + angle: number +) { let currentAngle = Math.atan2(-robot.dy, robot.dx); currentAngle -= angle; @@ -311,10 +383,7 @@ export function rotate(angle: number) { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.actionLog.push({type: 'rotate', position: getPositionWithRotation()}); - - // debug log - logCoordinates(); + logAction('rotate', getPositionWithRotation()); } /** @@ -332,10 +401,7 @@ export function turn_left() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.actionLog.push({type: 'rotateLeft', position: getPositionWithRotation()}); - - // debug log - logCoordinates(); + logAction('rotate', getPositionWithRotation()); } /** @@ -352,10 +418,7 @@ export function turn_right() { if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - stateData.actionLog.push({type: 'rotateRight', position: getPositionWithRotation()}); - - // debug log - logCoordinates(); + logAction('rotate', getPositionWithRotation()); } // ======= // @@ -363,164 +426,284 @@ export function turn_right() { // ======= // /** + * Checks if the robot's entered areas satisfy the condition * + * @returns if the entered areas satisfy the condition */ -export function enteredAreas( - check : (area : Area[]) => void +export function entered_areas( + callback : (areas : Area[]) => boolean ) : boolean { - // TO BE IMPLEMENTED - return false; + return callback(state.areaLog); } -// ==================== // -// Unassigned / Helpers // -// ==================== // - -export function rotate_left(angle: number) { - let currentAngle = Math.atan2(-robot.dy, robot.dx); - - currentAngle += angle; +// ================= // +// DATA READ HELPERS // +// ================= // - robot.dx = Math.cos(currentAngle); - robot.dy = -Math.sin(currentAngle); +/** + * Gets the position of the robot + * + * @returns the position of the robot + */ +function getPosition(): Point { + return { + x: robot.x, + y: robot.y + }; +} - if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; - if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; +/** + * Gets the position of the robot (with rotation) + * + * @returns the position of the robot (with rotation) + */ +function getPositionWithRotation(): PointWithRotation { + const angle = Math.atan2(-robot.dy, robot.dx); + return {x: robot.x, y: robot.y, rotation: angle}; +} - stateData.actionLog.push({type: 'rotateLeft', position: getPositionWithRotation()}); +// ======================== // +// RAYCAST AND AREA HELPERS // +// ======================== // - logCoordinates(); +// A collision between a ray and an area +interface Collision { + distance: number + area: Area } -export function getX():number { - return robot.x; +/** + * Get the distance between the robot and area, if the robot is facing the area + * Casts 3 rays from the robot's left, middle and right + * + * @param filter the given areas (optional) + * @returns the minimum distance, or null (if no collision) + */ +function robot_raycast( + filter: (area: Area) => boolean = () => true +) : Collision[] { + return state.areas + .filter(filter) // Apply filter + .map(area => robot_raycast_area(area)) // Raycast each area on the map + .concat([ + robot_raycast_area({vertices: bounds, isObstacle: true, flags: {}}) // Raycast map bounds as well + ]) + .filter(col => col !== null) // Remove null collisions + .sort((a, b) => a.distance - b.distance); // Sort by distance } -export function getY():number { - return robot.y; +/** + * Get the distance between the robot and area, if the robot is facing the area + * Casts 3 rays from the robot's left, middle and right + * + * @param area to check + * @returns the minimum distance, or null (if no collision) + */ +function robot_raycast_area( + area: Area +) : Collision | null { + // raycast from 3 sources: left, middle, right + const raycast_sources: Point[] = [-1, 0, 1] + .map(mult => ({ + x: robot.x + mult * robot.radius * robot.dy, + y: robot.y + mult * robot.radius * robot.dx + })); + + // Raycast 3 times, one for each source + const collisions: Collision[] = raycast_sources + .map(source => raycast( + {origin: source, target: {x: robot.dx + source.x, y: robot.dy + source.y}}, area)) + .filter(col => col !== null); + + // Return null if no intersection + return collisions.length > 0 + ? collisions.reduce((acc, col) => acc.distance > col.distance ? col : acc) + : null; } -// detects if there are walls 10 units ahead of the robot -// add as a command later -export function sensor(): boolean { - const dist = findDistanceToWall(); - stateData.actionLog.push({type: 'sensor', position: getPositionWithRotation()}); - if (dist <= 10 + robot.radius) { - return true; - } - return false; +/** + * Check which areas fall along a ray + * + * @param ray to cast + * @param areas to check + * @returns collisions between the ray and areas + */ +function raycast_multi( + ray: Ray, + areas: Area[] +) : Collision[] { + return areas + .map(area => raycast(ray, area)) // Raycast each area + .filter(col => col !== null) // Remove null collisions + .sort((a, b) => a.distance - b.distance); // Sort by distance } -// returns the distance from the nearest wall -function findDistanceToWall(): number { - let minDist: number = Infinity; +/** + * Get the shortest distance between a ray and an area + * + * @param ray being cast + * @param area to check + * @returns the collision with the minimum distance, or null (if no collision) + */ +function raycast( + ray: Ray, + area: Area +) : Collision | null { + const { vertices } = area; - // loop through all the walls - for (const wall of stateData.areas) { - const intersectionDist = raycast(wall.vertices); // do the raycast + // Store the minimum distance + let distance = Infinity; - // if intersection is closer, update minDist - if (intersectionDist !== null && intersectionDist < minDist) { - minDist = intersectionDist; - } + for (let i = 0; i < vertices.length; i++) { + // Border line segment + const border: LineSegment = { + p1: {x: vertices[i].x, y: vertices[i].y}, + p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} + }; + + // Compute the minimum distance + const distanceToIntersection: number = getIntersection(ray, border); + + // Save the new minimum, if necessary + if (distanceToIntersection < distance) distance = distanceToIntersection; } - // check outer bounds as well - const intersectionDist = raycast(bounds); - if (intersectionDist !== null && intersectionDist < minDist) { - minDist = intersectionDist; + // Return null if no collision + return distance < Infinity + ? {distance, area} + : null; +} + +/** + * Find the area the robot is in + * + * @returns if the robot is within the area + */ +function area_of_point( + point: Point +) : Area | null { + // Return the first area the point is within + for (const area of state.areas) { + if (is_within_area(point, area)) return area; } - // Closest intersection point - // By all rights, there should always be an intersection point since the robot is always within the bounds - // and the bounds should be a collision - // but something goes wrong, will just return 0 - return minDist === Infinity ? 0 : minDist; + // Otherwise return null + return null; } -// does the raycast logic for one particular area -// three rays are cast: one from the center, one from the top and one from the bottom. the minimum dist is returned -// return null if no collision -function raycast( - vertices: Point[] -): number | null { - let minDist = Infinity; +/** + * Check if the point is within the area + * + * @param point potentially within the area + * @param area to check + * @returns if the point is within the area + */ +function is_within_area( + point: Point, + area: Area +) : boolean { + const { vertices } = area; + + // Cast a ray to the right of the point + const ray = { + origin: point, + target: {x: point.x + 1, y: point.y + 0} + }; + + // Count the intersections + let intersections = 0; for (let i = 0; i < vertices.length; i++) { - // wall line segment - const x1 = vertices[i].x, y1 = vertices[i].y; - const x2 = vertices[(i + 1) % vertices.length].x, y2 = vertices[(i + 1) % vertices.length].y; - - // calculate the top and bottom coordinates of the robot - const topX = robot.x - robot.radius * robot.dy; - const topY = robot.y - robot.radius * robot.dx; - - const bottomX = robot.x + robot.radius * robot.dy; - const bottomY = robot.y + robot.radius * robot.dx; - - // raycast from 3 sources: top, middle, bottom - const raycast_sources: Point[] = [ - {x: robot.x, y: robot.y}, - {x: topX, y: topY}, - {x: bottomX, y: bottomY} - ]; - - for (const source of raycast_sources) { - const intersectionDist = getIntersection(source.x, source.y, robot.dx + source.x, robot.dy + source.y, x1, y1, x2, y2); - if (intersectionDist !== null && intersectionDist < minDist) { - minDist = intersectionDist; - } - } + // Border line segment + const border: LineSegment = { + p1: {x: vertices[i].x, y: vertices[i].y}, + p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} + }; + + // Increment intersections if the ray intersects the border + if (getIntersection(ray, border) < Infinity) intersections++; } - return minDist === Infinity ? null : minDist; + // Even => Outside; Odd => Inside + return intersections % 2 === 1; } -// Determine if a ray and a line segment intersect, and if so, determine the collision point -// returns null if there's no collision, or the distance to the line segment if collides -function getIntersection(x1, y1, x2, y2, x3, y3, x4, y4): number | null { - const denom = ((x2 - x1)*(y4 - y3)-(y2 - y1)*(x4 - x3)); - let r; - let s; - let x; - let y; - let b = false; +/** + * Determine if a ray and a line segment intersect + * If they intersect, determine the distance from the ray's origin to the collision point + * + * @param ray being checked + * @param line to check intersection + * @returns the distance to the line segment, or infinity (if no collision) + */ +function getIntersection( + { origin, target }: Ray, + { p1, p2 }: LineSegment +) : number { + const denom: number = ((target.x - origin.x)*(p2.y - p1.y)-(target.y - origin.y)*(p2.x - p1.x)); // If lines are collinear or parallel - if (denom === 0) return null; + if (denom === 0) return Infinity; // Intersection in ray "local" coordinates - r = (((y1 - y3) * (x4 - x3)) - (x1 - x3) * (y4 - y3)) / denom; + const r: number = (((origin.y - p1.y) * (p2.x - p1.x)) - (origin.x - p1.x) * (p2.y - p1.y)) / denom; // Intersection in segment "local" coordinates - s = (((y1 - y3) * (x2 - x1)) - (x1 - x3) * (y2 - y1)) / denom; - - // The algorithm gives the intersection of two infinite lines, determine if it lies on the side that the ray is defined on - if (r >= 0) { - // If point along the line segment - if (s >= 0 && s <= 1) { - b = true; - // Get point coordinates (offset by r local units from start of ray) - x = x1 + r * (x2 - x1); - y = y1 + r * (y2 - y1); - } - } + const s: number = (((origin.y - p1.y) * (target.x - origin.x)) - (origin.x - p1.x) * (target.y - origin.y)) / denom; - if (!b) return null; + // Check if line segment is behind ray, or not on the line segment + if (r < 0 || s < 0 || s > 1) return Infinity; return r; } -function alrCollided() { - return !stateData.success; +// =============== // +// LOGGING HELPERS // +// =============== // + +/** + * Add a movement to the action log + * + * @param type of action + * @param position to move to + */ +function logAction( + type: 'begin' | 'move' | 'rotate' | 'sensor', + position: PointWithRotation +) { + state.actionLog.push({type, position}); } -function getPositionWithRotation(): PointWithRotation { - const angle = Math.atan2(-robot.dy, robot.dx); - return {x: robot.x, y: robot.y, rotation: angle}; +/** + * Add an area to the area log + * + * @param area to log + */ +function logArea( + area: Area +) { + if ( + state.areaLog.length > 0 // Check for empty area log + && areaEquals(area, state.areaLog[state.areaLog.length - 1]) // Check if same area repeated + ) return; + + state.areaLog.push(area); } -// debug -function logCoordinates() { - stateData.messages.push(`x: ${robot.x}, y: ${robot.y}, dx: ${robot.dx}, dy: ${robot.dy}`); +/** + * Compare two areas for equality + * + * @param a the first area to compare + * @param b the second area to compare + * @returns if a == b + */ +function areaEquals(a: Area, b: Area) { + if ( + a.vertices.length !== b.vertices.length // a and b must have an equal number of vertices + || a.vertices.some((v, i) => v.x !== b.vertices[i].x || v.y !== b.vertices[i].y) // a and b's vertices must be the same + || a.isObstacle !== b.isObstacle // Either both a and b or neither a nor b are obstacles + || Object.keys(a.flags).length === Object.length + ) return false; + + return true; } diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 653142b74b..8f4b4fab0d 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -9,8 +9,8 @@ */ export { - init, create_area, create_obstacle, create_rect_obstacle, + init, create_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_flags, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - enteredAreas + entered_areas } from './functions'; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 4fe7f8c1c1..1a53f78b98 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import type { Area, Action, PointWithRotation, StateData } from '../../../bundles/robot_minigame/functions'; +import type { Area, Action, PointWithRotation, RobotMap } from '../../../bundles/robot_minigame/functions'; /** * Calculate the acute angle between 2 angles @@ -118,7 +118,7 @@ const ANIMATION_SPEED : number = 2; interface MapProps { children?: never className?: never - state: StateData + state: RobotMap } const RobotSimulation : React.FC = ({ @@ -153,7 +153,7 @@ const RobotSimulation : React.FC = ({ // Ensure canvas is preloaded correctly useEffect(() => { - // Only trigger if animationStatus is 0 + // Only load if animationStatus is 0 if (animationStatus !== 0) return; const canvas = canvasRef.current; @@ -164,8 +164,8 @@ const RobotSimulation : React.FC = ({ // Reset current action currentAction.current = 1; - // Reset robot position - robot.current = Object.assign({}, actionLog[0].position); + // Reset robot position if action log has actions + if (actionLog.length > 0) robot.current = Object.assign({}, actionLog[0].position); // Update canvas dimensions canvas.width = width; @@ -197,22 +197,22 @@ const RobotSimulation : React.FC = ({ } // Get current action - const { type, position: {x: tx, y: ty, rotation: targetRotation} }: Action = actionLog[currentAction.current]; + const { type, position: target }: Action = actionLog[currentAction.current]; switch(type) { case 'move': { // Calculate the distance to target point - const dx = tx - robot.current.x; - const dy = ty - robot.current.y; + const dx = target.x - robot.current.x; + const dy = target.y - robot.current.y; const distance = Math.sqrt( - (tx - robot.current.x) ** 2 + - (ty - robot.current.y) ** 2); + (target.x - robot.current.x) ** 2 + + (target.y - robot.current.y) ** 2); // If distance to target point is small if (distance <= ANIMATION_SPEED) { // Snap to the target point - robot.current.x = tx; - robot.current.y = ty; + robot.current.x = target.x; + robot.current.y = target.y; // Move on to next action currentAction.current++; @@ -223,18 +223,18 @@ const RobotSimulation : React.FC = ({ robot.current.x += (dx / distance) * ANIMATION_SPEED; robot.current.y += (dy / distance) * ANIMATION_SPEED; break; - } case 'rotate': { + } case 'rotate': // If rotation is close to target rotation - if (Math.abs(targetRotation - robot.current.rotation) <= 0.1) { + if (Math.abs(target.rotation - robot.current.rotation) <= 0.1) { // Snap to the target point - robot.current.rotation = targetRotation; + robot.current.rotation = target.rotation; // Move on to next action currentAction.current++; break; } - robot.current.rotation += smallestAngle(targetRotation, robot.current.rotation) > 0 ? 0.1 : -0.1; + robot.current.rotation += smallestAngle(target.rotation, robot.current.rotation) > 0 ? 0.1 : -0.1; if (robot.current.rotation > Math.PI) { robot.current.rotation -= 2 * Math.PI; @@ -244,9 +244,11 @@ const RobotSimulation : React.FC = ({ robot.current.rotation += 2 * Math.PI; } break; - } case 'sensor': - animationPauseUntil.current = Date.now() + 1000; + case 'sensor': + animationPauseUntil.current = Date.now() + 500; break; + default: + robot.current = Object.assign({}, target); } drawAll(ctx, width, height, areas, robot.current, robotSize); @@ -255,8 +257,6 @@ const RobotSimulation : React.FC = ({ return () => clearInterval(interval); }, [animationStatus, width, height, areas, robotSize]); - console.log(animationStatus); - // Store a reference to the HTML canvas const canvasRef = useRef(null); @@ -268,7 +268,7 @@ const RobotSimulation : React.FC = ({ ? : animationStatus === 2 ? - : } + : } {animationStatus === 3 &&

{message}

}
From 20bd34f2adf6a2a80323b0182d50971e6249e9ee Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Mon, 24 Mar 2025 12:59:00 +0800 Subject: [PATCH 19/46] Implement entered_colors(); Fix rotation render; Change create_area() flag input --- src/bundles/robot_minigame/functions.ts | 210 ++++++++++++------ src/bundles/robot_minigame/index.ts | 8 +- .../RobotMaze/components/RobotSimulation.tsx | 8 +- src/tabs/RobotMaze/index.tsx | 4 +- 4 files changed, 155 insertions(+), 75 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index d26520d5f4..bfa75ab5da 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -87,55 +87,6 @@ let bounds: Point[]; // sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime context.moduleContexts.robot_minigame.state = state; -/** - * Teleport the robot - * - * @param x coordinate of the robot - * @param y coordinate of the robot - */ -function set_pos( - x: number, - y: number -) { - // Init functions should not run after initialization - if (state.isInit) return; - - robot.x = x; - robot.y = y; -} - -/** - * Set the rotation of the robot - * - * @param rotation in radians - */ -function set_rotation( - rotation: number -) { - // Init functions should not run after initialization - if (state.isInit) return; - - robot.dx = Math.cos(rotation); - robot.dy = -Math.sin(rotation); -} - -/** - * Set the width and height of the map - * - * @param width of the map - * @param height of the map - */ -function set_dimensions( - width: number, - height: number -) { - // Init functions should not run after initialization - if (state.isInit) return; - - state.width = width; - state.height = height; -} - // ===== // // SETUP // // ===== // @@ -183,34 +134,80 @@ export function init( * @param flags any additional flags the area may have */ export function create_area( - rawVertices: number[], + vertices: number[], isObstacle: boolean, - flags: AreaFlags = {} + flags: any[] ) { // Init functions should not run after initialization if (state.isInit) return; - if (rawVertices.length % 2 !== 0) throw new Error('Odd number of arguments given (expected even)'); + if (vertices.length % 2 !== 0) throw new Error('Odd number of vertice x-y coordinates given (expected even)'); + + if (flags.length % 2 !== 0) throw new Error('Odd number of flag arguments given (expected even)'); // Store vertices as Point array - const vertices: Point[] = []; + const parsedVertices: Point[] = []; // Parse x-y pairs into Points - for (let i = 0; i < rawVertices.length / 2; i++) { - vertices[i] = { - x: rawVertices[i * 2], - y: rawVertices[i * 2 + 1] + for (let i = 0; i < vertices.length / 2; i++) { + parsedVertices[i] = { + x: vertices[i * 2], + y: vertices[i * 2 + 1] }; } + // Store flags as an object + const parsedFlags = {}; + + // Parse flag-value pairs into flags + for (let i = 0; i < flags.length / 2; i++) { + // Retrieve flag + const flag = flags[i * 2]; + + // Check flag is string + if (typeof flag !== 'string') throw new Error(`Flag arguments must be strings (${flag} is a ${typeof flag})`); + + // Add flag to object + parsedFlags[flag] = flags[i * 2 + 1]; + } + // Store the new area state.areas.push({ - vertices, + vertices: parsedVertices, isObstacle, - flags + flags: parsedFlags }); } +/** + * Creates a new rectangular, axis-aligned area + * + * @param x top left corner of the rectangle + * @param y top right corner of the rectangle + * @param width of the rectangle + * @param height of the rectangle + * @param isObstacle a boolean indicating if the area is an obstacle or not + * @param flags any additional flags the area may have + */ +export function create_rect_area( + x: number, + y: number, + width: number, + height: number, + isObstacle: boolean, + flags: any[] +) { + // Init functions should not run after initialization + if (state.isInit) return; + + create_area([ + x, y, + x + width, y, + x + width, y + height, + x, y + height + ], isObstacle, flags); +} + /** * Creates a new obstacle * @@ -222,7 +219,7 @@ export function create_obstacle( // Init functions should not run after initialization if (state.isInit) return; - create_area(vertices, true); + create_area(vertices, true, []); } /** @@ -242,12 +239,7 @@ export function create_rect_obstacle( // Init functions should not run after initialization if (state.isInit) return; - create_obstacle([ - x, y, - x + width, y, - x + width, y + height, - x, y + height - ]); + create_rect_area(x, y, width, height, true, []); } /** @@ -425,6 +417,15 @@ export function turn_right() { // TESTING // // ======= // +/** + * Inform the simulator that the testing phase is starting + */ +export function start_testing() { + if (state.isComplete) throw new Error('May not start testing twice!'); + + state.isComplete = true; +} + /** * Checks if the robot's entered areas satisfy the condition * @@ -433,9 +434,74 @@ export function turn_right() { export function entered_areas( callback : (areas : Area[]) => boolean ) : boolean { + // Testing functions should only run after the simulation is complete + if (!state.isComplete) return false; + return callback(state.areaLog); } +/** + * Check if the robot has entered different areas with the given colors in order + * + * @param colors in order + * @returns if the robot entered the given colors in order + */ +export function entered_colors( + colors: string[] +) : boolean { + // Testing functions should only run after the simulation is complete + if (!state.isComplete) return false; + + return state.areaLog + .filter(area => colors.includes(area.flags.color)) // Filter relevant colors + .filter(filterAdjacentDuplicateAreas) // Filter adjacent duplicates + .every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color +} + +// ================== // +// DATA WRITE HELPERS // +// ================== // + +/** + * Teleport the robot + * + * @param x coordinate of the robot + * @param y coordinate of the robot + */ +function set_pos( + x: number, + y: number +) { + robot.x = x; + robot.y = y; +} + +/** + * Set the rotation of the robot + * + * @param rotation in radians + */ +function set_rotation( + rotation: number +) { + robot.dx = Math.cos(rotation); + robot.dy = -Math.sin(rotation); +} + +/** + * Set the width and height of the map + * + * @param width of the map + * @param height of the map + */ +function set_dimensions( + width: number, + height: number +) { + state.width = width; + state.height = height; +} + // ================= // // DATA READ HELPERS // // ================= // @@ -707,3 +773,15 @@ function areaEquals(a: Area, b: Area) { return true; } + +/** + * Filter callback to remove adjacent duplicate areas + * + * @param area currently being checked + * @param i index of area + * @param areas the full array being filtered + * @returns if the current area is not a duplicate of the previous area + */ +const filterAdjacentDuplicateAreas = (area : Area, i : number, areas: Area[]) : boolean => + i === 0 // First one is always correct + || !areaEquals(area, areas[i - 1]); // Otherwise check for equality against previous area diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 8f4b4fab0d..861b20ef5c 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -1,7 +1,5 @@ /** - * A single sentence summarising the module (this sentence is displayed larger). - * - * Sentences describing the module. More sentences about the module. + * The robot_minigame module allows us to control a robot to complete various tasks * * @module robot_minigame * @author Koh Wai Kei @@ -9,8 +7,8 @@ */ export { - init, create_area, create_obstacle, create_rect_obstacle, complete_init, + init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_flags, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - entered_areas + start_testing, entered_areas, entered_colors } from './functions'; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 1a53f78b98..22af6af841 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -39,7 +39,7 @@ const drawBorders = (ctx: CanvasRenderingContext2D, width: number, height: numbe // Draw the areas of the map const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { - for (const { vertices } of areas) { + for (const { vertices, isObstacle, flags } of areas) { ctx.beginPath(); ctx.moveTo(vertices[0].x, vertices[0].y); for (const vertex of vertices.slice(1)) { @@ -47,7 +47,9 @@ const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { } ctx.closePath(); - ctx.fillStyle = 'rgba(169, 169, 169, 0.5)'; // Set the fill color + ctx.fillStyle = isObstacle // Obstacles are gray + ? 'rgba(169, 169, 169, 0.5)' + : flags.color || 'none'; // Areas may have color ctx.fill(); // Fill the polygon ctx.strokeStyle = 'rgb(53, 53, 53)'; // Set the stroke color @@ -225,7 +227,7 @@ const RobotSimulation : React.FC = ({ break; } case 'rotate': // If rotation is close to target rotation - if (Math.abs(target.rotation - robot.current.rotation) <= 0.1) { + if (Math.abs((target.rotation - robot.current.rotation) % (2 * Math.PI)) < 0.1) { // Snap to the target point robot.current.rotation = target.rotation; diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 41b23c78b3..3ce901675e 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -34,8 +34,10 @@ export default { * @returns {boolean} */ toSpawn(context: DebuggerContext) { + // !!! TEMPORARY DEBUGGING FUNCTION, REMOVE ONCE MODULE IS COMPLETE !!! console.log(context.context?.moduleContexts?.robot_minigame.state); - return context.context?.moduleContexts?.robot_minigame.state.isInit; + + return context.context?.moduleContexts?.robot_minigame.state.isComplete; }, /** From 4ae4a4cc0d7428fcd979093b46e4ece9e148f6b7 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Mon, 24 Mar 2025 16:20:35 +0800 Subject: [PATCH 20/46] Make get_flags() private; Update internal documentation --- src/bundles/robot_minigame/functions.ts | 80 ++++++++++++++----------- src/bundles/robot_minigame/index.ts | 2 +- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index bfa75ab5da..00300e54f7 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -93,7 +93,7 @@ context.moduleContexts.robot_minigame.state = state; /** * Shorthand function that initializes a new simulation with a map of size width * height - * Also sets the initial position an rotation of the robot + * Also sets the initial position and rotation of the robot * * @param width of the map * @param height of the map @@ -127,7 +127,7 @@ export function init( } /** - * Creates a new area with the given vertices and flags + * Create a new area with the given vertices and flags * * @param vertices of the area in alternating x-y pairs * @param isObstacle a boolean indicating if the area is an obstacle or not @@ -180,10 +180,10 @@ export function create_area( } /** - * Creates a new rectangular, axis-aligned area + * Create a new rectangular, axis-aligned area * - * @param x top left corner of the rectangle - * @param y top right corner of the rectangle + * @param x coordinate of the top left corner of the rectangle + * @param y coordinate of the top left corner of the rectangle * @param width of the rectangle * @param height of the rectangle * @param isObstacle a boolean indicating if the area is an obstacle or not @@ -209,7 +209,7 @@ export function create_rect_area( } /** - * Creates a new obstacle + * Create a new obstacle * * @param vertices of the obstacle */ @@ -223,10 +223,10 @@ export function create_obstacle( } /** - * Creates a new rectangular, axis-aligned obstacle + * Create a new rectangular, axis-aligned obstacle * - * @param x top left corner of the rectangle - * @param y top right corner of the rectangle + * @param x coordinate of the top left corner of the rectangle + * @param y coordinate of the top left corner of the rectangle * @param width of the rectangle * @param height of the rectangle */ @@ -254,7 +254,7 @@ export function complete_init() { // ======= // /** - * Get the distance to the closest collidable area + * Get the distance to the closest obstacle * * @returns the distance to the closest obstacle, or infinity (if robot is out of bounds) */ @@ -276,24 +276,7 @@ export function get_distance() : number { } /** - * Gets the flags of the area containing the point (x, y) - * - * @param x coordinate - * @param y coordinate - * @returns the flags of the area containing (x, y) - */ -export function get_flags( - x: number, - y: number -) : AreaFlags { - // Find the area containing the point - const area: Area | null = area_of_point({x, y}); - - return area === null ? {} : area.flags; -} - -/** - * Gets the color of the area under the robot + * Get the color of the area under the robot * * @returns the color of the area under the robot */ @@ -358,9 +341,9 @@ export function move_forward_to_wall() { } /** - * Rotates the robot clockwise by the given angle + * Rotate the robot clockwise by the given angle * - * @param angle the angle (in radians) to rotate clockwise + * @param angle (in radians) to rotate clockwise */ export function rotate( angle: number @@ -379,7 +362,7 @@ export function rotate( } /** - * Turns the robot 90 degrees to the left + * Turn the robot 90 degrees to the left */ export function turn_left() { let currentAngle = Math.atan2(-robot.dy, robot.dx); @@ -397,7 +380,7 @@ export function turn_left() { } /** - * Turns the robot 90 degrees to the right + * Turn the robot 90 degrees to the right */ export function turn_right() { let currentAngle = Math.atan2(-robot.dy, robot.dx); @@ -427,9 +410,9 @@ export function start_testing() { } /** - * Checks if the robot's entered areas satisfy the condition + * Checks if the robot's entered areas satisfy the callback * - * @returns if the entered areas satisfy the condition + * @returns if the entered areas satisfy the callback */ export function entered_areas( callback : (areas : Area[]) => boolean @@ -443,7 +426,7 @@ export function entered_areas( /** * Check if the robot has entered different areas with the given colors in order * - * @param colors in order + * @param colors in the order visited * @returns if the robot entered the given colors in order */ export function entered_colors( @@ -458,6 +441,12 @@ export function entered_colors( .every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color } +// ================== +// ================== +// PRIVATE FUNCTIONS: +// ================== +// ================== + // ================== // // DATA WRITE HELPERS // // ================== // @@ -756,6 +745,10 @@ function logArea( state.areaLog.push(area); } +// ============ // +// AREA HELPERS // +// ============ // + /** * Compare two areas for equality * @@ -785,3 +778,20 @@ function areaEquals(a: Area, b: Area) { const filterAdjacentDuplicateAreas = (area : Area, i : number, areas: Area[]) : boolean => i === 0 // First one is always correct || !areaEquals(area, areas[i - 1]); // Otherwise check for equality against previous area + +/** + * Gets the flags of the area containing the point (x, y) + * + * @param x coordinate + * @param y coordinate + * @returns the flags of the area containing (x, y) + */ +function get_flags( + x: number, + y: number +) : AreaFlags { + // Find the area containing the point + const area: Area | null = area_of_point({x, y}); + + return area === null ? {} : area.flags; +} diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 861b20ef5c..a706c8c10c 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -8,7 +8,7 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, - get_distance, get_flags, get_color, + get_distance, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, start_testing, entered_areas, entered_colors } from './functions'; From 43e2f67d568b19eacb0a1a9eeecd4790918ae49d Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 26 Mar 2025 08:36:21 +0800 Subject: [PATCH 21/46] Fix entered_colors() --- src/bundles/robot_minigame/functions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 00300e54f7..4ec207ce3d 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -435,10 +435,11 @@ export function entered_colors( // Testing functions should only run after the simulation is complete if (!state.isComplete) return false; - return state.areaLog + const coloredAreas = state.areaLog .filter(area => colors.includes(area.flags.color)) // Filter relevant colors - .filter(filterAdjacentDuplicateAreas) // Filter adjacent duplicates - .every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color + .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates + + return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color } // ================== From 8c06138040d89667cfdec049dd90dab27d8a2ce0 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 26 Mar 2025 09:45:26 +0800 Subject: [PATCH 22/46] fixed areaEquals, added debug functionality --- src/bundles/robot_minigame/functions.ts | 9 ++++++--- src/tabs/RobotMaze/components/RobotSimulation.tsx | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 4ec207ce3d..b5b07e815f 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -54,6 +54,7 @@ export interface RobotMap { areaLog: Area[] actionLog: Action[] message: string + debugLog: string[] } const state: RobotMap = { @@ -65,7 +66,8 @@ const state: RobotMap = { areas: [], areaLog: [], actionLog: [], - message: 'moved successfully' + message: 'moved successfully', + debugLog: [] }; interface Robot extends Point { @@ -438,7 +440,7 @@ export function entered_colors( const coloredAreas = state.areaLog .filter(area => colors.includes(area.flags.color)) // Filter relevant colors .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates - + return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color } @@ -762,7 +764,8 @@ function areaEquals(a: Area, b: Area) { a.vertices.length !== b.vertices.length // a and b must have an equal number of vertices || a.vertices.some((v, i) => v.x !== b.vertices[i].x || v.y !== b.vertices[i].y) // a and b's vertices must be the same || a.isObstacle !== b.isObstacle // Either both a and b or neither a nor b are obstacles - || Object.keys(a.flags).length === Object.length + || Object.keys(a.flags).length !== Object.keys(b.flags).length // Check flags length equality + || Object.keys(a.flags).some(key => a.flags[key] !== b.flags[key]) // Check flag value equality ) return false; return true; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 22af6af841..d9b251eb22 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import type { Area, Action, PointWithRotation, RobotMap } from '../../../bundles/robot_minigame/functions'; +import { Properties } from '@blueprintjs/icons'; +import { State } from 'nbody'; /** * Calculate the acute angle between 2 angles @@ -134,7 +136,8 @@ const RobotSimulation : React.FC = ({ message, // success, // messages, - robotSize + robotSize, + debugLog } }) => { // Store animation status @@ -155,6 +158,9 @@ const RobotSimulation : React.FC = ({ // Ensure canvas is preloaded correctly useEffect(() => { + // DEBUG LOG REMOVE LATER + console.log(debugLog); + // Only load if animationStatus is 0 if (animationStatus !== 0) return; From 8d8cb20f16eaf95482a14ed5a9595205ad5196fc Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 26 Mar 2025 10:24:24 +0800 Subject: [PATCH 23/46] fixed get_distance not accounting for radius of robot --- src/bundles/robot_minigame/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index b5b07e815f..59e3a8a429 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -265,7 +265,7 @@ export function get_distance() : number { const obstacleCollisions: Collision[] = robot_raycast((area: Area) => area.isObstacle); // If an obstacle is found, return its distance - if (obstacleCollisions.length > 0) return obstacleCollisions[0].distance; + if (obstacleCollisions.length > 0) return obstacleCollisions[0].distance - robot.radius; // Find the distance to the bounds const boundsCollision: Collision | null = robot_raycast_area({ @@ -274,7 +274,7 @@ export function get_distance() : number { flags: {} }); - return boundsCollision === null ? Infinity : boundsCollision.distance; + return boundsCollision === null ? Infinity : boundsCollision.distance - robot.radius; } /** From 8cac2e8aa2a4ddab61ff92ed7e966a93f6e485b6 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 26 Mar 2025 10:26:56 +0800 Subject: [PATCH 24/46] removed unnecessary imports --- src/tabs/RobotMaze/components/RobotSimulation.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index d9b251eb22..d95f557ed4 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; import type { Area, Action, PointWithRotation, RobotMap } from '../../../bundles/robot_minigame/functions'; -import { Properties } from '@blueprintjs/icons'; -import { State } from 'nbody'; /** * Calculate the acute angle between 2 angles From 2f9cb4fcf3c222b3ac8e843ad3ff5331bd9498b3 Mon Sep 17 00:00:00 2001 From: Koh Wai Kei Date: Wed, 26 Mar 2025 11:04:58 +0800 Subject: [PATCH 25/46] tweaked get_distance and move_forward_to_wall --- src/bundles/robot_minigame/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 59e3a8a429..8d49a2488e 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -339,7 +339,7 @@ const SAFE_DISTANCE_FROM_WALL : number = 10; */ export function move_forward_to_wall() { // Move forward the furthest possible safe distance + a lil extra offset - move_forward(Math.max(get_distance() - robot.radius - SAFE_DISTANCE_FROM_WALL, 0)); + move_forward(Math.max(get_distance() - SAFE_DISTANCE_FROM_WALL, 0)); } /** From 5a5c541fad4f9fa1aa7be83b84ccc91ca413f17b Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 27 Mar 2025 10:29:35 +0800 Subject: [PATCH 26/46] Allow multiple maps (incomplete) --- src/bundles/robot_minigame/functions.ts | 299 +++++++++--------- src/bundles/robot_minigame/index.ts | 2 +- .../RobotMaze/components/RobotSimulation.tsx | 80 +++-- src/tabs/RobotMaze/index.tsx | 3 +- 4 files changed, 180 insertions(+), 204 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 8d49a2488e..de04c79301 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -44,48 +44,34 @@ export interface Area { flags: AreaFlags } +export interface Robot extends PointWithRotation { + radius: number +} + export interface RobotMap { - isInit: boolean - isComplete: boolean width: number height: number - robotSize: number + robot: Robot areas: Area[] areaLog: Area[] actionLog: Action[] message: string - debugLog: string[] } -const state: RobotMap = { - isInit: false, - isComplete: false, - width: 500, - height: 500, - robotSize: 15, - areas: [], - areaLog: [], - actionLog: [], - message: 'moved successfully', - debugLog: [] -}; - -interface Robot extends Point { - dx: number - dy: number - radius: number +export interface RobotMinigame { + isInit: boolean + isComplete: boolean + activeMap: number + maps: RobotMap[] } -const robot: Robot = { - x: 25, // default start pos, puts it at the top left corner of canvas without colliding with the walls - y: 25, - dx: 1, - dy: 0, - radius: 15 // give the robot a circular hitbox +const state: RobotMinigame = { + isInit: false, + isComplete: false, + activeMap: -1, + maps: [] }; -let bounds: Point[]; - // sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime context.moduleContexts.robot_minigame.state = state; @@ -111,21 +97,25 @@ export function init( rotation: number ) { // Init functions should not run after initialization - if (state.isInit) return; + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); - set_dimensions(width, height); - set_pos(posX, posY); - set_rotation(rotation); - - // Store the starting position in the actionLog - logAction('begin', getPositionWithRotation()); + const robot: Robot = { + x: posX, + y: posY, + rotation, + radius: 15 + }; - bounds = [ - {x: 0, y: 0}, - {x: width, y: 0}, - {x: width, y: height}, - {x: 0, y: height} - ]; + // Push the new map to state and make it the active map + state.activeMap = state.maps.push({ + width, + height, + robot, + areas: [], + areaLog: [], + actionLog: [{type: 'begin', position: Object.assign({}, robot)}], + message: 'Moved successfully!' + }) - 1; } /** @@ -141,7 +131,7 @@ export function create_area( flags: any[] ) { // Init functions should not run after initialization - if (state.isInit) return; + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); if (vertices.length % 2 !== 0) throw new Error('Odd number of vertice x-y coordinates given (expected even)'); @@ -174,7 +164,7 @@ export function create_area( } // Store the new area - state.areas.push({ + getMap().areas.push({ vertices: parsedVertices, isObstacle, flags: parsedFlags @@ -200,7 +190,7 @@ export function create_rect_area( flags: any[] ) { // Init functions should not run after initialization - if (state.isInit) return; + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); create_area([ x, y, @@ -219,7 +209,7 @@ export function create_obstacle( vertices: number[] ) { // Init functions should not run after initialization - if (state.isInit) return; + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); create_area(vertices, true, []); } @@ -239,7 +229,7 @@ export function create_rect_obstacle( height: number ) { // Init functions should not run after initialization - if (state.isInit) return; + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); create_rect_area(x, y, width, height, true, []); } @@ -261,20 +251,11 @@ export function complete_init() { * @returns the distance to the closest obstacle, or infinity (if robot is out of bounds) */ export function get_distance() : number { - // Check for all obstacles in the robot's path + // Check for all obstacles in the robot's path (including bounds) const obstacleCollisions: Collision[] = robot_raycast((area: Area) => area.isObstacle); // If an obstacle is found, return its distance - if (obstacleCollisions.length > 0) return obstacleCollisions[0].distance - robot.radius; - - // Find the distance to the bounds - const boundsCollision: Collision | null = robot_raycast_area({ - vertices: bounds, - isObstacle: true, - flags: {} - }); - - return boundsCollision === null ? Infinity : boundsCollision.distance - robot.radius; + return obstacleCollisions.length > 0 ? obstacleCollisions[0].distance - getRobot().radius : Infinity; } /** @@ -283,7 +264,7 @@ export function get_distance() : number { * @returns the color of the area under the robot */ export function get_color() : string { - return get_flags(robot.x, robot.y).color; + return getRobotFlags().color; } // ======= // @@ -298,6 +279,9 @@ export function get_color() : string { export function move_forward( distance: number ) { + // Get the robot + const robot = getRobot(); + // Check for all areas in the robot's path const collisions: Collision[] = robot_raycast() .filter(col => col.distance < distance + robot.radius); @@ -312,11 +296,11 @@ export function move_forward( const finalDistance = (col.distance - robot.radius + 1); // Move the robot to its final position - robot.x = robot.x + finalDistance * robot.dx; - robot.y = robot.y + finalDistance * robot.dy; + robot.x = robot.x + finalDistance * Math.cos(robot.rotation); + robot.y = robot.y + finalDistance * Math.sin(robot.rotation); // Update the final message - state.message = `Collided with wall at (${robot.x + col.distance * robot.dx},${robot.y + col.distance * robot.dy})`; + getMap().message = `Collided with wall at (${robot.x},${robot.y})`; // Throw an error to interrupt the simulation throw new Error('Collided with wall'); @@ -324,8 +308,8 @@ export function move_forward( } // Move the robot to its end position - robot.x = robot.x + distance * robot.dx; - robot.y = robot.y + distance * robot.dy; + robot.x = robot.x + distance * Math.cos(robot.rotation); + robot.y = robot.y + distance * Math.sin(robot.rotation); // Store the action in the actionLog logAction('move', getPositionWithRotation()); @@ -350,15 +334,14 @@ export function move_forward_to_wall() { export function rotate( angle: number ) { - let currentAngle = Math.atan2(-robot.dy, robot.dx); - - currentAngle -= angle; + // Get the robot + const robot = getRobot(); - robot.dx = Math.cos(currentAngle); - robot.dy = -Math.sin(currentAngle); + // Update robot rotation + robot.rotation -= angle; - if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; - if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; + // Normalise robot rotation within -pi and pi + robot.rotation = robot.rotation + (robot.rotation > Math.PI ? -2 * Math.PI : robot.rotation < -Math.PI ? 2 * Math.PI : 0); logAction('rotate', getPositionWithRotation()); } @@ -367,35 +350,14 @@ export function rotate( * Turn the robot 90 degrees to the left */ export function turn_left() { - let currentAngle = Math.atan2(-robot.dy, robot.dx); - - currentAngle += Math.PI / 2; - - robot.dx = Math.cos(currentAngle); - robot.dy = -Math.sin(currentAngle); - - // prevent floating point issues - if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; - if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - - logAction('rotate', getPositionWithRotation()); + rotate(Math.PI / 2); } /** * Turn the robot 90 degrees to the right */ export function turn_right() { - let currentAngle = Math.atan2(-robot.dy, robot.dx); - - currentAngle -= Math.PI / 2; - - robot.dx = Math.cos(currentAngle); - robot.dy = -Math.sin(currentAngle); - - if (robot.dx < 0.00001 && robot.dx > -0.00001) robot.dx = 0; - if (robot.dy < 0.00001 && robot.dy > -0.00001) robot.dy = 0; - - logAction('rotate', getPositionWithRotation()); + rotate(-Math.PI / 2); } // ======= // @@ -411,6 +373,23 @@ export function start_testing() { state.isComplete = true; } +/** + * Set the given map as the active map + * + * @param id index of the map in the array + */ +export function set_active_map( + id: number +) { + // Testing functions should only run after the simulation is complete + if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); + + // Confirm that map with the given id exists + if (id >= state.maps.length) throw new Error('Given map does not exist!'); + + state.activeMap = id; +} + /** * Checks if the robot's entered areas satisfy the callback * @@ -420,9 +399,9 @@ export function entered_areas( callback : (areas : Area[]) => boolean ) : boolean { // Testing functions should only run after the simulation is complete - if (!state.isComplete) return false; + if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); - return callback(state.areaLog); + return callback(getMap().areaLog); } /** @@ -435,13 +414,15 @@ export function entered_colors( colors: string[] ) : boolean { // Testing functions should only run after the simulation is complete - if (!state.isComplete) return false; + if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); + + return entered_areas(areas => { + const coloredAreas = areas + .filter(area => colors.includes(area.flags.color)) // Filter relevant colors + .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates - const coloredAreas = state.areaLog - .filter(area => colors.includes(area.flags.color)) // Filter relevant colors - .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates - - return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color + return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color + }); } // ================== @@ -450,64 +431,43 @@ export function entered_colors( // ================== // ================== -// ================== // -// DATA WRITE HELPERS // -// ================== // +// =========== // +// MAP HELPERS // +// =========== // /** - * Teleport the robot + * Get the active map * - * @param x coordinate of the robot - * @param y coordinate of the robot + * @returns the active map */ -function set_pos( - x: number, - y: number -) { - robot.x = x; - robot.y = y; +function getMap() : RobotMap { + return state.maps[state.activeMap]; } /** - * Set the rotation of the robot + * Get the active robot * - * @param rotation in radians + * @returns the active robot */ -function set_rotation( - rotation: number -) { - robot.dx = Math.cos(rotation); - robot.dy = -Math.sin(rotation); +function getRobot() : Robot { + return getMap().robot; } /** - * Set the width and height of the map + * Get the bound of the active map * - * @param width of the map - * @param height of the map + * @returns the bounds of the active map */ -function set_dimensions( - width: number, - height: number -) { - state.width = width; - state.height = height; -} - -// ================= // -// DATA READ HELPERS // -// ================= // +function getBounds() : Point[] { + // Get active map + const { width, height } = getMap(); -/** - * Gets the position of the robot - * - * @returns the position of the robot - */ -function getPosition(): Point { - return { - x: robot.x, - y: robot.y - }; + return [ + {x: 0, y: 0}, + {x: width, y: 0}, + {x: width, y: height}, + {x: 0, y: height} + ]; } /** @@ -516,8 +476,11 @@ function getPosition(): Point { * @returns the position of the robot (with rotation) */ function getPositionWithRotation(): PointWithRotation { - const angle = Math.atan2(-robot.dy, robot.dx); - return {x: robot.x, y: robot.y, rotation: angle}; + // Get the robot + const {x, y, rotation} = getRobot(); + + // Parse the robot + return {x, y, rotation}; } // ======================== // @@ -540,11 +503,11 @@ interface Collision { function robot_raycast( filter: (area: Area) => boolean = () => true ) : Collision[] { - return state.areas + return getMap().areas .filter(filter) // Apply filter .map(area => robot_raycast_area(area)) // Raycast each area on the map .concat([ - robot_raycast_area({vertices: bounds, isObstacle: true, flags: {}}) // Raycast map bounds as well + robot_raycast_area({vertices: getBounds(), isObstacle: true, flags: {}}) // Raycast map bounds as well ]) .filter(col => col !== null) // Remove null collisions .sort((a, b) => a.distance - b.distance); // Sort by distance @@ -560,17 +523,22 @@ function robot_raycast( function robot_raycast_area( area: Area ) : Collision | null { + // Get the robot + const robot = getRobot(); + + const dx = Math.cos(robot.rotation), dy = Math.sin(robot.rotation); + // raycast from 3 sources: left, middle, right const raycast_sources: Point[] = [-1, 0, 1] .map(mult => ({ - x: robot.x + mult * robot.radius * robot.dy, - y: robot.y + mult * robot.radius * robot.dx + x: robot.x + mult * robot.radius * dy, + y: robot.y + mult * robot.radius * dx })); // Raycast 3 times, one for each source const collisions: Collision[] = raycast_sources .map(source => raycast( - {origin: source, target: {x: robot.dx + source.x, y: robot.dy + source.y}}, area)) + {origin: source, target: {x: dx + source.x, y: dy + source.y}}, area)) .filter(col => col !== null); // Return null if no intersection @@ -641,7 +609,7 @@ function area_of_point( point: Point ) : Area | null { // Return the first area the point is within - for (const area of state.areas) { + for (const area of getMap().areas) { if (is_within_area(point, area)) return area; } @@ -729,7 +697,7 @@ function logAction( type: 'begin' | 'move' | 'rotate' | 'sensor', position: PointWithRotation ) { - state.actionLog.push({type, position}); + getMap().actionLog.push({type, position}); } /** @@ -740,12 +708,15 @@ function logAction( function logArea( area: Area ) { + // Get the area log + const areaLog = getMap().areaLog; + if ( - state.areaLog.length > 0 // Check for empty area log - && areaEquals(area, state.areaLog[state.areaLog.length - 1]) // Check if same area repeated + areaLog.length > 0 // Check for empty area log + && areaEquals(area, areaLog[areaLog.length - 1]) // Check if same area repeated ) return; - state.areaLog.push(area); + areaLog.push(area); } // ============ // @@ -790,7 +761,7 @@ const filterAdjacentDuplicateAreas = (area : Area, i : number, areas: Area[]) : * @param y coordinate * @returns the flags of the area containing (x, y) */ -function get_flags( +function getFlags( x: number, y: number ) : AreaFlags { @@ -799,3 +770,15 @@ function get_flags( return area === null ? {} : area.flags; } + +/** + * Gets the flags of the area containing the robot + * + * @returns the flags of the robot's area + */ +function getRobotFlags() : AreaFlags { + // Get the robot + const robot = getRobot(); + + return getFlags(robot.x, robot.y); +} diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index a706c8c10c..7efdfb2461 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -10,5 +10,5 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - start_testing, entered_areas, entered_colors + start_testing, set_active_map, entered_areas, entered_colors } from './functions'; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index d95f557ed4..f376f49ffd 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import type { Area, Action, PointWithRotation, RobotMap } from '../../../bundles/robot_minigame/functions'; +import type { Area, Action, PointWithRotation, Robot, RobotMinigame } from '../../../bundles/robot_minigame/functions'; /** * Calculate the acute angle between 2 angles @@ -60,14 +60,12 @@ const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { // Draw the robot const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWithRotation, size: number) => { - const centerX = x; - const centerY = y; - + // Save the background state ctx.save(); // translates the origin of the canvas to the center of the robot, then rotate - ctx.translate(centerX, centerY); - ctx.rotate(-rotation); + ctx.translate(x, y); + ctx.rotate(rotation); ctx.beginPath(); // Begin drawing robot @@ -84,7 +82,7 @@ const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWith ctx.lineTo(0, 0); ctx.stroke(); - // restore state of the background + // Restore the background state ctx.restore(); }; @@ -96,8 +94,7 @@ const drawAll = ( width : number, height : number, areas: Area[], - {x, y, rotation} : Robot, - robotSize: number + {x, y, rotation, radius: robotSize} : Robot ) => { ctx.reset(); drawBorders(ctx, width, height); @@ -105,12 +102,6 @@ const drawAll = ( drawRobot(ctx, {x, y, rotation}, robotSize); }; -interface Robot { - x: number - y: number - rotation: number -} - // The speed to move at const ANIMATION_SPEED : number = 2; @@ -120,24 +111,25 @@ const ANIMATION_SPEED : number = 2; interface MapProps { children?: never className?: never - state: RobotMap + state: RobotMinigame, } const RobotSimulation : React.FC = ({ - state: { - // isInit, + state: { maps } +}) => { + // Store the active map + const [active, setActive] = useState(0); + + // Retrieve the relevant map + const { width, height, + robot: {radius: robotSize}, areas, - // areaLog, actionLog, - message, - // success, - // messages, - robotSize, - debugLog - } -}) => { + message + } = maps[active]; + // Store animation status // 0 => Loaded / Loading // 1 => Running @@ -152,13 +144,10 @@ const RobotSimulation : React.FC = ({ const currentAction = useRef(1); // Store robot status - const robot = useRef({x: 0, y: 0, rotation: 0}); + const robot = useRef({x: 0, y: 0, rotation: 0, radius: 1}); // Ensure canvas is preloaded correctly useEffect(() => { - // DEBUG LOG REMOVE LATER - console.log(debugLog); - // Only load if animationStatus is 0 if (animationStatus !== 0) return; @@ -171,14 +160,14 @@ const RobotSimulation : React.FC = ({ currentAction.current = 1; // Reset robot position if action log has actions - if (actionLog.length > 0) robot.current = Object.assign({}, actionLog[0].position); + if (actionLog.length > 0) robot.current = Object.assign({}, {radius: robotSize}, actionLog[0].position); // Update canvas dimensions canvas.width = width; canvas.height = height; - drawAll(ctx, width, height, areas, robot.current, robotSize); - }, [animationStatus, width, height, areas, robotSize]); + drawAll(ctx, width, height, areas, robot.current); + }, [animationStatus]); // Handle animation useEffect(() => { @@ -254,28 +243,31 @@ const RobotSimulation : React.FC = ({ animationPauseUntil.current = Date.now() + 500; break; default: - robot.current = Object.assign({}, target); + robot.current = Object.assign({}, {radius: robot.current.radius}, target); } - drawAll(ctx, width, height, areas, robot.current, robotSize); + drawAll(ctx, width, height, areas, robot.current); }, 10); return () => clearInterval(interval); - }, [animationStatus, width, height, areas, robotSize]); + }, [animationStatus]); // Store a reference to the HTML canvas const canvasRef = useRef(null); return ( <> - {animationStatus === 0 - ? - : animationStatus === 1 - ? - : animationStatus === 2 - ? - : } - {animationStatus === 3 &&

{message}

} +
+ {maps.map((_, i) => )} + {animationStatus === 0 + ? + : animationStatus === 1 + ? + : animationStatus === 2 + ? + : } + {animationStatus === 3 && message} +
diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 3ce901675e..0b57309aa7 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useState } from 'react'; +import type { RobotMinigame } from '../../bundles/robot_minigame/functions'; import type { DebuggerContext } from '../../typings/type_helpers'; import RobotSimulation from './components/RobotSimulation'; From c63f2094e4463b4a1921cb7e11addecd921b021a Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 27 Mar 2025 10:43:18 +0800 Subject: [PATCH 27/46] Remove multiple map functionality --- src/bundles/robot_minigame/functions.ts | 104 ++++++------------ src/bundles/robot_minigame/index.ts | 2 +- .../RobotMaze/components/RobotSimulation.tsx | 16 +-- src/tabs/RobotMaze/index.tsx | 2 +- 4 files changed, 40 insertions(+), 84 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index de04c79301..7fc62492e8 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -48,7 +48,8 @@ export interface Robot extends PointWithRotation { radius: number } -export interface RobotMap { +export interface RobotMinigame { + isInit: boolean width: number height: number robot: Robot @@ -58,18 +59,16 @@ export interface RobotMap { message: string } -export interface RobotMinigame { - isInit: boolean - isComplete: boolean - activeMap: number - maps: RobotMap[] -} - +// Default state before initialisation const state: RobotMinigame = { isInit: false, - isComplete: false, - activeMap: -1, - maps: [] + width: 500, + height: 500, + robot: {x: 250, y: 250, rotation: 0, radius: 15}, + areas: [], + areaLog: [], + actionLog: [], + message: "" }; // sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime @@ -106,16 +105,18 @@ export function init( radius: 15 }; - // Push the new map to state and make it the active map - state.activeMap = state.maps.push({ - width, - height, - robot, - areas: [], - areaLog: [], - actionLog: [{type: 'begin', position: Object.assign({}, robot)}], - message: 'Moved successfully!' - }) - 1; + // Update the map's dimensions + state.width = width; + state.height = height; + + // Update the robot + state.robot = robot; + + // Update the action log with the robot's starting position + state.actionLog = [{type: 'begin', position: Object.assign({}, robot)}]; + + // Update the success message + state.message = "Please run this in the assessments tab!"; } /** @@ -164,7 +165,7 @@ export function create_area( } // Store the new area - getMap().areas.push({ + state.areas.push({ vertices: parsedVertices, isObstacle, flags: parsedFlags @@ -238,6 +239,8 @@ export function create_rect_obstacle( * Inform the simulator that the initialisation phase is complete */ export function complete_init() { + if (state.actionLog.length === 0) throw new Error("May not complete initialization without first running init()"); + state.isInit = true; } @@ -300,7 +303,7 @@ export function move_forward( robot.y = robot.y + finalDistance * Math.sin(robot.rotation); // Update the final message - getMap().message = `Collided with wall at (${robot.x},${robot.y})`; + state.message = `Collided with wall at (${robot.x},${robot.y})`; // Throw an error to interrupt the simulation throw new Error('Collided with wall'); @@ -364,32 +367,6 @@ export function turn_right() { // TESTING // // ======= // -/** - * Inform the simulator that the testing phase is starting - */ -export function start_testing() { - if (state.isComplete) throw new Error('May not start testing twice!'); - - state.isComplete = true; -} - -/** - * Set the given map as the active map - * - * @param id index of the map in the array - */ -export function set_active_map( - id: number -) { - // Testing functions should only run after the simulation is complete - if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); - - // Confirm that map with the given id exists - if (id >= state.maps.length) throw new Error('Given map does not exist!'); - - state.activeMap = id; -} - /** * Checks if the robot's entered areas satisfy the callback * @@ -398,10 +375,7 @@ export function set_active_map( export function entered_areas( callback : (areas : Area[]) => boolean ) : boolean { - // Testing functions should only run after the simulation is complete - if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); - - return callback(getMap().areaLog); + return callback(state.areaLog); } /** @@ -413,9 +387,6 @@ export function entered_areas( export function entered_colors( colors: string[] ) : boolean { - // Testing functions should only run after the simulation is complete - if (!state.isComplete) throw new Error('May not use testing functions before starting testing! Use start_testing() first!'); - return entered_areas(areas => { const coloredAreas = areas .filter(area => colors.includes(area.flags.color)) // Filter relevant colors @@ -435,22 +406,13 @@ export function entered_colors( // MAP HELPERS // // =========== // -/** - * Get the active map - * - * @returns the active map - */ -function getMap() : RobotMap { - return state.maps[state.activeMap]; -} - /** * Get the active robot * * @returns the active robot */ function getRobot() : Robot { - return getMap().robot; + return state.robot; } /** @@ -460,7 +422,7 @@ function getRobot() : Robot { */ function getBounds() : Point[] { // Get active map - const { width, height } = getMap(); + const { width, height } = state; return [ {x: 0, y: 0}, @@ -503,7 +465,7 @@ interface Collision { function robot_raycast( filter: (area: Area) => boolean = () => true ) : Collision[] { - return getMap().areas + return state.areas .filter(filter) // Apply filter .map(area => robot_raycast_area(area)) // Raycast each area on the map .concat([ @@ -609,7 +571,7 @@ function area_of_point( point: Point ) : Area | null { // Return the first area the point is within - for (const area of getMap().areas) { + for (const area of state.areas) { if (is_within_area(point, area)) return area; } @@ -697,7 +659,7 @@ function logAction( type: 'begin' | 'move' | 'rotate' | 'sensor', position: PointWithRotation ) { - getMap().actionLog.push({type, position}); + state.actionLog.push({type, position}); } /** @@ -709,7 +671,7 @@ function logArea( area: Area ) { // Get the area log - const areaLog = getMap().areaLog; + const areaLog = state.areaLog; if ( areaLog.length > 0 // Check for empty area log diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 7efdfb2461..18797ccf43 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -10,5 +10,5 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - start_testing, set_active_map, entered_areas, entered_colors + entered_areas, entered_colors } from './functions'; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index f376f49ffd..2cbc915698 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -115,21 +115,15 @@ interface MapProps { } const RobotSimulation : React.FC = ({ - state: { maps } -}) => { - // Store the active map - const [active, setActive] = useState(0); - - // Retrieve the relevant map - const { + state: { width, height, robot: {radius: robotSize}, areas, actionLog, message - } = maps[active]; - + } +}) => { // Store animation status // 0 => Loaded / Loading // 1 => Running @@ -258,7 +252,7 @@ const RobotSimulation : React.FC = ({ return ( <>
- {maps.map((_, i) => )} + {animationStatus === 0 ? : animationStatus === 1 @@ -266,7 +260,7 @@ const RobotSimulation : React.FC = ({ : animationStatus === 2 ? : } - {animationStatus === 3 && message} + {animationStatus === 3 && {message}}
diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 0b57309aa7..28c27b1601 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -38,7 +38,7 @@ export default { // !!! TEMPORARY DEBUGGING FUNCTION, REMOVE ONCE MODULE IS COMPLETE !!! console.log(context.context?.moduleContexts?.robot_minigame.state); - return context.context?.moduleContexts?.robot_minigame.state.isComplete; + return context.context?.moduleContexts?.robot_minigame.state.isInit; }, /** From 8e2ed88189aee7f288be9a7736b26be1b82c0593 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 27 Mar 2025 10:45:47 +0800 Subject: [PATCH 28/46] Lint and tsc --- src/bundles/robot_minigame/functions.ts | 6 +++--- src/tabs/RobotMaze/components/RobotSimulation.tsx | 4 ++-- src/tabs/RobotMaze/index.tsx | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 7fc62492e8..4e7291cf31 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -68,7 +68,7 @@ const state: RobotMinigame = { areas: [], areaLog: [], actionLog: [], - message: "" + message: '' }; // sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime @@ -116,7 +116,7 @@ export function init( state.actionLog = [{type: 'begin', position: Object.assign({}, robot)}]; // Update the success message - state.message = "Please run this in the assessments tab!"; + state.message = 'Please run this in the assessments tab!'; } /** @@ -239,7 +239,7 @@ export function create_rect_obstacle( * Inform the simulator that the initialisation phase is complete */ export function complete_init() { - if (state.actionLog.length === 0) throw new Error("May not complete initialization without first running init()"); + if (state.actionLog.length === 0) throw new Error('May not complete initialization without first running init()'); state.isInit = true; } diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 2cbc915698..a884dd5645 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -115,7 +115,7 @@ interface MapProps { } const RobotSimulation : React.FC = ({ - state: { + state: { width, height, robot: {radius: robotSize}, @@ -252,7 +252,7 @@ const RobotSimulation : React.FC = ({ return ( <>
- + {animationStatus === 0 ? : animationStatus === 1 diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 28c27b1601..1b1a65adde 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,5 +1,4 @@ -import React, { useState } from 'react'; -import type { RobotMinigame } from '../../bundles/robot_minigame/functions'; +import React from 'react'; import type { DebuggerContext } from '../../typings/type_helpers'; import RobotSimulation from './components/RobotSimulation'; From 631fd231a84c599ed5a10688b81e831f39b0c62a Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Mon, 31 Mar 2025 13:32:39 +0800 Subject: [PATCH 29/46] Predeclare tests in prepend, run_tests() in postpend --- src/bundles/robot_minigame/functions.ts | 81 +++++++++++++------ src/bundles/robot_minigame/index.ts | 2 +- .../RobotMaze/components/RobotSimulation.tsx | 31 ++++--- 3 files changed, 79 insertions(+), 35 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 4e7291cf31..d50b00fe09 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -12,10 +12,14 @@ interface Point { } // A point (x, y) with rotation -export interface PointWithRotation extends Point { +interface PointWithRotation extends Point { rotation: number } +export interface Robot extends PointWithRotation { + radius: number +} + // A line segment between p1 and p2 interface LineSegment { p1: Point @@ -44,8 +48,14 @@ export interface Area { flags: AreaFlags } -export interface Robot extends PointWithRotation { - radius: number +interface Test { + type: string + test: Function +} + +interface AreaTest extends Test { + type: 'area' + test: (areas: Area[]) => boolean } export interface RobotMinigame { @@ -56,6 +66,7 @@ export interface RobotMinigame { areas: Area[] areaLog: Area[] actionLog: Action[] + tests: Test[] message: string } @@ -68,6 +79,7 @@ const state: RobotMinigame = { areas: [], areaLog: [], actionLog: [], + tests: [], message: '' }; @@ -235,6 +247,27 @@ export function create_rect_obstacle( create_rect_area(x, y, width, height, true, []); } +/** + * Check if the robot has entered different areas with the given colors in order + * + * @param colors in the order visited + * @returns if the robot entered the given colors in order + */ +export function should_enter_colors( + colors: string[] +) { + state.tests.push({ + type: 'area', + test: (areas: Area[]) => { + const coloredAreas = areas + .filter((area: Area) => colors.includes(area.flags.color)) // Filter relevant colors + .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates + + return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color + } + } as AreaTest); +} + /** * Inform the simulator that the initialisation phase is complete */ @@ -368,32 +401,30 @@ export function turn_right() { // ======= // /** - * Checks if the robot's entered areas satisfy the callback + * Run the stored tests in state * - * @returns if the entered areas satisfy the callback + * @returns if all tests pass */ -export function entered_areas( - callback : (areas : Area[]) => boolean -) : boolean { - return callback(state.areaLog); -} +export function run_tests() : boolean { + // Run each test in order + for (const test of state.tests) { + // Store status in a variable + let success: boolean; + + switch(test.type) { + case 'area': + success = test.test(state.areaLog); + break; + default: + success = true; + } -/** - * Check if the robot has entered different areas with the given colors in order - * - * @param colors in the order visited - * @returns if the robot entered the given colors in order - */ -export function entered_colors( - colors: string[] -) : boolean { - return entered_areas(areas => { - const coloredAreas = areas - .filter(area => colors.includes(area.flags.color)) // Filter relevant colors - .filter(filterAdjacentDuplicateAreas); // Filter adjacent duplicates + // If the test fails, return false + if (!success) return false; + } - return coloredAreas.length === colors.length && coloredAreas.every(({ flags: { color } }, i) => color === colors[i]); // Check if each area has the expected color - }); + // If all tests pass, return true + return true; } // ================== diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 18797ccf43..a6b936dcae 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -10,5 +10,5 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - entered_areas, entered_colors + should_enter_colors, run_tests } from './functions'; diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index a884dd5645..0b1b6b1966 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import type { Area, Action, PointWithRotation, Robot, RobotMinigame } from '../../../bundles/robot_minigame/functions'; +import type { Area, Action, Robot, RobotMinigame } from '../../../bundles/robot_minigame/functions'; /** * Calculate the acute angle between 2 angles @@ -8,7 +8,10 @@ import type { Area, Action, PointWithRotation, Robot, RobotMinigame } from '../. * @param current rotation * @returns the acute angle between */ -const smallestAngle = (target, current) => { +const smallestAngle = ( + target: number, + current: number +) => { const dr = (target - current) % (2 * Math.PI); if (dr > 0 && dr > Math.PI) return dr - (2 * Math.PI); @@ -24,7 +27,11 @@ const smallestAngle = (target, current) => { * @param width of the map * @param height of the map */ -const drawBorders = (ctx: CanvasRenderingContext2D, width: number, height: number) => { +const drawBorders = ( + ctx: CanvasRenderingContext2D, + width: number, + height: number +) => { ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, height); @@ -38,7 +45,10 @@ const drawBorders = (ctx: CanvasRenderingContext2D, width: number, height: numbe }; // Draw the areas of the map -const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { +const drawAreas = ( + ctx: CanvasRenderingContext2D, + areas: Area[] +) => { for (const { vertices, isObstacle, flags } of areas) { ctx.beginPath(); ctx.moveTo(vertices[0].x, vertices[0].y); @@ -59,7 +69,10 @@ const drawAreas = (ctx: CanvasRenderingContext2D, areas: Area[]) => { }; // Draw the robot -const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWithRotation, size: number) => { +const drawRobot = ( + ctx: CanvasRenderingContext2D, + { x, y, rotation, radius }: Robot +) => { // Save the background state ctx.save(); @@ -69,7 +82,7 @@ const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWith ctx.beginPath(); // Begin drawing robot - ctx.arc(0, 0, size, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); // Full circle (0 to 2π radians) ctx.fillStyle = 'black'; // Set the fill color ctx.fill(); // Fill the circle @@ -78,7 +91,7 @@ const drawRobot = (ctx: CanvasRenderingContext2D, { x, y, rotation } : PointWith ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.beginPath(); - ctx.moveTo(size, 0); + ctx.moveTo(radius, 0); ctx.lineTo(0, 0); ctx.stroke(); @@ -94,12 +107,12 @@ const drawAll = ( width : number, height : number, areas: Area[], - {x, y, rotation, radius: robotSize} : Robot + robot : Robot ) => { ctx.reset(); drawBorders(ctx, width, height); drawAreas(ctx, areas); - drawRobot(ctx, {x, y, rotation}, robotSize); + drawRobot(ctx, robot); }; // The speed to move at From 38102806fbb3a3f23332da8697b026f47851cba3 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Mon, 31 Mar 2025 17:43:20 +0800 Subject: [PATCH 30/46] Separate type declarations, run_tests(areaLog, tests) for simulation, and run_all_tests() for testcases into separate files --- src/bundles/robot_minigame/functions.ts | 84 +++---------------- src/bundles/robot_minigame/index.ts | 2 +- src/bundles/robot_minigame/tests.ts | 34 ++++++++ src/bundles/robot_minigame/types.ts | 53 ++++++++++++ .../RobotMaze/components/RobotSimulation.tsx | 7 +- 5 files changed, 104 insertions(+), 76 deletions(-) create mode 100644 src/bundles/robot_minigame/tests.ts create mode 100644 src/bundles/robot_minigame/types.ts diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index d50b00fe09..cce1d520c6 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -5,20 +5,14 @@ import context from 'js-slang/context'; // type List // } from 'js-slang/dist/stdlib/list'; -// A point (x, y) -interface Point { - x: number - y: number -} - -// A point (x, y) with rotation -interface PointWithRotation extends Point { - rotation: number -} - -export interface Robot extends PointWithRotation { - radius: number -} +import { run_tests } from './tests'; +import type { + Point, PointWithRotation, Robot, + Action, + AreaFlags, Area, + AreaTest, + RobotMinigame +} from './types'; // A line segment between p1 and p2 interface LineSegment { @@ -32,44 +26,6 @@ interface Ray { target: Point } -// A stored action -export interface Action { - type: 'begin' | 'move' | 'rotate' | 'sensor' - position: PointWithRotation -} - -interface AreaFlags { - [name: string]: any -} - -export interface Area { - vertices: Point[] - isObstacle: boolean - flags: AreaFlags -} - -interface Test { - type: string - test: Function -} - -interface AreaTest extends Test { - type: 'area' - test: (areas: Area[]) => boolean -} - -export interface RobotMinigame { - isInit: boolean - width: number - height: number - robot: Robot - areas: Area[] - areaLog: Area[] - actionLog: Action[] - tests: Test[] - message: string -} - // Default state before initialisation const state: RobotMinigame = { isInit: false, @@ -405,26 +361,8 @@ export function turn_right() { * * @returns if all tests pass */ -export function run_tests() : boolean { - // Run each test in order - for (const test of state.tests) { - // Store status in a variable - let success: boolean; - - switch(test.type) { - case 'area': - success = test.test(state.areaLog); - break; - default: - success = true; - } - - // If the test fails, return false - if (!success) return false; - } - - // If all tests pass, return true - return true; +export function run_all_tests() : boolean { + return run_tests(state); } // ================== @@ -690,7 +628,7 @@ function logAction( type: 'begin' | 'move' | 'rotate' | 'sensor', position: PointWithRotation ) { - state.actionLog.push({type, position}); + state.actionLog.push({type, position} as Action); } /** diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index a6b936dcae..43c0cfc790 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -10,5 +10,5 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, get_distance, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, - should_enter_colors, run_tests + should_enter_colors, run_all_tests } from './functions'; diff --git a/src/bundles/robot_minigame/tests.ts b/src/bundles/robot_minigame/tests.ts new file mode 100644 index 0000000000..afccca29b7 --- /dev/null +++ b/src/bundles/robot_minigame/tests.ts @@ -0,0 +1,34 @@ +import type { Area, Test } from './types'; + +/** + * Run the stored tests in state + * + * @returns if all tests pass + */ +export function run_tests({ + areaLog, + tests +}: { + areaLog: Area[], + tests: Test[] +}) : boolean { + // Run each test in order + for (const test of tests) { + // Store status in a variable + let success: boolean; + + switch(test.type) { + case 'area': + success = test.test(areaLog); + break; + default: + success = true; + } + + // If the test fails, return false + if (!success) return false; + } + + // If all tests pass, return true + return true; +} diff --git a/src/bundles/robot_minigame/types.ts b/src/bundles/robot_minigame/types.ts new file mode 100644 index 0000000000..ea0820fcc2 --- /dev/null +++ b/src/bundles/robot_minigame/types.ts @@ -0,0 +1,53 @@ +// A point (x, y) +export interface Point { + x: number + y: number +} + +// A point (x, y) with rotation +export interface PointWithRotation extends Point { + rotation: number +} + +// The robot with position and radius +export interface Robot extends PointWithRotation { + radius: number +} + +// A stored action +export interface Action { + type: 'begin' | 'move' | 'rotate' | 'sensor' + position: PointWithRotation +} + +export interface AreaFlags { + [name: string]: any +} + +export interface Area { + vertices: Point[] + isObstacle: boolean + flags: AreaFlags +} + +export interface Test { + type: string + test: Function +} + +export interface AreaTest extends Test { + type: 'area' + test: (areas: Area[]) => boolean +} + +export interface RobotMinigame { + isInit: boolean + width: number + height: number + robot: Robot + areas: Area[] + areaLog: Area[] + actionLog: Action[] + tests: Test[] + message: string +} diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 0b1b6b1966..f4953d344e 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import type { Area, Action, Robot, RobotMinigame } from '../../../bundles/robot_minigame/functions'; +import { run_tests } from '../../../bundles/robot_minigame/tests'; +import type { Area, Action, Robot, RobotMinigame } from '../../../bundles/robot_minigame/types'; /** * Calculate the acute angle between 2 angles @@ -134,6 +135,8 @@ const RobotSimulation : React.FC = ({ robot: {radius: robotSize}, areas, actionLog, + areaLog, + tests, message } }) => { @@ -273,7 +276,7 @@ const RobotSimulation : React.FC = ({ : animationStatus === 2 ? : } - {animationStatus === 3 && {message}} + {animationStatus === 3 && {run_tests({tests, areaLog}) ? 'Success! 🎉' : message}}
From 702a842814137436f1695d506332d05a14bf6275 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 2 Apr 2025 15:12:18 +0800 Subject: [PATCH 31/46] Add sense_obstacle() --- src/bundles/robot_minigame/functions.ts | 10 ++++++++++ src/bundles/robot_minigame/index.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index cce1d520c6..9fe097edb6 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -250,6 +250,16 @@ export function get_distance() : number { return obstacleCollisions.length > 0 ? obstacleCollisions[0].distance - getRobot().radius : Infinity; } +// The maximum distance the robot can detect obstacles at +const SENSOR_RANGE: number = 15; + +/** + * Check if there is an obstacle within a predefined distance from the robot + */ +export function sense_obstacle() : boolean { + return get_distance() > SENSOR_RANGE; +} + /** * Get the color of the area under the robot * diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 43c0cfc790..ed5107f336 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -8,7 +8,7 @@ export { init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, - get_distance, get_color, + get_distance, sense_obstacle, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, should_enter_colors, run_all_tests } from './functions'; From 606e82351c8f54a4a38ef03117b8af1db12c812c Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 2 Apr 2025 15:22:48 +0800 Subject: [PATCH 32/46] Add @esbuild/linux-64 dependency --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 95d5d90576..96ac9bb01e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@commander-js/extra-typings": "^12.0.0", + "@esbuild/linux-x64": "^0.25.2", "@stylistic/eslint-plugin": "^1.7.0", "@types/dom-mediacapture-record": "^1.0.11", "@types/estree": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 114bca87d4..73772fc22b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -669,6 +669,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== +"@esbuild/linux-x64@^0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb" + integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg== + "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" From 8568f8ecd7926439ea6e73e882959a96b1f09795 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 2 Apr 2025 15:36:48 +0800 Subject: [PATCH 33/46] Remove @esbuild/linux-x64 dev dependency --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index 96ac9bb01e..95d5d90576 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ }, "devDependencies": { "@commander-js/extra-typings": "^12.0.0", - "@esbuild/linux-x64": "^0.25.2", "@stylistic/eslint-plugin": "^1.7.0", "@types/dom-mediacapture-record": "^1.0.11", "@types/estree": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 73772fc22b..114bca87d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -669,11 +669,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== -"@esbuild/linux-x64@^0.25.2": - version "0.25.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb" - integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg== - "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" From 6a4154c74d8f6927e7766b4d1da1e80e62557ac1 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 2 Apr 2025 15:52:42 +0800 Subject: [PATCH 34/46] Remove scripts/bin.js and add /dist as in #314 --- package.json | 2 +- scripts/bin.js | 38 -------------------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 scripts/bin.js diff --git a/package.json b/package.json index 95d5d90576..e5e6ef2f26 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "lint": "yarn scripts lint", "prepare": "husky", "postinstall": "patch-package && yarn scripts:build", - "scripts": "node --max-old-space-size=4096 scripts/bin.js", + "scripts": "node --max-old-space-size=4096 scripts/dist/bin.js", "serve": "http-server --cors=* -c-1 -p 8022 ./build", "template": "yarn scripts template", "test": "yarn scripts test", diff --git a/scripts/bin.js b/scripts/bin.js deleted file mode 100644 index 9dbe766b43..0000000000 --- a/scripts/bin.js +++ /dev/null @@ -1,38 +0,0 @@ -import{Command as dr}from"@commander-js/extra-typings";import{Command as Vt}from"@commander-js/extra-typings";import{Option as wt}from"@commander-js/extra-typings";import bt from"fs/promises";async function D(e){try{let t=await bt.readFile(e,"utf-8");return JSON.parse(t)}catch(t){throw t.code==="ENOENT"?new Error(`Could not locate manifest file at ${e}`):t}}var S=class extends wt{default(t,r){return super.default(t,r)}},h=new S("--srcDir ","Location of the source files").default("src"),U=new S("--outDir ","Location of output directory").default("build"),P=new S("--manifest ","Location of manifest").default("modules.json"),We=new S("--lint","Run ESLint"),ce=new S("--fix","Fix automatically fixable linting errors").implies({lint:!0}),O=new S("-b, --bundles ","Manually specify which bundles").default(null),L=new S("-t, --tabs ","Manually specify which tabs").default(null);function p(...e){return Promise.all(e)}function z(e){return async(...t)=>{let r=performance.now();return{result:await e(...t),elapsed:performance.now()-r}}}function Je(e){return Object.entries(e)}var k=async(e,t,r,n=!0)=>{let i=await D(e),o=Object.keys(i),s=Object.values(i).flatMap(f=>f.tabs),a=[],d=[],l=f=>f==null;function b(){let f=t.filter(N=>!o.includes(N));if(f.length>0)throw new Error(`Unknown bundles: ${f.join(", ")}`);a=a.concat(t),n&&(d=[...d,...t.flatMap(N=>i[N].tabs)])}function m(){let f=r.filter(N=>!s.includes(N));if(f.length>0)throw new Error(`Unknown tabs: ${f.join(", ")}`);d=d.concat(r)}function x(){a=a.concat(o)}function $(){d=d.concat(s)}return l(t)&&l(r)?(x(),$()):(t!==null&&b(),r!==null&&m()),{bundles:[...new Set(a)],tabs:[...new Set(d)],modulesSpecified:!l(t)}};async function Ue(e,t){let r=await D(e),n=Object.keys(r);if(t===null)return{bundles:n,modulesSpecified:!1};let i=t.filter(o=>!n.includes(o));if(i.length>0)throw new Error(`Unknown bundles: ${i.join(", ")}`);return{bundles:[...new Set(t)],modulesSpecified:!0}}async function ze(e,t){let r=await D(e),n=Object.values(r).flatMap(o=>o.tabs);if(t===null)return{tabs:n};let i=t.filter(o=>!n.includes(o));if(i.length>0)throw new Error(`Unknown tabs: ${i.join(", ")}`);return{tabs:[...new Set(t)]}}import{copyFile as Ct}from"fs/promises";import{Command as $t}from"@commander-js/extra-typings";import c from"chalk";import{Table as Pt}from"console-table-printer";import{Command as xt}from"@commander-js/extra-typings";import Y from"chalk";import*as de from"typedoc";async function j(e,t,r,n){let i=await de.Application.bootstrap({categorizeByGroup:!0,entryPoints:A(t,e),excludeInternal:!0,logLevel:r?"Info":"Error",name:"Source Academy Modules",readme:"./scripts/src/build/docs/docsreadme.md",tsconfig:`${t}/tsconfig.json`,skipErrorChecking:!0,preserveWatchOutput:n},[new de.TSConfigReader]);if(n)return[null,i];let o=await i.convert();if(!o)throw new Error("Failed to initialize typedoc - Make sure to check that the source files have no compilation errors!");return[o,i]}var ee=z(async(e,t,[r,n])=>{if(e.modulesSpecified)return{severity:"warn",error:"Not all modules were built, skipping building HTML documentation"};try{return await n.generateDocs(r,`${t}/documentation`),{severity:"success"}}catch(i){return{severity:"error",error:i}}});function xe({result:e,elapsed:t}){let r=`${(t/1e3).toFixed(2)}s`;switch(e.severity){case"success":return`${Y.cyanBright("Built HTML documentation")} ${Y.greenBright("successfully")} in ${r}`;case"warn":return Y.yellowBright(e.error);case"error":return`${Y.redBright("Failed")} ${Y.cyanBright("to build HTML documentation: ")} ${e.error}`}}var he=()=>new xt("html").addOption(h).addOption(U).addOption(P).option("-v, --verbose").action(async e=>{let t=await k(e.manifest,null,[],!1),r=await j(t.bundles,e.srcDir,e.verbose,!1),n=await ee(t,e.outDir,r);console.log(xe(n)),n.result.severity==="error"&&process.exit(1)});import V from"chalk";import{loadESLint as vt}from"eslint";import{Command as ht}from"@commander-js/extra-typings";function me(e,t){return new ht(e).description(t).addOption(h).addOption(P).addOption(O).addOption(L)}function pe(e,t){return async r=>{console.log(te(r,{}));let n=await e(r),i=t(n);console.log(i),n.result.severity==="error"&&process.exit(1)}}var fe=z(async({bundles:e,tabs:t,srcDir:r,fix:n})=>{let i=await vt({useFlatConfig:!0}),o=new i({fix:n}),s=[...e.map(a=>`${r}/bundles/${a}/**/*.ts`),...t.map(a=>`${r}/tabs/${a}/**/*.ts*`)];try{let a=await o.lintFiles(s);n&&await i.outputFixes(a);let l=await(await o.loadFormatter("stylish")).format(a),b=K(a,({warningCount:m,errorCount:x,fatalErrorCount:$})=>!n&&$+x>0||n&&$>0?"error":m>0?"warn":"success");return{formatted:l,severity:b}}catch(a){return{severity:"error",formatted:a.toString()}}});function ve({elapsed:e,result:{formatted:t,severity:r}}){let n;return r==="error"?n=V.cyanBright("with ")+V.redBright("errors"):r==="warn"?n=V.cyanBright("with ")+V.yellowBright("warnings"):n=V.greenBright("successfully"),`${V.cyanBright(`Linting completed in ${q(e,1e3)}s ${n}:`)} -${t}`}var Tt=pe((...e)=>fe(...e),ve);function Ve(){return me("lint","Run eslint").addOption(ce).action(async e=>{let t=await k(e.manifest,e.bundles,e.tabs,!1);await Tt({...e,...t})})}import Bt from"fs/promises";import Ke from"path";import Q from"chalk";import Z from"typescript";async function Rt(e){let t=Ke.join(e,"tsconfig.json");try{let r=await Bt.readFile(t,"utf-8"),{error:n,config:i}=Z.parseConfigFileTextToJson(t,r);if(n)return{severity:"error",results:[n]};let{errors:o,options:s}=Z.parseJsonConfigFileContent(i,Z.sys,e);return o.length>0?{severity:"error",results:o}:{severity:"success",results:s}}catch(r){return{severity:"error",error:r}}}var ge=z(async({bundles:e,tabs:t,srcDir:r})=>{let n=await Rt(r);if(n.severity==="error")return n;let i=[];e.length>0&&A(r,e).forEach(o=>i.push(o)),t.length>0&&G(r,t).forEach(o=>i.push(o));try{let o=Z.createProgram(i,n.results),s=o.emit(),a=Z.getPreEmitDiagnostics(o).concat(s.diagnostics);return{severity:a.length>0?"error":"success",results:a}}catch(o){return{severity:"error",error:o}}});function Te({elapsed:e,result:t}){if(t.severity==="error"&&t.error)return`${Q.cyanBright(`tsc finished with ${Q.redBright("errors")} in ${q(e,1e3)}s: ${t.error}`)}`;let r=Z.formatDiagnosticsWithColorAndContext(t.results,{getNewLine:()=>` -`,getCurrentDirectory:()=>process.cwd(),getCanonicalFileName:n=>Ke.basename(n)});return t.severity==="error"?`${r} -${Q.cyanBright(`tsc finished with ${Q.redBright("errors")} in ${q(e,1e3)}s`)}`:`${r} -${Q.cyanBright(`tsc completed ${Q.greenBright("successfully")} in ${q(e,1e3)}s`)}`}var Ot=pe((...e)=>ge(...e),Te),qe=()=>me("tsc","Run the typescript compiler to perform type checking").action(async e=>{let t=await k(e.manifest,e.bundles,e.tabs,!0);await Ot({...e,...t})});async function Be(e,t,{tsc:r,lint:n,...i}){let o={...i,bundles:e,tabs:t};if(r){if(!n){let l=await ge(o);return{tsc:l,severity:l.result.severity}}let[s,a]=await p(ge(o),fe(o)),d=K([s,a],({result:{severity:l}})=>l);return{tsc:s,lint:a,severity:d}}if(n){let s=await fe(o);return{lint:s,severity:s.result.severity}}return null}function Qe(e){let t=[];if(e.tsc&&t.push(Te(e.tsc)),e.lint){let r=ve(e.lint);t.push(r)}return t.length>0?t.join(` -`):null}var kt=e=>e.severity==="success",Ze=e=>e.severity==="warn";function K(e,t){let r="success";for(let n of e){let i;if("severity"in n)i=n.severity;else{if(!t)throw new Error(`Mapping function required to convert ${n} to severity`);i=t(n)}if(i==="error")return"error";i==="warn"&&(r="warn")}return r}var A=(e,t)=>t.map(r=>`${e}/bundles/${r}/index.ts`),G=(e,t)=>t.map(r=>`${e}/tabs/${r}/index.tsx`),q=(e,t)=>(e/t).toFixed(2);function re(e,t,r){let n=s=>s.severity!=="success",i=Je(e).map(([s,a])=>{if(s==="html")return[a.result.severity,xe(a)];let d=K(a),l=s[0].toUpperCase()+s.slice(1);if(!t)return d==="success"?["success",`${c.cyanBright(`${l} built`)} ${c.greenBright("successfully")} -`]:d==="warn"?["warn",c.cyanBright(`${l} built with ${c.yellowBright("warnings")}: -${a.filter(Ze).map(({name:m,error:x},$)=>c.yellowBright(`${$+1}. ${m}: ${x}`)).join(` -`)} -`)]:["error",c.cyanBright(`${l} build ${c.redBright("failed")} with errors: -${a.filter(n).map(({name:m,error:x,severity:$},f)=>$==="error"?c.redBright(`${f+1}. Error ${m}: ${x}`):c.yellowBright(`${f+1}. Warning ${m}: ${x}`)).join(` -`)} -`)];let b=new Pt({columns:[{name:"name",title:l},{name:"severity",title:"Status"},{name:"error",title:"Errors"}]});return a.forEach(m=>{Ze(m)?b.addRow({...m,severity:"Warning"},{color:"yellow"}):kt(m)?b.addRow({...m,error:"-",severity:"Success"},{color:"green"}):b.addRow({...m,severity:"Error"},{color:"red"})}),d==="success"?["success",`${c.cyanBright(`${l} built`)} ${c.greenBright("successfully")}: -${b.render()} -`]:d==="warn"?["warn",`${c.cyanBright(`${l} built`)} with ${c.yellowBright("warnings")}: -${b.render()} -`]:["error",`${c.cyanBright(`${l} build ${c.redBright("failed")} with errors`)}: -${b.render()} -`]});console.log(i.map(s=>s[1]).join(` -`));let o=K(i,([s])=>s);return o==="error"&&r&&process.exit(1),o}function te({bundles:e,tabs:t},{tsc:r,lint:n},i){let o=[];return r&&o.push(c.yellowBright("--tsc specified, will run typescript checker")),n&&o.push(c.yellowBright("Linting specified, will run ESlint")),i!=="bundles"&&e.length>0&&(o.push(c.magentaBright("Processing the following bundles:")),e.forEach((s,a)=>o.push(`${a+1}. ${s}`))),i!=="tabs"&&t.length>0&&(o.push(c.magentaBright("Processing the following tabs:")),t.forEach((s,a)=>o.push(`${a+1}. ${s}`))),o.join(` -`)}function v(e,t){return async r=>{let n;switch(t){case"bundles":{n=await ze(r.manifest,r.tabs);break}case"tabs":{n=await Ue(r.manifest,r.bundles);break}case void 0:{n=await k(r.manifest,r.bundles,r.tabs);break}}console.log(te(n,r,t));let i=await Be(n.bundles,n.tabs,{lint:r.lint,fix:r.fix,tsc:r.tsc,srcDir:r.srcDir});if(i!==null){let s=Qe(i);console.log(s),i.severity==="error"&&process.exit(1)}let o=await e(n,r);re(o,r.verbose,!0),await Ct(r.manifest,`${r.outDir}/modules.json`)}}function C(e,t){return new $t(e).description(t).addOption(h).addOption(U).addOption(We).addOption(ce).addOption(P).option("--tsc","Run tsc before building")}import Xe from"fs/promises";import*as ne from"typedoc";var Re=e=>{var t=//g,n=/\t|\r|\uf8ff/g,i=/\\([\\\|`*_{}\[\]()#+\-~])/g,o=/^([*\-=_] *){3,}$/gm,s=/\n *> *([^]*?)(?=(\n|$){2})/g,a=/\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g,d=/<\/(ol|ul)>\n\n<\1>/g,l=/(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+)|`)(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g,b=/\n((```|~~~).*\n?([^]*?)\n?\2|(( {4}.*?\n)+))/g,m=/((!?)\[(.*?)\]\((.*?)( ".*")?\)|\\([\\`*_{}\[\]()#+\-.!~]))/g,x=/\n(( *\|.*?\| *\n)+)/g,$=/^.*\n( *\|( *\:?-+\:?-+\:? *\|)* *\n|)/,f=/.*\n/g,N=/\||(.*?[^\\])\|/g,mt=/(?=^|>|\n)([>\s]*?)(#{1,6}) (.*?)( #*)? *(?=\n|$)/g,pt=/(?=^|>|\n)\s*\n+([^<]+?)\n+\s*(?=\n|<|$)/g,ft=/-\d+\uf8ff/g;function T(u,g){e=e.replace(u,g)}function B(u,g){return"<"+u+">"+g+""}function Le(u){return u.replace(s,function(g,w){return B("blockquote",Le(H(w.replace(/^ *> */gm,""))))})}function _e(u){return u.replace(a,function(g,w,y,R,X,I){var J=B("li",H(I.split(RegExp(` - ?`+w+"(?:(?:\\d+|[a-zA-Z])[.)]|[*\\-+]) +","g")).map(_e).join("
  • ")));return` -`+(y?'
      ':parseInt(y,36)-9+'" style="list-style-type:'+(X?"low":"upp")+'er-alpha">')+J+"
    ":B("ul",J))})}function H(u){return u.replace(l,function(g,w,y,R,X,I,J,gt,Fe,yt){return w+B(R?Fe?"strong":"em":X?Fe?"s":"sub":I?"sup":J?"small":gt?"big":"code",H(yt))})}function le(u){return u.replace(i,"$1")}var we=[],ue=0;return e=` -`+e+` -`,T(t,"<"),T(r,">"),T(n," "),e=Le(e),T(o,"
    "),e=_e(e),T(d,""),T(b,function(u,g,w,y,R){return we[--ue]=B("pre",B("code",y||R.replace(/^ {4}/gm,""))),ue+"\uF8FF"}),T(m,function(u,g,w,y,R,X,I){return we[--ue]=R?w?''+y+'':''+le(H(y))+"":I,ue+"\uF8FF"}),T(x,function(u,g){var w=g.match($)[1];return` -`+B("table",g.replace(f,function(y,R){return y==w?"":B("tr",y.replace(N,function(X,I,J){return J?B(w&&!R?"th":"td",le(H(I||""))):""}))}))}),T(mt,function(u,g,w,y){return g+B("h"+w.length,le(H(y)))}),T(pt,function(u,g){return B("p",le(H(g)))}),T(ft,function(u){return we[parseInt(u)]}),e.trim()};var Oe=e=>e.stringify(ne.TypeContext.none),jt={[ne.ReflectionKind.Function](e){let[t]=e.signatures,r;t.comment?r=Re(t.comment.summary.map(({text:i})=>i).join("")):r="No description available";let n=t.parameters.map(({type:i,name:o})=>[o,Oe(i)]);return{kind:"function",name:e.name,description:r,params:n,retType:Oe(t.type)}},[ne.ReflectionKind.Variable](e){let t;return e.comment?t=Re(e.comment.summary.map(({text:r})=>r).join("")):t="No description available",{kind:"variable",name:e.name,description:t,type:Oe(e.type)}}};async function Ge(e,t,r){try{let n=t.children.reduce((i,o)=>{let s=jt[o.kind];return{...i,[o.name]:s?s(o):{kind:"unknown"}}},{});return await Xe.writeFile(`${r}/jsons/${e}.json`,JSON.stringify(n,null,2)),{name:e,severity:"success"}}catch(n){return{name:e,severity:"error",error:n}}}var ie=async({bundles:e},t,r)=>{if(await Xe.mkdir(`${t}/jsons`,{recursive:!0}),e.length===1){let[i]=e;return{jsons:[await Ge(i,r,t)]}}return{jsons:await Promise.all(e.map(i=>Ge(i,r.getChildByName(i),t)))}},St=v(async(e,{srcDir:t,outDir:r,verbose:n})=>{let[i]=await j(e.bundles,t,n,!1);return ie(e,r,i)},"tabs"),Ce=()=>C("jsons","Build json documentation").addOption(O).action(e=>St({...e,tabs:[]}));async function $e(e,t,r){let[n,i]=await Promise.all([ie(e,t,r[0]),ee(e,t,r)]);return{...n,html:i}}var Et=v(async(e,{srcDir:t,outDir:r,verbose:n})=>{let i=await j(e.bundles,t,n,!1);return $e(e,r,i)},"tabs"),Ye=()=>C("docs","Build HTML and json documentation").addOption(O).action(e=>Et({...e,tabs:[]}));import It from"fs/promises";import{build as Lt}from"esbuild";import Dt from"fs/promises";import At from"path";import{parse as Mt}from"acorn";import{generate as Nt}from"astring";var M={bundle:!0,format:"iife",define:{process:JSON.stringify({env:{NODE_ENV:"production"}})},external:["js-slang*"],globalName:"module",platform:"browser",target:"es6",write:!1};async function _({path:e,text:t},r){let[n,i]=e.split(At.sep).slice(-3,-1),o=null;try{let s=Mt(t,{ecmaVersion:6}),a;s.body[0].type==="VariableDeclaration"?a=s.body[0]:a=s.body[1];let m={type:"ExportDefaultDeclaration",declaration:{...a.declarations[0].init.callee,params:[{type:"Identifier",name:"require"}]}};o=await Dt.open(`${r}/${n}/${i}.js`,"w");let x=o.createWriteStream();return Nt(m,{output:x}),{severity:"success",name:i}}catch(s){return{name:i,severity:"error",error:s}}finally{await o?.close()}}var Ht=["js-slang","js-slang/context","js-slang/dist/cse-machine/interpreter","js-slang/dist/stdlib","js-slang/dist/stdlib/list","js-slang/dist/stdlib/misc","js-slang/dist/stdlib/stream","js-slang/dist/types","js-slang/dist/utils/assert","js-slang/dist/utils/stringify","js-slang/dist/parser/parser"],F={name:"js-slang import checker",setup(e){e.onResolve({filter:/^js-slang/u},t=>{if(!Ht.includes(t.path))return{errors:[{text:"This export from js-slang is not supported!"}]}})}},ye={name:"Assert Polyfill",setup(e){e.onResolve({filter:/^assert/u},()=>({path:"assert",namespace:"bundleAssert"})),e.onLoad({filter:/^assert/u,namespace:"bundleAssert"},()=>({contents:` - export default function assert(condition, message) { - if (condition) return; - - if (typeof message === 'string' || message === undefined) { - throw new Error(message); - } - - throw message; - } - `}))}};var Pe=async({bundles:e},{srcDir:t,outDir:r})=>{let[{outputFiles:n}]=await p(Lt({...M,entryPoints:A(t,e),outbase:r,outdir:r,plugins:[ye,F],tsconfig:`${t}/tsconfig.json`}),It.mkdir(`${r}/bundles`,{recursive:!0}));return{bundles:await Promise.all(n.map(o=>_(o,r)))}},_t=v((...e)=>Pe(...e)),ke=()=>C("bundles","Build bundles").addOption(O).action(e=>_t({...e,tabs:[]}));import Ft from"fs/promises";import{build as Wt}from"esbuild";var je={name:"Tab Context",setup(e){e.onResolve({filter:/^js-slang\/context/u},()=>({errors:[{text:"If you see this message, it means that your tab code is importing js-slang/context directly or indirectly. Do not do this"}]}))}},Se=async({tabs:e},{srcDir:t,outDir:r})=>{let[{outputFiles:n}]=await p(Wt({...M,entryPoints:G(t,e),external:[...M.external,"react","react-ace","react-dom","react/jsx-runtime","@blueprintjs/*"],jsx:"automatic",outbase:r,outdir:r,tsconfig:`${t}/tsconfig.json`,plugins:[je,F]}),Ft.mkdir(`${r}/tabs`,{recursive:!0}));return{tabs:await Promise.all(n.map(o=>_(o,r)))}},Jt=v((...e)=>Se(...e),"bundles"),Ee=()=>C("tabs","Build tabs").addOption(L).action(e=>Jt({...e,bundles:[]}));var De=async(e,t)=>{let[r,n]=await Promise.all([Pe(e,t),Se(e,t)]);return{...r,...n}},In=v(De);import oe from"fs/promises";import{Command as Ut}from"@commander-js/extra-typings";import se from"chalk";import{context as et}from"esbuild";var zt=()=>new Promise((e,t)=>{process.stdin.setRawMode(!0),process.stdin.on("data",r=>{let n=[...r];n.length>0&&n[0]===3&&(console.log("^C"),process.stdin.setRawMode(!1),e())}),process.stdin.on("error",t)});function Ae(){return new Ut("watch").description("Watch the source directory and rebuild on changes").addOption(h).addOption(U).addOption(P).option("-v, --verbose").action(async e=>{let[t]=await p(k(e.manifest,null,null),oe.mkdir(e.outDir).then(()=>oe.copyFile(e.manifest,`${e.outDir}/modules.json`)));console.log(te(t,{}));let r=null,n=null;try{await p(oe.mkdir(`${e.outDir}/bundles`),oe.mkdir(`${e.outDir}/tabs`),oe.mkdir(`${e.outDir}/jsons`)),[r,n]=await p(et({...M,entryPoints:A(e.srcDir,t.bundles),outbase:e.outDir,outdir:e.outDir,plugins:[ye,F,{name:"Bundles output",setup(i){i.onStart(()=>{console.log(se.magentaBright("Beginning bundles build..."))}),i.onEnd(async({outputFiles:o})=>{let s=await Promise.all(o.map(a=>_(a,e.outDir)));re({bundles:s},e.verbose,!1)})}}],tsconfig:`${e.srcDir}/tsconfig.json`}),et({...M,entryPoints:G(e.srcDir,t.tabs),outbase:e.outDir,outdir:e.outDir,plugins:[je,F,{name:"Tabs output",setup(i){i.onStart(()=>{console.log(se.magentaBright("Beginning tabs build..."))}),i.onEnd(async({outputFiles:o})=>{let s=await Promise.all(o.map(a=>_(a,e.outDir)));re({tabs:s},e.verbose,!1)})}}],tsconfig:`${e.srcDir}/tsconfig.json`}),j(t.bundles,e.srcDir,e.verbose,!0).then(([,i])=>{i.convertAndWatch(async o=>{let[s,a]=await p(ie(t,e.outDir,o),ee(t,e.outDir,[o,i]));re({...s,html:a},e.verbose,!1)})})),console.log(se.yellowBright(`Watching ${se.cyanBright(`./${e.srcDir}`)} for changes -Press CTRL + C to stop`)),await zt(),console.log(se.yellowBright("Quitting!")),await p(r.cancel(),n.cancel())}finally{await p(r?.dispose(),n?.dispose())}process.exit()})}var Kt=async(e,t)=>{let r=await j(e.bundles,t.srcDir,t.verbose,!1),[n,i]=await Promise.all([De(e,t),$e(e,t.outDir,r)]);return{...n,...i}},qt=v(Kt),Qt=()=>C("all","Build bundles and tabs and documentation").addOption(O).addOption(L).action(qt),Zt=()=>new Vt("build").addCommand(Qt(),{isDefault:!0}).addCommand(ke()).addCommand(Ye()).addCommand(he()).addCommand(Ce()).addCommand(Ee()).addCommand(Ae()),tt=Zt;import{Command as ir}from"@commander-js/extra-typings";import Me from"fs/promises";import{createInterface as Gt}from"readline/promises";import ae from"chalk";var rt=()=>Gt({input:process.stdin,output:process.stdout});function nt(...e){return console.log(...e.map(t=>ae.grey(t)))}function it(...e){return console.log(...e.map(t=>ae.red(t)))}function E(...e){return console.log(...e.map(t=>ae.yellow(t)))}function be(...e){return console.log(...e.map(t=>ae.green(t)))}function W(e,t){return t.question(ae.blueBright(`${e} -`))}var Xt=/\b[a-z0-9]+(?:_[a-z0-9]+)*\b/u,Yt=/^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u;function ot(e){return Xt.test(e)}function st(e){return Yt.test(e)}var Ne=(e,t)=>Object.keys(e).includes(t);async function er(e,t){for(;;){let r=await W("What is the name of your new module? (eg. binary_tree)",t);if(ot(r)===!1)E("Module names must be in snake case. (eg. binary_tree)");else if(Ne(e,r))E("A module with the same name already exists.");else return r}}async function at({srcDir:e,manifest:t},r){let n=await D(t),i=await er(n,r),o=`${e}/bundles/${i}`;await Me.mkdir(o,{recursive:!0}),await p(Me.copyFile("./scripts/src/templates/templates/__bundle__.ts",`${o}/index.ts`),Me.writeFile(t,JSON.stringify({...n,[i]:{tabs:[]}},null,2))),be(`Bundle for module ${i} created at ${o}.`)}import He from"fs/promises";function tr(e,t){return Object.values(e).flatMap(r=>r.tabs).includes(t)}async function rr(e,t){for(;;){let r=await W("Add a new tab to which module?",t);if(!Ne(e,r))E(`Module ${r} does not exist.`);else return r}}async function nr(e,t){for(;;){let r=await W("What is the name of your new tab? (eg. BinaryTree)",t);if(tr(e,r))E("A tab with the same name already exists.");else if(!st(r))E("Tab names must be in pascal case. (eg. BinaryTree)");else return r}}async function lt({manifest:e,srcDir:t},r){let n=await D(e),i=await rr(n,r),o=await nr(n,r),s=`${t}/tabs/${o}`;await He.mkdir(s,{recursive:!0}),await p(He.copyFile("./scripts/src/templates/templates/__tab__.tsx",`${s}/index.tsx`),He.writeFile(e,JSON.stringify({...n,[i]:{tabs:[...n[i].tabs,o]}},null,2))),be(`Tab ${o} for module ${i} created at ${s}.`)}async function or(e){for(;;){let t=await W("What would you like to create? (module/tab)",e);if(t!=="module"&&t!=="tab")E("Please answer with only 'module' or 'tab'.");else return t}}function Ie(){return new ir("template").addOption(h).addOption(P).description("Interactively create a new module or tab").action(async e=>{let t=rt();try{let r=await or(t);r==="module"?await at(e,t):r==="tab"&&await lt(e,t)}catch(r){it(`ERROR: ${r.message}`),nt("Terminating module app...")}finally{t.close()}})}import ct from"path";import{Command as lr}from"@commander-js/extra-typings";import ur from"lodash";import sr from"path";import ar from"jest";function ut(e,t){return ar.run(e,sr.join(t,"jest.config.js"))}var cr=()=>new lr("test").description("Run jest").addOption(h).allowUnknownOption().action(({srcDir:e},t)=>{let[r,n]=ur.partition(t.args,s=>s.startsWith("-")),i=r.findIndex(s=>s.startsWith("--srcDir"));i!==-1&&r.splice(i,1);let o=r.concat(n.map(s=>s.split(ct.win32.sep).join(ct.posix.sep)));return ut(o,e)}),dt=cr;await new dr("scripts").addCommand(tt()).addCommand(Ve()).addCommand(dt()).addCommand(qe()).addCommand(Ie()).parseAsync(); From bc0c7466e38db8bb1f57605c99a563fcddd21d18 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 2 Apr 2025 16:01:26 +0800 Subject: [PATCH 35/46] Conform to #121 yarn package manager version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5e6ef2f26..56e43ddfdd 100644 --- a/package.json +++ b/package.json @@ -136,5 +136,5 @@ "resolutions": { "**/gl": "^8.0.2" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } From 9d55b9b0e48ddbe1cfa43426f0807599a14abe7a Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 3 Apr 2025 18:02:03 +0800 Subject: [PATCH 36/46] Write begin as an explicit case for action animation switch --- src/tabs/RobotMaze/components/RobotSimulation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index f4953d344e..4c0a0d6c59 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -252,7 +252,7 @@ const RobotSimulation : React.FC = ({ case 'sensor': animationPauseUntil.current = Date.now() + 500; break; - default: + case 'begin': robot.current = Object.assign({}, {radius: robot.current.radius}, target); } From 827df684a012c1da54858f8eb95d81e424eef0a9 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 3 Apr 2025 18:07:19 +0800 Subject: [PATCH 37/46] Remove unused raycast_multi() --- src/bundles/robot_minigame/functions.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 9fe097edb6..9e4234cc7e 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -488,23 +488,6 @@ function robot_raycast_area( : null; } -/** - * Check which areas fall along a ray - * - * @param ray to cast - * @param areas to check - * @returns collisions between the ray and areas - */ -function raycast_multi( - ray: Ray, - areas: Area[] -) : Collision[] { - return areas - .map(area => raycast(ray, area)) // Raycast each area - .filter(col => col !== null) // Remove null collisions - .sort((a, b) => a.distance - b.distance); // Sort by distance -} - /** * Get the shortest distance between a ray and an area * From a5acb35c83a7ab4e93b7888770af834c2db8adcc Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 3 Apr 2025 18:56:34 +0800 Subject: [PATCH 38/46] No longer throw error on collision with obstacle --- src/bundles/robot_minigame/functions.ts | 22 ++++++++++++++-------- src/bundles/robot_minigame/types.ts | 1 + 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 9e4234cc7e..a2a3cb4b97 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -29,6 +29,7 @@ interface Ray { // Default state before initialisation const state: RobotMinigame = { isInit: false, + hasCollided: false, width: 500, height: 500, robot: {x: 250, y: 250, rotation: 0, radius: 15}, @@ -281,6 +282,9 @@ export function get_color() : string { export function move_forward( distance: number ) { + // Ignore if robot has collided with an obstacle + if (state.hasCollided) return; + // Get the robot const robot = getRobot(); @@ -295,17 +299,13 @@ export function move_forward( // Handle a collision with an obstacle if (col.area.isObstacle) { // Calculate find distance - const finalDistance = (col.distance - robot.radius + 1); - - // Move the robot to its final position - robot.x = robot.x + finalDistance * Math.cos(robot.rotation); - robot.y = robot.y + finalDistance * Math.sin(robot.rotation); + distance = col.distance - robot.radius + 1; // Update the final message - state.message = `Collided with wall at (${robot.x},${robot.y})`; + state.message = `Collided with wall at (${robot.x + distance * Math.cos(robot.rotation)},${robot.y + distance * Math.sin(robot.rotation)})`; - // Throw an error to interrupt the simulation - throw new Error('Collided with wall'); + // Update state to reflect that the robot has collided with an obstacle + state.hasCollided = true; } } @@ -324,6 +324,9 @@ const SAFE_DISTANCE_FROM_WALL : number = 10; * Move the robot forward to within a predefined distance of the wall */ export function move_forward_to_wall() { + // Ignore if robot has collided with an obstacle + if (state.hasCollided) return; + // Move forward the furthest possible safe distance + a lil extra offset move_forward(Math.max(get_distance() - SAFE_DISTANCE_FROM_WALL, 0)); } @@ -336,6 +339,9 @@ export function move_forward_to_wall() { export function rotate( angle: number ) { + // Ignore if robot has collided with an obstacle + if (state.hasCollided) return; + // Get the robot const robot = getRobot(); diff --git a/src/bundles/robot_minigame/types.ts b/src/bundles/robot_minigame/types.ts index ea0820fcc2..4c52e27c0f 100644 --- a/src/bundles/robot_minigame/types.ts +++ b/src/bundles/robot_minigame/types.ts @@ -42,6 +42,7 @@ export interface AreaTest extends Test { export interface RobotMinigame { isInit: boolean + hasCollided: boolean width: number height: number robot: Robot From 424f45692f203560dbc0b0a162f7758f3a809d6c Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 3 Apr 2025 19:18:56 +0800 Subject: [PATCH 39/46] Move stateless raycast and area helpers into separate file --- src/bundles/robot_minigame/functions.ts | 142 +----------------- src/bundles/robot_minigame/helpers/areas.ts | 140 +++++++++++++++++ .../robot_minigame/{ => helpers}/tests.ts | 2 +- .../RobotMaze/components/RobotSimulation.tsx | 2 +- 4 files changed, 144 insertions(+), 142 deletions(-) create mode 100644 src/bundles/robot_minigame/helpers/areas.ts rename src/bundles/robot_minigame/{ => helpers}/tests.ts (92%) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index a2a3cb4b97..cbe1d05544 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -5,7 +5,8 @@ import context from 'js-slang/context'; // type List // } from 'js-slang/dist/stdlib/list'; -import { run_tests } from './tests'; +import { areaEquals, is_within_area, raycast, type Collision } from './helpers/areas'; +import { run_tests } from './helpers/tests'; import type { Point, PointWithRotation, Robot, Action, @@ -14,18 +15,6 @@ import type { RobotMinigame } from './types'; -// A line segment between p1 and p2 -interface LineSegment { - p1: Point - p2: Point -} - -// A ray from origin towards target -interface Ray { - origin: Point - target: Point -} - // Default state before initialisation const state: RobotMinigame = { isInit: false, @@ -434,12 +423,6 @@ function getPositionWithRotation(): PointWithRotation { // RAYCAST AND AREA HELPERS // // ======================== // -// A collision between a ray and an area -interface Collision { - distance: number - area: Area -} - /** * Get the distance between the robot and area, if the robot is facing the area * Casts 3 rays from the robot's left, middle and right @@ -494,42 +477,6 @@ function robot_raycast_area( : null; } -/** - * Get the shortest distance between a ray and an area - * - * @param ray being cast - * @param area to check - * @returns the collision with the minimum distance, or null (if no collision) - */ -function raycast( - ray: Ray, - area: Area -) : Collision | null { - const { vertices } = area; - - // Store the minimum distance - let distance = Infinity; - - for (let i = 0; i < vertices.length; i++) { - // Border line segment - const border: LineSegment = { - p1: {x: vertices[i].x, y: vertices[i].y}, - p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} - }; - - // Compute the minimum distance - const distanceToIntersection: number = getIntersection(ray, border); - - // Save the new minimum, if necessary - if (distanceToIntersection < distance) distance = distanceToIntersection; - } - - // Return null if no collision - return distance < Infinity - ? {distance, area} - : null; -} - /** * Find the area the robot is in * @@ -547,72 +494,6 @@ function area_of_point( return null; } -/** - * Check if the point is within the area - * - * @param point potentially within the area - * @param area to check - * @returns if the point is within the area - */ -function is_within_area( - point: Point, - area: Area -) : boolean { - const { vertices } = area; - - // Cast a ray to the right of the point - const ray = { - origin: point, - target: {x: point.x + 1, y: point.y + 0} - }; - - // Count the intersections - let intersections = 0; - - for (let i = 0; i < vertices.length; i++) { - // Border line segment - const border: LineSegment = { - p1: {x: vertices[i].x, y: vertices[i].y}, - p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} - }; - - // Increment intersections if the ray intersects the border - if (getIntersection(ray, border) < Infinity) intersections++; - } - - // Even => Outside; Odd => Inside - return intersections % 2 === 1; -} - -/** - * Determine if a ray and a line segment intersect - * If they intersect, determine the distance from the ray's origin to the collision point - * - * @param ray being checked - * @param line to check intersection - * @returns the distance to the line segment, or infinity (if no collision) - */ -function getIntersection( - { origin, target }: Ray, - { p1, p2 }: LineSegment -) : number { - const denom: number = ((target.x - origin.x)*(p2.y - p1.y)-(target.y - origin.y)*(p2.x - p1.x)); - - // If lines are collinear or parallel - if (denom === 0) return Infinity; - - // Intersection in ray "local" coordinates - const r: number = (((origin.y - p1.y) * (p2.x - p1.x)) - (origin.x - p1.x) * (p2.y - p1.y)) / denom; - - // Intersection in segment "local" coordinates - const s: number = (((origin.y - p1.y) * (target.x - origin.x)) - (origin.x - p1.x) * (target.y - origin.y)) / denom; - - // Check if line segment is behind ray, or not on the line segment - if (r < 0 || s < 0 || s > 1) return Infinity; - - return r; -} - // =============== // // LOGGING HELPERS // // =============== // @@ -653,25 +534,6 @@ function logArea( // AREA HELPERS // // ============ // -/** - * Compare two areas for equality - * - * @param a the first area to compare - * @param b the second area to compare - * @returns if a == b - */ -function areaEquals(a: Area, b: Area) { - if ( - a.vertices.length !== b.vertices.length // a and b must have an equal number of vertices - || a.vertices.some((v, i) => v.x !== b.vertices[i].x || v.y !== b.vertices[i].y) // a and b's vertices must be the same - || a.isObstacle !== b.isObstacle // Either both a and b or neither a nor b are obstacles - || Object.keys(a.flags).length !== Object.keys(b.flags).length // Check flags length equality - || Object.keys(a.flags).some(key => a.flags[key] !== b.flags[key]) // Check flag value equality - ) return false; - - return true; -} - /** * Filter callback to remove adjacent duplicate areas * diff --git a/src/bundles/robot_minigame/helpers/areas.ts b/src/bundles/robot_minigame/helpers/areas.ts new file mode 100644 index 0000000000..389fb2f661 --- /dev/null +++ b/src/bundles/robot_minigame/helpers/areas.ts @@ -0,0 +1,140 @@ +import type { Area, Point } from '../types'; + +// A line segment between p1 and p2 +interface LineSegment { + p1: Point + p2: Point +} + +// A ray from origin towards target +interface Ray { + origin: Point + target: Point +} + +// A collision between a ray and an area +export interface Collision { + distance: number + area: Area +} + +/** + * Determine if a ray and a line segment intersect + * If they intersect, determine the distance from the ray's origin to the collision point + * + * @param ray being checked + * @param line to check intersection + * @returns the distance to the line segment, or infinity (if no collision) + */ +function getIntersection( + { origin, target }: Ray, + { p1, p2 }: LineSegment +) : number { + const denom: number = ((target.x - origin.x)*(p2.y - p1.y)-(target.y - origin.y)*(p2.x - p1.x)); + + // If lines are collinear or parallel + if (denom === 0) return Infinity; + + // Intersection in ray "local" coordinates + const r: number = (((origin.y - p1.y) * (p2.x - p1.x)) - (origin.x - p1.x) * (p2.y - p1.y)) / denom; + + // Intersection in segment "local" coordinates + const s: number = (((origin.y - p1.y) * (target.x - origin.x)) - (origin.x - p1.x) * (target.y - origin.y)) / denom; + + // Check if line segment is behind ray, or not on the line segment + if (r < 0 || s < 0 || s > 1) return Infinity; + + return r; +} + +/** + * Get the shortest distance between a ray and an area + * + * @param ray being cast + * @param area to check + * @returns the collision with the minimum distance, or null (if no collision) + */ +export function raycast( + ray: Ray, + area: Area +) : Collision | null { + const { vertices } = area; + + // Store the minimum distance + let distance = Infinity; + + for (let i = 0; i < vertices.length; i++) { + // Border line segment + const border: LineSegment = { + p1: {x: vertices[i].x, y: vertices[i].y}, + p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} + }; + + // Compute the minimum distance + const distanceToIntersection: number = getIntersection(ray, border); + + // Save the new minimum, if necessary + if (distanceToIntersection < distance) distance = distanceToIntersection; + } + + // Return null if no collision + return distance < Infinity + ? {distance, area} + : null; +} + +/** + * Check if the point is within the area + * + * @param point potentially within the area + * @param area to check + * @returns if the point is within the area + */ +export function is_within_area( + point: Point, + area: Area +) : boolean { + const { vertices } = area; + + // Cast a ray to the right of the point + const ray = { + origin: point, + target: {x: point.x + 1, y: point.y + 0} + }; + + // Count the intersections + let intersections = 0; + + for (let i = 0; i < vertices.length; i++) { + // Border line segment + const border: LineSegment = { + p1: {x: vertices[i].x, y: vertices[i].y}, + p2: {x: vertices[(i + 1) % vertices.length].x, y: vertices[(i + 1) % vertices.length].y} + }; + + // Increment intersections if the ray intersects the border + if (getIntersection(ray, border) < Infinity) intersections++; + } + + // Even => Outside; Odd => Inside + return intersections % 2 === 1; +} + +/** + * Compare two areas for equality + * + * @param a the first area to compare + * @param b the second area to compare + * @returns if a == b + */ +export function areaEquals(a: Area, b: Area) { + if ( + a.vertices.length !== b.vertices.length // a and b must have an equal number of vertices + || a.vertices.some((v, i) => v.x !== b.vertices[i].x || v.y !== b.vertices[i].y) // a and b's vertices must be the same + || a.isObstacle !== b.isObstacle // Either both a and b or neither a nor b are obstacles + || Object.keys(a.flags).length !== Object.keys(b.flags).length // Check flags length equality + || Object.keys(a.flags).some(key => a.flags[key] !== b.flags[key]) // Check flag value equality + ) return false; + + return true; +} diff --git a/src/bundles/robot_minigame/tests.ts b/src/bundles/robot_minigame/helpers/tests.ts similarity index 92% rename from src/bundles/robot_minigame/tests.ts rename to src/bundles/robot_minigame/helpers/tests.ts index afccca29b7..4e1d7c0003 100644 --- a/src/bundles/robot_minigame/tests.ts +++ b/src/bundles/robot_minigame/helpers/tests.ts @@ -1,4 +1,4 @@ -import type { Area, Test } from './types'; +import type { Area, Test } from '../types'; /** * Run the stored tests in state diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/components/RobotSimulation.tsx index 4c0a0d6c59..ec8af07ea5 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/components/RobotSimulation.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { run_tests } from '../../../bundles/robot_minigame/tests'; +import { run_tests } from '../../../bundles/robot_minigame/helpers/tests'; import type { Area, Action, Robot, RobotMinigame } from '../../../bundles/robot_minigame/types'; /** From 202181b25e8649339a4ec1614a0c53a3251635de Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Thu, 3 Apr 2025 19:31:49 +0800 Subject: [PATCH 40/46] Tests should fail if the robot has collided with an obstacle --- src/bundles/robot_minigame/functions.ts | 2 +- src/tabs/RobotMaze/{components => }/RobotSimulation.tsx | 7 ++++--- src/tabs/RobotMaze/index.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename src/tabs/RobotMaze/{components => }/RobotSimulation.tsx (96%) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index cbe1d05544..dc56e3028d 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -367,7 +367,7 @@ export function turn_right() { * @returns if all tests pass */ export function run_all_tests() : boolean { - return run_tests(state); + return !state.hasCollided && run_tests(state); } // ================== diff --git a/src/tabs/RobotMaze/components/RobotSimulation.tsx b/src/tabs/RobotMaze/RobotSimulation.tsx similarity index 96% rename from src/tabs/RobotMaze/components/RobotSimulation.tsx rename to src/tabs/RobotMaze/RobotSimulation.tsx index ec8af07ea5..8990a47c5d 100644 --- a/src/tabs/RobotMaze/components/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/RobotSimulation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { run_tests } from '../../../bundles/robot_minigame/helpers/tests'; -import type { Area, Action, Robot, RobotMinigame } from '../../../bundles/robot_minigame/types'; +import { run_tests } from '../../bundles/robot_minigame/helpers/tests'; +import type { Area, Action, Robot, RobotMinigame } from '../../bundles/robot_minigame/types'; /** * Calculate the acute angle between 2 angles @@ -130,6 +130,7 @@ interface MapProps { const RobotSimulation : React.FC = ({ state: { + hasCollided, width, height, robot: {radius: robotSize}, @@ -276,7 +277,7 @@ const RobotSimulation : React.FC = ({ : animationStatus === 2 ? : } - {animationStatus === 3 && {run_tests({tests, areaLog}) ? 'Success! 🎉' : message}} + {animationStatus === 3 && {!hasCollided && run_tests({tests, areaLog}) ? 'Success! 🎉' : message}}
  • diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index 1b1a65adde..e8cd7884b0 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { DebuggerContext } from '../../typings/type_helpers'; -import RobotSimulation from './components/RobotSimulation'; +import RobotSimulation from './RobotSimulation'; /** * Renders the robot minigame in the assessment workspace From 880ebdfc041f22d32e7f3cc0fe5f6507f65a7fac Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Wed, 16 Apr 2025 10:27:59 +0800 Subject: [PATCH 41/46] Fix serve http-server cors; Flip sense_obstacle() --- package.json | 2 +- src/bundles/robot_minigame/functions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7a7371a4a5..d79de36047 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "prepare": "husky", "postinstall": "patch-package && yarn scripts:build", "scripts": "node --max-old-space-size=4096 scripts/dist/bin.js", - "serve": "http-server --cors=* -c-1 -p 8022 ./build", + "serve": "http-server --cors -c-1 -p 8022 ./build", "template": "yarn scripts template", "test": "yarn scripts test", "test:all": "yarn test && yarn scripts:test", diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index dc56e3028d..977fe6ab82 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -247,7 +247,7 @@ const SENSOR_RANGE: number = 15; * Check if there is an obstacle within a predefined distance from the robot */ export function sense_obstacle() : boolean { - return get_distance() > SENSOR_RANGE; + return get_distance() < SENSOR_RANGE; } /** From b7128daa31206d2d9138bb0172eb8150d30bf25a Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Sat, 14 Jun 2025 18:18:10 +0800 Subject: [PATCH 42/46] Remove temporary debug log --- src/tabs/RobotMaze/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index e8cd7884b0..e4bd86e4b5 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -34,9 +34,6 @@ export default { * @returns {boolean} */ toSpawn(context: DebuggerContext) { - // !!! TEMPORARY DEBUGGING FUNCTION, REMOVE ONCE MODULE IS COMPLETE !!! - console.log(context.context?.moduleContexts?.robot_minigame.state); - return context.context?.moduleContexts?.robot_minigame.state.isInit; }, From df66413809e75a8d4a6f0578f52fa1592ed923db Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Sun, 15 Jun 2025 16:13:39 +0800 Subject: [PATCH 43/46] Touchups as per RichDom's review --- src/bundles/robot_minigame/helpers/tests.ts | 15 ++------------- src/tabs/RobotMaze/RobotSimulation.tsx | 2 -- src/tabs/RobotMaze/index.tsx | 8 ++------ 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/bundles/robot_minigame/helpers/tests.ts b/src/bundles/robot_minigame/helpers/tests.ts index 4e1d7c0003..4c1ee145ff 100644 --- a/src/bundles/robot_minigame/helpers/tests.ts +++ b/src/bundles/robot_minigame/helpers/tests.ts @@ -14,19 +14,8 @@ export function run_tests({ }) : boolean { // Run each test in order for (const test of tests) { - // Store status in a variable - let success: boolean; - - switch(test.type) { - case 'area': - success = test.test(areaLog); - break; - default: - success = true; - } - - // If the test fails, return false - if (!success) return false; + // Can replace with a switch statement when more success conditions appear + if (test.type === 'area' && !test.test(areaLog)) return false; } // If all tests pass, return true diff --git a/src/tabs/RobotMaze/RobotSimulation.tsx b/src/tabs/RobotMaze/RobotSimulation.tsx index 8990a47c5d..01c0d38bfb 100644 --- a/src/tabs/RobotMaze/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/RobotSimulation.tsx @@ -123,8 +123,6 @@ const ANIMATION_SPEED : number = 2; * React Component props for the Tab. */ interface MapProps { - children?: never - className?: never state: RobotMinigame, } diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/RobotMaze/index.tsx index e4bd86e4b5..c479770010 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/RobotMaze/index.tsx @@ -12,8 +12,6 @@ import RobotSimulation from './RobotSimulation'; * React Component props for the Tab. */ interface MainProps { - children?: never - className?: never context?: DebuggerContext } @@ -33,16 +31,14 @@ export default { * @param {DebuggerContext} context * @returns {boolean} */ - toSpawn(context: DebuggerContext) { - return context.context?.moduleContexts?.robot_minigame.state.isInit; - }, + toSpawn: (context: DebuggerContext) => context.context?.moduleContexts?.robot_minigame.state.isInit, /** * This function will be called to render the module tab in the side contents * on Source Academy frontend. * @param {DebuggerContext} context */ - body: (context: any) => , + body: (context: DebuggerContext) => , /** * The Tab's icon tooltip in the side contents on Source Academy frontend. From 910934fbb6748e792bf75ba6829211d6fd2a044e Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Fri, 4 Jul 2025 13:22:40 +0800 Subject: [PATCH 44/46] Allow developers to change border color with set_border_color --- src/bundles/csg/jscad/renderer.ts | 16 ++++++++-------- src/bundles/robot_minigame/functions.ts | 17 ++++++++++++++++- src/bundles/robot_minigame/index.ts | 3 ++- src/bundles/robot_minigame/types.ts | 6 ++++++ src/tabs/RobotMaze/RobotSimulation.tsx | 24 ++++++++++++++---------- 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/bundles/csg/jscad/renderer.ts b/src/bundles/csg/jscad/renderer.ts index d82a6505f8..22507ba32f 100644 --- a/src/bundles/csg/jscad/renderer.ts +++ b/src/bundles/csg/jscad/renderer.ts @@ -66,12 +66,12 @@ class MultiGridEntity implements MultiGridEntityType { color?: AlphaColor; subColor?: AlphaColor; } = { - drawCmd: 'drawGrid', - show: true, + drawCmd: 'drawGrid', + show: true, - color: hexToAlphaColor(BP_TEXT_COLOR), - subColor: hexToAlphaColor(ACE_GUTTER_TEXT_COLOR) - }; + color: hexToAlphaColor(BP_TEXT_COLOR), + subColor: hexToAlphaColor(ACE_GUTTER_TEXT_COLOR) + }; ticks: [number, number] = [MAIN_TICKS, SUB_TICKS]; @@ -87,9 +87,9 @@ class AxisEntity implements AxisEntityType { drawCmd: 'drawAxis'; show: boolean; } = { - drawCmd: 'drawAxis', - show: true - }; + drawCmd: 'drawAxis', + show: true + }; alwaysVisible: boolean = false; diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 977fe6ab82..89e330175e 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -21,7 +21,8 @@ const state: RobotMinigame = { hasCollided: false, width: 500, height: 500, - robot: {x: 250, y: 250, rotation: 0, radius: 15}, + border: {}, + robot: { x: 250, y: 250, rotation: 0, radius: 15 }, areas: [], areaLog: [], actionLog: [], @@ -77,6 +78,20 @@ export function init( state.message = 'Please run this in the assessments tab!'; } +/** + * Set the color of the map border + * + * @param {string} color of the border (in any CSS-accepted format) + */ +export function set_border_color( + color: string +) { + // Init functions should not run after initialization + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); + + state.border.color = color; +} + /** * Create a new area with the given vertices and flags * diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index ed5107f336..8561fbac5f 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -7,7 +7,8 @@ */ export { - init, create_area, create_rect_area, create_obstacle, create_rect_obstacle, complete_init, + init, complete_init, set_border_color, + create_area, create_rect_area, create_obstacle, create_rect_obstacle, get_distance, sense_obstacle, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, should_enter_colors, run_all_tests diff --git a/src/bundles/robot_minigame/types.ts b/src/bundles/robot_minigame/types.ts index 4c52e27c0f..345e870388 100644 --- a/src/bundles/robot_minigame/types.ts +++ b/src/bundles/robot_minigame/types.ts @@ -1,3 +1,8 @@ +// A config storing border data +export interface BorderConfig { + color?: string +} + // A point (x, y) export interface Point { x: number @@ -45,6 +50,7 @@ export interface RobotMinigame { hasCollided: boolean width: number height: number + border: BorderConfig robot: Robot areas: Area[] areaLog: Area[] diff --git a/src/tabs/RobotMaze/RobotSimulation.tsx b/src/tabs/RobotMaze/RobotSimulation.tsx index 01c0d38bfb..d2049538f6 100644 --- a/src/tabs/RobotMaze/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/RobotSimulation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { run_tests } from '../../bundles/robot_minigame/helpers/tests'; -import type { Area, Action, Robot, RobotMinigame } from '../../bundles/robot_minigame/types'; +import type { Area, Action, BorderConfig, Robot, RobotMinigame } from '../../bundles/robot_minigame/types'; /** * Calculate the acute angle between 2 angles @@ -31,7 +31,8 @@ const smallestAngle = ( const drawBorders = ( ctx: CanvasRenderingContext2D, width: number, - height: number + height: number, + border: BorderConfig ) => { ctx.beginPath(); ctx.moveTo(0, 0); @@ -40,7 +41,7 @@ const drawBorders = ( ctx.lineTo(width, 0); ctx.closePath(); - ctx.strokeStyle = 'gray'; + ctx.strokeStyle = border.color || 'gray'; ctx.lineWidth = 3; ctx.stroke(); }; @@ -104,15 +105,16 @@ const drawRobot = ( * Render the current game state */ const drawAll = ( - ctx : CanvasRenderingContext2D, - width : number, - height : number, + ctx: CanvasRenderingContext2D, + width: number, + height: number, + border: BorderConfig, areas: Area[], - robot : Robot + robot: Robot ) => { ctx.reset(); - drawBorders(ctx, width, height); drawAreas(ctx, areas); + drawBorders(ctx, width, height, border); drawRobot(ctx, robot); }; @@ -131,6 +133,7 @@ const RobotSimulation : React.FC = ({ hasCollided, width, height, + border, robot: {radius: robotSize}, areas, actionLog, @@ -175,7 +178,7 @@ const RobotSimulation : React.FC = ({ canvas.width = width; canvas.height = height; - drawAll(ctx, width, height, areas, robot.current); + drawAll(ctx, width, height, border, areas, robot.current); }, [animationStatus]); // Handle animation @@ -183,6 +186,7 @@ const RobotSimulation : React.FC = ({ if (animationStatus !== 1) return; const interval = setInterval(() => { + // Retrieve canvas 2d context const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); @@ -255,7 +259,7 @@ const RobotSimulation : React.FC = ({ robot.current = Object.assign({}, {radius: robot.current.radius}, target); } - drawAll(ctx, width, height, areas, robot.current); + drawAll(ctx, width, height, border, areas, robot.current); }, 10); return () => clearInterval(interval); From 839d114716415da21c319936a712362d0657c001 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Fri, 4 Jul 2025 16:58:00 +0800 Subject: [PATCH 45/46] Added set_border_width; Changed area input formatting --- src/bundles/robot_minigame/functions.ts | 64 +++++++++++-------------- src/bundles/robot_minigame/index.ts | 2 +- src/bundles/robot_minigame/types.ts | 1 + src/tabs/RobotMaze/RobotSimulation.tsx | 2 +- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/robot_minigame/functions.ts index 89e330175e..72cd1e44a1 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/robot_minigame/functions.ts @@ -92,50 +92,40 @@ export function set_border_color( state.border.color = color; } +/** + * Set the width of the map border + * + * @param {number} width of the border + */ +export function set_border_width( + width: number +) { + // Init functions should not run after initialization + if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); + + state.border.width = width; +} + /** * Create a new area with the given vertices and flags * - * @param vertices of the area in alternating x-y pairs + * @param vertices of the area (in x-y pairs) * @param isObstacle a boolean indicating if the area is an obstacle or not - * @param flags any additional flags the area may have + * @param flags any additional flags the area may have (in key-value pairs) */ export function create_area( - vertices: number[], + vertices: [x: number, y: number][], isObstacle: boolean, - flags: any[] + flags: [key: string, value: any][] ) { // Init functions should not run after initialization if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); - if (vertices.length % 2 !== 0) throw new Error('Odd number of vertice x-y coordinates given (expected even)'); - - if (flags.length % 2 !== 0) throw new Error('Odd number of flag arguments given (expected even)'); - // Store vertices as Point array - const parsedVertices: Point[] = []; - - // Parse x-y pairs into Points - for (let i = 0; i < vertices.length / 2; i++) { - parsedVertices[i] = { - x: vertices[i * 2], - y: vertices[i * 2 + 1] - }; - } + const parsedVertices: Point[] = vertices.map(v => ({ x: v[0], y: v[1] })); // Store flags as an object - const parsedFlags = {}; - - // Parse flag-value pairs into flags - for (let i = 0; i < flags.length / 2; i++) { - // Retrieve flag - const flag = flags[i * 2]; - - // Check flag is string - if (typeof flag !== 'string') throw new Error(`Flag arguments must be strings (${flag} is a ${typeof flag})`); - - // Add flag to object - parsedFlags[flag] = flags[i * 2 + 1]; - } + const parsedFlags = flags.reduce((acc, f) => ({ ...acc, [f[0]]: f[1] }), {}); // Store the new area state.areas.push({ @@ -153,7 +143,7 @@ export function create_area( * @param width of the rectangle * @param height of the rectangle * @param isObstacle a boolean indicating if the area is an obstacle or not - * @param flags any additional flags the area may have + * @param flags any additional flags the area may have (in key-value pairs) */ export function create_rect_area( x: number, @@ -161,16 +151,16 @@ export function create_rect_area( width: number, height: number, isObstacle: boolean, - flags: any[] + flags: [key: string, value: any][] ) { // Init functions should not run after initialization if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); create_area([ - x, y, - x + width, y, - x + width, y + height, - x, y + height + [x, y], + [x + width, y], + [x + width, y + height], + [x, y + height] ], isObstacle, flags); } @@ -180,7 +170,7 @@ export function create_rect_area( * @param vertices of the obstacle */ export function create_obstacle( - vertices: number[] + vertices: [x: number, y: number][] ) { // Init functions should not run after initialization if (state.isInit) throw new Error('May not use initialization functions after initialization is complete!'); diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/robot_minigame/index.ts index 8561fbac5f..edfb14d3da 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/robot_minigame/index.ts @@ -7,7 +7,7 @@ */ export { - init, complete_init, set_border_color, + init, complete_init, set_border_color, set_border_width, create_area, create_rect_area, create_obstacle, create_rect_obstacle, get_distance, sense_obstacle, get_color, move_forward, move_forward_to_wall, rotate, turn_left, turn_right, diff --git a/src/bundles/robot_minigame/types.ts b/src/bundles/robot_minigame/types.ts index 345e870388..902768078c 100644 --- a/src/bundles/robot_minigame/types.ts +++ b/src/bundles/robot_minigame/types.ts @@ -1,6 +1,7 @@ // A config storing border data export interface BorderConfig { color?: string + width?: number } // A point (x, y) diff --git a/src/tabs/RobotMaze/RobotSimulation.tsx b/src/tabs/RobotMaze/RobotSimulation.tsx index d2049538f6..b1ba463584 100644 --- a/src/tabs/RobotMaze/RobotSimulation.tsx +++ b/src/tabs/RobotMaze/RobotSimulation.tsx @@ -42,7 +42,7 @@ const drawBorders = ( ctx.closePath(); ctx.strokeStyle = border.color || 'gray'; - ctx.lineWidth = 3; + ctx.lineWidth = border.width || 3; ctx.stroke(); }; From 8824bfb59059eb3c481456707acf8a9e27b9edc6 Mon Sep 17 00:00:00 2001 From: JustATin555 Date: Fri, 4 Jul 2025 17:32:24 +0800 Subject: [PATCH 46/46] Rename robot_simulation to maze --- modules.json | 4 ++-- .../{robot_minigame => maze}/functions.ts | 6 +++--- .../{robot_minigame => maze}/helpers/areas.ts | 0 .../{robot_minigame => maze}/helpers/tests.ts | 0 src/bundles/{robot_minigame => maze}/index.ts | 4 ++-- src/bundles/{robot_minigame => maze}/types.ts | 7 ++++++- .../MazeSimulation.tsx} | 10 +++++----- src/tabs/{RobotMaze => Maze}/index.tsx | 16 ++++++++-------- 8 files changed, 26 insertions(+), 21 deletions(-) rename src/bundles/{robot_minigame => maze}/functions.ts (99%) rename src/bundles/{robot_minigame => maze}/helpers/areas.ts (100%) rename src/bundles/{robot_minigame => maze}/helpers/tests.ts (100%) rename src/bundles/{robot_minigame => maze}/index.ts (77%) rename src/bundles/{robot_minigame => maze}/types.ts (84%) rename src/tabs/{RobotMaze/RobotSimulation.tsx => Maze/MazeSimulation.tsx} (96%) rename src/tabs/{RobotMaze => Maze}/index.tsx (71%) diff --git a/modules.json b/modules.json index c3b68d0430..f4841bd161 100644 --- a/modules.json +++ b/modules.json @@ -117,9 +117,9 @@ "Nbody" ] }, - "robot_minigame": { + "maze": { "tabs": [ - "RobotMaze" + "Maze" ] }, "unittest": { diff --git a/src/bundles/robot_minigame/functions.ts b/src/bundles/maze/functions.ts similarity index 99% rename from src/bundles/robot_minigame/functions.ts rename to src/bundles/maze/functions.ts index 72cd1e44a1..285bf36258 100644 --- a/src/bundles/robot_minigame/functions.ts +++ b/src/bundles/maze/functions.ts @@ -12,11 +12,11 @@ import type { Action, AreaFlags, Area, AreaTest, - RobotMinigame + Maze } from './types'; // Default state before initialisation -const state: RobotMinigame = { +const state: Maze = { isInit: false, hasCollided: false, width: 500, @@ -31,7 +31,7 @@ const state: RobotMinigame = { }; // sets the context to the state obj, mostly for convenience so i dont have to type context.... everytime -context.moduleContexts.robot_minigame.state = state; +context.moduleContexts.maze.state = state; // ===== // // SETUP // diff --git a/src/bundles/robot_minigame/helpers/areas.ts b/src/bundles/maze/helpers/areas.ts similarity index 100% rename from src/bundles/robot_minigame/helpers/areas.ts rename to src/bundles/maze/helpers/areas.ts diff --git a/src/bundles/robot_minigame/helpers/tests.ts b/src/bundles/maze/helpers/tests.ts similarity index 100% rename from src/bundles/robot_minigame/helpers/tests.ts rename to src/bundles/maze/helpers/tests.ts diff --git a/src/bundles/robot_minigame/index.ts b/src/bundles/maze/index.ts similarity index 77% rename from src/bundles/robot_minigame/index.ts rename to src/bundles/maze/index.ts index edfb14d3da..b9785918ff 100644 --- a/src/bundles/robot_minigame/index.ts +++ b/src/bundles/maze/index.ts @@ -1,7 +1,7 @@ /** - * The robot_minigame module allows us to control a robot to complete various tasks + * The maze module allows us to guide a robot through a maze. * - * @module robot_minigame + * @module maze * @author Koh Wai Kei * @author Justin Cheng */ diff --git a/src/bundles/robot_minigame/types.ts b/src/bundles/maze/types.ts similarity index 84% rename from src/bundles/robot_minigame/types.ts rename to src/bundles/maze/types.ts index 902768078c..c3e51e4f30 100644 --- a/src/bundles/robot_minigame/types.ts +++ b/src/bundles/maze/types.ts @@ -26,27 +26,32 @@ export interface Action { position: PointWithRotation } +// Any optional flags an area may have (e.g. color) export interface AreaFlags { [name: string]: any } +// An area within the maze export interface Area { vertices: Point[] isObstacle: boolean flags: AreaFlags } +// A test for the student to pass export interface Test { type: string test: Function } +// A test testing an area export interface AreaTest extends Test { type: 'area' test: (areas: Area[]) => boolean } -export interface RobotMinigame { +// The main maze state +export interface Maze { isInit: boolean hasCollided: boolean width: number diff --git a/src/tabs/RobotMaze/RobotSimulation.tsx b/src/tabs/Maze/MazeSimulation.tsx similarity index 96% rename from src/tabs/RobotMaze/RobotSimulation.tsx rename to src/tabs/Maze/MazeSimulation.tsx index b1ba463584..afb394ab11 100644 --- a/src/tabs/RobotMaze/RobotSimulation.tsx +++ b/src/tabs/Maze/MazeSimulation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { run_tests } from '../../bundles/robot_minigame/helpers/tests'; -import type { Area, Action, BorderConfig, Robot, RobotMinigame } from '../../bundles/robot_minigame/types'; +import { run_tests } from '../../bundles/maze/helpers/tests'; +import type { Area, Action, BorderConfig, Robot, Maze } from '../../bundles/maze/types'; /** * Calculate the acute angle between 2 angles @@ -125,10 +125,10 @@ const ANIMATION_SPEED : number = 2; * React Component props for the Tab. */ interface MapProps { - state: RobotMinigame, + state: Maze, } -const RobotSimulation : React.FC = ({ +const MazeSimulation : React.FC = ({ state: { hasCollided, width, @@ -288,4 +288,4 @@ const RobotSimulation : React.FC = ({ ); }; -export default RobotSimulation; +export default MazeSimulation; diff --git a/src/tabs/RobotMaze/index.tsx b/src/tabs/Maze/index.tsx similarity index 71% rename from src/tabs/RobotMaze/index.tsx rename to src/tabs/Maze/index.tsx index c479770010..d05ee5282d 100644 --- a/src/tabs/RobotMaze/index.tsx +++ b/src/tabs/Maze/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; import type { DebuggerContext } from '../../typings/type_helpers'; -import RobotSimulation from './RobotSimulation'; +import MazeSimulation from './MazeSimulation'; /** - * Renders the robot minigame in the assessment workspace + * Renders the maze simulation in the assessment workspace * @author Koh Wai Kei * @author Justin Cheng */ @@ -18,9 +18,9 @@ interface MainProps { /** * The main React Component of the Tab. */ -const RobotMaze : React.FC = ({ context }) => { +const Maze : React.FC = ({ context }) => { return ( - + ); }; @@ -31,24 +31,24 @@ export default { * @param {DebuggerContext} context * @returns {boolean} */ - toSpawn: (context: DebuggerContext) => context.context?.moduleContexts?.robot_minigame.state.isInit, + toSpawn: (context: DebuggerContext) => context.context?.moduleContexts?.maze.state.isInit, /** * This function will be called to render the module tab in the side contents * on Source Academy frontend. * @param {DebuggerContext} context */ - body: (context: DebuggerContext) => , + body: (context: DebuggerContext) => , /** * The Tab's icon tooltip in the side contents on Source Academy frontend. */ - label: 'Robot Maze', + label: 'Maze', /** * BlueprintJS IconName element's name, used to render the icon which will be * displayed in the side contents panel. * @see https://blueprintjs.com/docs/#icons */ - iconName: 'build', + iconName: 'layout-grid', };