From e1f452c21af24701eeebe6f0fc784dcaf3002d6a Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Sun, 12 Jan 2025 18:52:41 +0000 Subject: [PATCH 01/16] Add check for variable only arguemnts --- .../extensions/placeholder-widget.ts | 1 - .../exercises/draw/DrawExercise.tsx | 55 +++++++++++- .../SolveExercisePage/test-runner/expect.ts | 7 ++ .../execGenericTest.ts | 11 ++- .../execProjectTest.ts | 11 ++- .../generateExpects.ts | 20 +++-- .../components/bootcamp/types/Matchers.d.ts | 1 + app/javascript/interpreter/executor.ts | 8 +- app/javascript/interpreter/frames.ts | 4 +- bootcamp_content/projects/drawing/config.json | 2 +- .../exercises/structured-house/config.json | 57 +++++++++++++ .../exercises/structured-house/example.jiki | 85 +++++++++++++++++++ .../structured-house/introduction.md | 38 +++++++++ .../exercises/structured-house/stub.jiki | 45 ++++++++++ .../exercises/structured-house/task-1.md | 18 ++++ db/bootcamp_seeds.rb | 51 ++++++----- 16 files changed, 371 insertions(+), 43 deletions(-) create mode 100644 bootcamp_content/projects/drawing/exercises/structured-house/config.json create mode 100644 bootcamp_content/projects/drawing/exercises/structured-house/example.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/structured-house/introduction.md create mode 100644 bootcamp_content/projects/drawing/exercises/structured-house/stub.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/structured-house/task-1.md diff --git a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/placeholder-widget.ts b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/placeholder-widget.ts index 71bee7ae03..2103b6deca 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/placeholder-widget.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/placeholder-widget.ts @@ -65,7 +65,6 @@ export function placeholderExtension() { } function generateArrowSVG(length: number, right: number, hoverRight: number) { - console.log(hoverRight) let offset = 30 right = right + offset length = length + 10 diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx index 26a5b82fef..a617889a99 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx @@ -2,6 +2,14 @@ import { Exercise } from '../Exercise' import { aToR, rToA } from './utils' import * as Shapes from './shapes' import type { ExecutionContext } from '@/interpreter/executor' +import { InterpretResult } from '@/interpreter/interpreter' +import { + CallExpression, + Expression, + LiteralExpression, +} from '@/interpreter/expression' +import { ExpressionStatement } from '@/interpreter/statement' +import { Frame } from '@/interpreter/frames' class Shape { public constructor(public element: SVGElement) {} @@ -146,10 +154,16 @@ export default class DrawExercise extends Exercise { public getState() { return {} } - public numElements() { + public numElements(_: InterpretResult) { return this.shapes.length } - public getRectangleAt(x: number, y: number, width: number, height: number) { + public getRectangleAt( + _: InterpretResult, + x: number, + y: number, + width: number, + height: number + ) { return this.shapes.find((shape) => { if (shape instanceof Rectangle) { if (x !== undefined) { @@ -178,14 +192,25 @@ export default class DrawExercise extends Exercise { } }) } - public getCircleAt(cx: number, cy: number, radius: number) { + public getCircleAt( + _: InterpretResult, + cx: number, + cy: number, + radius: number + ) { return this.shapes.find((shape) => { if (shape instanceof Circle) { return shape.cx == cx && shape.cy == cy && shape.radius == radius } }) } - public getEllipseAt(x: number, y: number, rx: number, ry: number) { + public getEllipseAt( + _: InterpretResult, + x: number, + y: number, + rx: number, + ry: number + ) { return this.shapes.find((shape) => { if (shape instanceof Ellipse) { return shape.x == x && shape.y == y && shape.rx == rx && shape.ry == ry @@ -193,6 +218,7 @@ export default class DrawExercise extends Exercise { }) } public getTriangleAt( + _: InterpretResult, x1: number, y1: number, x2: number, @@ -232,6 +258,27 @@ export default class DrawExercise extends Exercise { }) } + public assertAllArgumentsAreVariables(interpreterResult: InterpretResult) { + return interpreterResult.frames.every((frame: Frame) => { + if (!(frame.context instanceof ExpressionStatement)) { + return true + } + + const context = frame.context as ExpressionStatement + if (!(context.expression instanceof CallExpression)) { + return true + } + + return context.expression.args.every((arg: Expression) => { + if (arg instanceof LiteralExpression) { + return false + } + + return true + }) + }) + } + public changePenColor(executionCtx: ExecutionContext, color: string) { this.penColor = color } diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts index eb45843dce..ccd937c4b8 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts @@ -37,6 +37,13 @@ export function expect({ pass: actual === expected, } }, + toBeTrue() { + return { + ...returnObject, + expected: true, + pass: actual === true, + } + }, toEqual(expected: any) { return { ...returnObject, diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts index 4cc9010369..e5b5f375c0 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts @@ -27,9 +27,14 @@ export function execGenericTest( const codeRun = testData.function + '(' + params.join(', ') + ')' - const expects = generateExpects(options.config.testsType, testData, { - actual, - }) + const expects = generateExpects( + options.config.testsType, + evaluated, + testData, + { + actual, + } + ) return { expects, diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts index dc651addd0..ee3ae8450d 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts @@ -37,9 +37,14 @@ export function execProjectTest( ? new AnimationTimeline({}, frames).populateTimeline(animations) : null - const expects = generateExpects(options.config.testsType, testData, { - exercise, - }) + const expects = generateExpects( + options.config.testsType, + evaluated, + testData, + { + exercise, + } + ) return { expects, diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts index 0a8a5a0e61..1b1712e494 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts @@ -1,21 +1,27 @@ import { expect } from '../expect' import type { Exercise } from '../../exercises/Exercise' +import { InterpretResult } from '@/interpreter/interpreter' export function generateExpects( testsType: 'io' | 'state', + interpreterResult: InterpretResult, testData: TaskTest, { exercise, actual }: { exercise?: Exercise; actual?: any } ) { if (testsType == 'state') { - return generateExpectsForStateTests(exercise!, testData) + return generateExpectsForStateTests(exercise!, interpreterResult, testData) } else { - return generateExpectsForIoTests(testData, actual) + return generateExpectsForIoTests(interpreterResult, testData, actual) } } // These are normal function in/out tests. We always know the actual value at this point // (as it's returned from the function) so we can just compare it to the expected value. -function generateExpectsForIoTests(testData: TaskTest, actual: any) { +function generateExpectsForIoTests( + interpreterResult: InterpretResult, + testData: TaskTest, + actual: any +) { const matcher = testData.matcher || 'toEqual' return [ @@ -31,7 +37,11 @@ function generateExpectsForIoTests(testData: TaskTest, actual: any) { // These are the state tests, where we're comparing mutiple different variables or functions // on the resulting exercise. -function generateExpectsForStateTests(exercise: Exercise, testData: TaskTest) { +function generateExpectsForStateTests( + exercise: Exercise, + interpreterResult: InterpretResult, + testData: TaskTest +) { // We only need to do this once, so do it outside the loop. const state = exercise.getState() @@ -57,7 +67,7 @@ function generateExpectsForStateTests(exercise: Exercise, testData: TaskTest) { // And then we get the function and call it. const fn = exercise[fnName] - actual = fn.bind(exercise).call(exercise, ...args) + actual = fn.bind(exercise).call(exercise, interpreterResult, ...args) } // Our normal state is much easier! We just check the state object that diff --git a/app/javascript/components/bootcamp/types/Matchers.d.ts b/app/javascript/components/bootcamp/types/Matchers.d.ts index 7f35800776..24f580ae3b 100644 --- a/app/javascript/components/bootcamp/types/Matchers.d.ts +++ b/app/javascript/components/bootcamp/types/Matchers.d.ts @@ -1,5 +1,6 @@ declare type AvailableMatchers = | 'toBe' + | 'toBeTrue' | 'toExist' | 'toEqual' | 'toBeGreaterThanOrEqual' diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index 02c5c770c2..b427d07242 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -185,7 +185,7 @@ export class Executor ): T { this.location = context.location const result = code() - this.addFrame(context.location, 'SUCCESS', result) + this.addFrame(context.location, 'SUCCESS', result, undefined, context) this.location = null return result.value } @@ -471,8 +471,6 @@ export class Executor const arity = callee.value.arity() const [minArity, maxArity] = isNumber(arity) ? [arity, arity] : arity - console.log(minArity, maxArity) - if (args.length < minArity || args.length > maxArity) { if (minArity !== maxArity) { this.error( @@ -938,7 +936,8 @@ export class Executor location: Location | null, status: FrameExecutionStatus, result?: EvaluationResult, - error?: RuntimeError + error?: RuntimeError, + context?: Statement | Expression ): void { if (location == null) location = Location.unknown @@ -952,6 +951,7 @@ export class Executor functions: this.environment.functions(), time: this.frameTime, description: '', + context: context, } frame.description = describeFrame(frame, this.externalFunctions) diff --git a/app/javascript/interpreter/frames.ts b/app/javascript/interpreter/frames.ts index a6757d4e04..072e353bc3 100644 --- a/app/javascript/interpreter/frames.ts +++ b/app/javascript/interpreter/frames.ts @@ -4,7 +4,8 @@ import { RuntimeError } from './error' export type FrameExecutionStatus = 'SUCCESS' | 'ERROR' import type { EvaluationResult } from './evaluation-result' import type { ExternalFunction } from './executor' -import { BinaryExpression, Expression } from './expression' +import { Expression } from './expression' +import { Statement } from './statement' export type FrameType = 'ERROR' | 'REPEAT' | 'EXPRESSION' @@ -19,6 +20,7 @@ export type Frame = { result?: EvaluationResult data?: Record description: string + context?: Statement | Expression } export type FrameWithResult = Frame & { result: EvaluationResult } diff --git a/bootcamp_content/projects/drawing/config.json b/bootcamp_content/projects/drawing/config.json index b540545951..b4fa1474da 100644 --- a/bootcamp_content/projects/drawing/config.json +++ b/bootcamp_content/projects/drawing/config.json @@ -2,5 +2,5 @@ "slug": "drawing", "title": "Drawing", "description": "With only a few basic shapes you can draw and animate almost anything.", - "exercises": ["penguin", "jumbled-house", "loops"] + "exercises": ["penguin", "jumbled-house", "structured-house"] } diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/config.json b/bootcamp_content/projects/drawing/exercises/structured-house/config.json new file mode 100644 index 0000000000..bc955a018d --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/config.json @@ -0,0 +1,57 @@ +{ + "title": "Structured House", + "description": "Use variables instead of hard-coded values", + "project_type": "draw", + "level": 1, + "concepts": [], + "tests_type": "state", + "tasks": [ + { + "name": "Correctly arrange the house", + "tests": [ + { + "slug": "draw-the-house", + "name": "Position the frame of the house.", + "function": "main", + "checks": [ + { + "name": "getRectangleAt(20,50,60,40)", + "matcher": "toExist", + "error_html": "The frame of the house is not correct." + }, + { + "name": "getTriangleAt(16,50, 50,30, 84,50)", + "matcher": "toExist", + "error_html": "The roof of the house is not at the correct position." + }, + { + "name": "getRectangleAt(30,55,12,13)", + "matcher": "toExist", + "error_html": "The left window frame isn't positioned correctly" + }, + { + "name": "getRectangleAt(58,55,12,13)", + "matcher": "toExist", + "error_html": "The right window frame isn't positioned correctly" + }, + { + "name": "getRectangleAt(43,72,14,18)", + "matcher": "toExist", + "error_html": "The door frame isn't positioned correctly" + }, + { + "name": "getCircleAt(55,81,1)", + "matcher": "toExist", + "error_html": "The door knob isn't positiioned correctly" + }, + { + "name": "assertAllArgumentsAreVariables()", + "matcher": "toBeTrue", + "error_html": "There still seem to be some inputs to functions that are not variables." + } + ] + } + ] + } + ] +} diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/example.jiki b/bootcamp_content/projects/drawing/exercises/structured-house/example.jiki new file mode 100644 index 0000000000..72ea35c642 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/example.jiki @@ -0,0 +1,85 @@ +// Sky variables +set sky_color to "#add8e6" +set sky_left to 0 +set sky_top to 0 +set sky_width to 100 +set sky_height to 20 + +// Grass variables +set grass_color to "#3cb372" +set grass_left to 0 +set grass_top to 80 +set grass_width to 100 +set grass_height to 20 + +// House Frame variables +set house_color to "#f0985b" +set house_left to 20 +set house_top to 50 +set house_width to 60 +set house_height to 40 + +// Roof variables +set roof_color to "#8b4513" +set roof_overhang to 4 +set roof_left to house_left - roof_overhang +set roof_right to house_left + house_width + roof_overhang +set roof_peak_x to house_left + house_width / 2 +set roof_peak_y to house_top - 20 +set roof_base_y to house_top + +// Left window variables +set window_color to "#FFFFFF" +set window1_left to 30 +set window1_top to 55 +set window_width to 12 +set window_height to 13 + +// Right window variables +set window2_left to 58 +set window2_top to 55 + +// Door variables +set door_color to "#A0512D" +set door_left to 43 +set door_top to 72 +set door_width to 14 +set door_height to 18 + +// Door knob variables +set knob_color to "#FFDF00" +set knob_center_x to 55 +set knob_center_y to 81 +set knob_radius to 1 + +// The sky +fill_color_hex(sky_color) +rectangle(sky_left, sky_top, sky_width, sky_height) + +// The grass +fill_color_hex(grass_color) +rectangle(grass_left, grass_top, grass_width, grass_height) + +// The frame of the house +fill_color_hex(house_color) +rectangle(house_left, house_top, house_width, house_height) + +// The roof +fill_color_hex(roof_color) +triangle(roof_left, roof_base_y, roof_peak_x, roof_peak_y, roof_right, roof_base_y) + +// The left window +fill_color_hex(window_color) +rectangle(window1_left, window1_top, window_width, window_height) + +// The second window +fill_color_hex(window_color) +rectangle(window2_left, window2_top, window_width, window_height) + +// The door +fill_color_hex(door_color) +rectangle(door_left, door_top, door_width, door_height) + +// The door knob +fill_color_hex(knob_color) +circle(knob_center_x, knob_center_y, knob_radius) \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/introduction.md b/bootcamp_content/projects/drawing/exercises/structured-house/introduction.md new file mode 100644 index 0000000000..62d2a05dc7 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/introduction.md @@ -0,0 +1,38 @@ +# Structured House + +Your task is to use variables to build the house. + +Change all the inputs in the functions to use variables. + +Set all the variables at the top before the first functions are used. We've set the first few to get you started. + +Every variable should either be: + +- A number that is specified in the instructions (e.g. the height of the frame); or +- A formula between two variables (e.g. `set roof_left to house_left - roof_overhang`) or a variable and a number specified in the instructions (e.g. `set door_knob_right to door_right - 1`). + +Do **not** manually set variables to numbers you've calculated yourself (e.g. DO NOT set `roof_left = 16`) + +The purpose of this exercise is get keep pushing you towards structured, ordered thinking. Take your time. + +As a reminder, the house should continue to look like this: + + + +### House Instructions + +- The frame of the house (the big square) should be 60 wide and 40 height. It should have it's top-left corner at 20x50. +- The roof sits snuggly on top of the house's frame. It should overhang the left and right of the house by 4 on each side. It should have a height of 20, and it's point should be centered horizontally (50). +- The windows are both the same size, with have a width of 12 and a height of 13. They both sit 5 from the top of the house frame, and 10 inset from the sides. +- The door is 14 wide and 18 tall, and sits at the bottom of the house in the center. +- The little door knob has a radius of 1, is inset 1 from the right, and is vertically centered in the door. + +### Functions + +The house uses the following functions: + +- `circle(x, y, radius)` +- `rectangle(x, y, width, height)` +- `ellipse(center_x, center_y, radius_x, radius_y)` +- `triangle(x1,y1, x2,y2, x3,y3)` +- `fill_color_hex(hex)` diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/stub.jiki b/bootcamp_content/projects/drawing/exercises/structured-house/stub.jiki new file mode 100644 index 0000000000..213f2a7f31 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/stub.jiki @@ -0,0 +1,45 @@ +// Sky variables +set sky_color to "#add8e6" +set sky_left to 0 +set sky_top to 0 +set sky_width to 100 +set sky_height to 0 + +// House Frame variables +set house_left to 20 + +// Roof variables +set roof_overhang to 4 +set roof_left to house_left - roof_overhang + +// The sky +fill_color_hex(sky_color) +rectangle(sky_left, sky_top, sky_width, sky_height) + +// The grass +fill_color_hex("#3cb372") +rectangle(0,80,100,100) + +// The frame of the house +fill_color_hex("#f0985b") +rectangle(house_left,50,60,40) + +// The roof +fill_color_hex("#8b4513") +triangle(roof_left,50, 50,30, 84,50) + +// The left window +fill_color_hex("#FFFFFF") +rectangle(30,55,12,13) + +// The second window +fill_color_hex("#FFFFFF") +rectangle(58,55,12,13) + +// The door +fill_color_hex("#A0512D") +rectangle(43,72,14,18) + +// The door knob +fill_color_hex("#FFDF00") +circle(55,81,1) \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/task-1.md b/bootcamp_content/projects/drawing/exercises/structured-house/task-1.md new file mode 100644 index 0000000000..2fc07c6618 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/task-1.md @@ -0,0 +1,18 @@ +Change all the function calls to use variables not numbers. + +For example, change: + +``` +// The frame of the house +rectangle(house_left,50,60,40) +``` + +to + +``` +set house_left to 20 +set house_top to 50 +set house_width to 60 +set house_height to 40 +rectangle(house_left, house_top, house_width, house_height) +``` diff --git a/db/bootcamp_seeds.rb b/db/bootcamp_seeds.rb index 7ea767f749..0bfe8f5d38 100644 --- a/db/bootcamp_seeds.rb +++ b/db/bootcamp_seeds.rb @@ -1,14 +1,4 @@ -return unless Rails.env.development? - -# rubocop:disable Layout/LineLength -Bootcamp::UserProject.destroy_all -Bootcamp::Submission.destroy_all -Bootcamp::Solution.destroy_all -Bootcamp::Exercise.destroy_all -Bootcamp::Project.destroy_all -Bootcamp::Concept.destroy_all -Bootcamp::Level.destroy_all - +# rubocop:disable Layout/LineLength: def concept_intro_for(slug) = File.read(Rails.root / "bootcamp_content/concepts/#{slug}.md") def project_config_for(slug) = JSON.parse(File.read(Rails.root / "bootcamp_content/projects/#{slug}/config.json"), @@ -23,11 +13,17 @@ def exercise_config_for(project_slug, exercise_slug) = JSON.parse( File.read(Rails.root / "bootcamp_content/projects/#{project_slug}/exercises/#{exercise_slug}/config.json"), symbolize_names: true ) +# rubocop:enable Layout/LineLength: JSON.parse(File.read(Rails.root / "bootcamp_content/levels/config.json"), symbolize_names: true).each.with_index do |details, idx| idx += 1 - Bootcamp::Level.create!( - idx:, + level = Bootcamp::Level.find_or_create_by!(idx:) do |l| + l.title = "" + l.description = "" + l.content_markdown = "" + end + + level.update!( title: details[:title], description: details[:description], content_markdown: File.read(Rails.root / "bootcamp_content/levels/#{idx}.md") @@ -35,8 +31,14 @@ def exercise_config_for(project_slug, end JSON.parse(File.read(Rails.root / "bootcamp_content/concepts/config.json"), symbolize_names: true).each do |details| - Bootcamp::Concept.create!( - slug: details[:slug], + concept = Bootcamp::Concept.find_or_create_by!(slug: details[:slug]) do |c| + c.title = "" + c.description = "" + c.content_markdown = "" + c.level_idx = details[:level] + end + + concept.update!( parent: details[:parent] ? Bootcamp::Concept.find_by!(slug: details[:parent]) : nil, title: details[:title], description: details[:description], @@ -58,16 +60,25 @@ def exercise_config_for(project_slug, projects.each do |project_slug| project_config = project_config_for(project_slug) - project = Bootcamp::Project.create!( - slug: project_slug, + project = Bootcamp::Project.find_or_create_by!(slug: project_slug) do |p| + p.title = project_config[:title] + p.description = project_config[:description] + p.introduction_markdown = project_intro_for(project_slug) + end + project.update!( title: project_config[:title], description: project_config[:description], introduction_markdown: project_intro_for(project_slug) ) project_config[:exercises].each.with_index do |exercise_slug, idx| exercise_config = exercise_config_for(project_slug, exercise_slug) - project.exercises.create!( - slug: exercise_slug, + exercise = project.exercises.find_or_create_by!(slug: exercise_slug) do |e| + e.idx = idx + 1 + e.title = "" + e.description = "" + e.level_idx = exercise_config[:level] + end + exercise.update!( idx: idx + 1, title: exercise_config[:title], description: exercise_config[:description], @@ -80,5 +91,3 @@ def exercise_config_for(project_slug, end # Bootcamp::UserProject::CreateAll.(User.find_by!(handle: 'iHiD')) - -# rubocop:enable Layout/LineLength From 419e5f1f96aebf988eafdf706dd008269b1314c0 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Sun, 12 Jan 2025 23:28:08 +0000 Subject: [PATCH 02/16] More content --- .../AnimationTimeline/AnimationTimeline.ts | 1 + .../SolveExercisePage/Scrubber/useScrubber.ts | 10 ++-- .../exercises/draw/DrawExercise.tsx | 27 ++++++--- .../exercises/draw/shapes.ts | 7 ++- .../SolveExercisePage/test-runner/expect.ts | 6 ++ .../components/bootcamp/types/Matchers.d.ts | 1 + app/javascript/interpreter/executor.ts | 24 ++++++-- bootcamp_content/projects/drawing/config.json | 8 ++- .../exercises/sprouting-flower/config.json | 32 +++++++++++ .../exercises/sprouting-flower/example.jiki | 45 +++++++++++++++ .../sprouting-flower/introduction.md | 5 ++ .../exercises/sprouting-flower/stub.jiki | 21 +++++++ .../exercises/sprouting-flower/task-1.md | 1 + .../exercises/structured-house/config.json | 2 +- .../drawing/exercises/sunset/config.json | 42 ++++++++++++++ .../drawing/exercises/sunset/example.jiki | 35 ++++++++++++ .../drawing/exercises/sunset/introduction.md | 30 ++++++++++ .../drawing/exercises/sunset/stub.jiki | 20 +++++++ .../drawing/exercises/sunset/task-1.md | 1 + bootcamp_content/projects/golf/config.json | 6 ++ .../golf/exercises/rolling-ball/config.json | 55 +++++++++++++++++++ .../golf/exercises/rolling-ball/example.jiki | 35 ++++++++++++ .../exercises/rolling-ball/introduction.md | 24 ++++++++ .../golf/exercises/rolling-ball/stub.jiki | 14 +++++ .../golf/exercises/rolling-ball/task-1.md | 1 + .../projects/golf/introduction.md | 7 +++ .../exercises/even-or-odd/config.json | 2 +- .../positive-negative-or-zero/config.json | 2 +- .../exercises/basic/config.json | 2 +- .../two-fer/exercises/basic/config.json | 2 +- .../exercises/cloud-rain-sun/introduction.md | 6 +- db/bootcamp_seeds.rb | 1 + 32 files changed, 448 insertions(+), 27 deletions(-) create mode 100644 bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json create mode 100644 bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md create mode 100644 bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md create mode 100644 bootcamp_content/projects/drawing/exercises/sunset/config.json create mode 100644 bootcamp_content/projects/drawing/exercises/sunset/example.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/sunset/introduction.md create mode 100644 bootcamp_content/projects/drawing/exercises/sunset/stub.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/sunset/task-1.md create mode 100644 bootcamp_content/projects/golf/config.json create mode 100644 bootcamp_content/projects/golf/exercises/rolling-ball/config.json create mode 100644 bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki create mode 100644 bootcamp_content/projects/golf/exercises/rolling-ball/introduction.md create mode 100644 bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki create mode 100644 bootcamp_content/projects/golf/exercises/rolling-ball/task-1.md create mode 100644 bootcamp_content/projects/golf/introduction.md diff --git a/app/javascript/components/bootcamp/SolveExercisePage/AnimationTimeline/AnimationTimeline.ts b/app/javascript/components/bootcamp/SolveExercisePage/AnimationTimeline/AnimationTimeline.ts index 38ec3d4872..b195e4146a 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/AnimationTimeline/AnimationTimeline.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/AnimationTimeline/AnimationTimeline.ts @@ -42,6 +42,7 @@ export class AnimationTimeline { public populateTimeline(animations: Animation[]) { animations.forEach((animation: Animation) => { + // console.log(animation.offset) this.animationTimeline.add( { ...animation, ...animation.transformations }, animation.offset diff --git a/app/javascript/components/bootcamp/SolveExercisePage/Scrubber/useScrubber.ts b/app/javascript/components/bootcamp/SolveExercisePage/Scrubber/useScrubber.ts index 99e96925cf..3e1597402c 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/Scrubber/useScrubber.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/Scrubber/useScrubber.ts @@ -9,6 +9,8 @@ import type { StaticError } from '@/interpreter/error' import { INFO_HIGHLIGHT_COLOR } from '../CodeMirror/extensions/lineHighlighter' import { scrollToHighlightedLine } from './scrollToHighlightedLine' +const FRAME_DURATION = 50 + export function useScrubber({ setIsPlaying, testResult, @@ -32,7 +34,7 @@ export function useScrubber({ testResult.animationTimeline.onUpdate((anime) => { setTimeout(() => { setValue(anime.currentTime) - }, 50) + }, FRAME_DURATION) }) } }, [testResult.view?.id, testResult.animationTimeline?.completed]) @@ -155,7 +157,7 @@ export function useScrubber({ targets: { value }, // if progress is closer to duration than time, then snap to duration value: closestTime, - duration: 50, + duration: FRAME_DURATION, easing: 'easeOutQuad', update: function (anim) { const newTime = Number(anim.animations[0].currentValue) @@ -214,7 +216,7 @@ export function useScrubber({ scrubberValueAnimation.current = anime({ targets: { value }, value: targetTime, - duration: 50, + duration: FRAME_DURATION, easing: 'easeOutQuad', update: function (anim) { const animatedTime = Number(anim.animations[0].currentValue) @@ -253,7 +255,7 @@ export function useScrubber({ scrubberValueAnimation.current = anime({ targets: { value }, value: targetTime, - duration: 50, + duration: FRAME_DURATION, easing: 'easeOutQuad', update: function (anim) { const animatedTime = Number(anim.animations[0].currentValue) diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx index a617889a99..4a5e329fea 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx @@ -67,10 +67,12 @@ class Triangle extends Shape { export type FillColor = | { type: 'hex'; color: string } | { type: 'rgb'; color: [number, number, number] } + | { type: 'hsl'; color: [number, number, number] } export default class DrawExercise extends Exercise { private canvas: HTMLDivElement private shapes: Shape[] = [] + private visibleShapes: Shape[] = [] private penColor = '#333333' private fillColor: FillColor = { type: 'hex', color: '#ff0000' } @@ -279,15 +281,18 @@ export default class DrawExercise extends Exercise { }) } - public changePenColor(executionCtx: ExecutionContext, color: string) { + public changePenColor(_: ExecutionContext, color: string) { this.penColor = color } - public fillColorHex(executionCtx: ExecutionContext, color: string) { + public fillColorHex(_: ExecutionContext, color: string) { this.fillColor = { type: 'hex', color: color } } - public fillColorRGB(executionCtx: ExecutionContext, red, green, blue) { + public fillColorRGB(_: ExecutionContext, red, green, blue) { this.fillColor = { type: 'rgb', color: [red, green, blue] } } + public fillColorHSL(_: ExecutionContext, h, s, l) { + this.fillColor = { type: 'hsl', color: [h, s, l] } + } public rectangle( executionCtx: ExecutionContext, @@ -312,6 +317,7 @@ export default class DrawExercise extends Exercise { const rect = new Rectangle(x, y, width, height, elem) this.shapes.push(rect) + this.visibleShapes.push(rect) this.animateElement(executionCtx, elem, absX, absY) return rect } @@ -335,6 +341,7 @@ export default class DrawExercise extends Exercise { const circle = new Circle(x, y, radius, elem) this.shapes.push(circle) + this.visibleShapes.push(circle) this.animateElement(executionCtx, elem, absX, absY) return circle } @@ -360,6 +367,7 @@ export default class DrawExercise extends Exercise { const ellipse = new Ellipse(x, y, rx, ry, elem) this.shapes.push(ellipse) + this.visibleShapes.push(ellipse) this.animateElement(executionCtx, elem, absX, absY) return ellipse } @@ -396,6 +404,7 @@ export default class DrawExercise extends Exercise { const triangle = new Triangle(x1, y1, x2, y2, x3, y3, elem) this.shapes.push(triangle) + this.visibleShapes.push(triangle) this.animateElement(executionCtx, elem, absX1, absY1) return triangle } @@ -461,13 +470,11 @@ export default class DrawExercise extends Exercise { }, offset: executionCtx.getCurrentTime(), }) - - executionCtx.fastForward(duration) } public clear(executionCtx: ExecutionContext) { const duration = 1 - this.shapes.forEach((shape) => { + this.visibleShapes.forEach((shape) => { this.addAnimation({ targets: `#${this.view.id} #${shape.element.id}`, duration, @@ -477,9 +484,8 @@ export default class DrawExercise extends Exercise { offset: executionCtx.getCurrentTime(), }) }) - executionCtx.fastForward(duration) - this.shapes = [] + this.visibleShapes = [] } public setBackgroundImage(imageUrl: string) { @@ -551,5 +557,10 @@ export default class DrawExercise extends Exercise { func: this.fillColorRGB.bind(this), description: 'Changes the fill color using three RGB values', }, + { + name: 'fill_color_hsl', + func: this.fillColorHSL.bind(this), + description: 'Changes the fill color using three HSL values', + }, ] } diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/shapes.ts b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/shapes.ts index 87afd62890..9f03681ec3 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/shapes.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/shapes.ts @@ -32,8 +32,13 @@ function createSVGElement( if (backgroundColor.type === 'hex') { elem.setAttribute('fill', backgroundColor.color) - } else { + } else if (backgroundColor.type === 'rgb') { elem.setAttribute('fill', 'rgb(' + backgroundColor.color.join(',') + ')') + } else { + elem.setAttribute( + 'fill', + `hsl(${backgroundColor.color[0]}, ${backgroundColor.color[1]}%, ${backgroundColor.color[2]}%)` + ) } for (const key in attrs) { diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts index ccd937c4b8..fdc0009209 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts @@ -30,6 +30,12 @@ export function expect({ pass: actual !== undefined && actual !== null, } }, + toNotExist() { + return { + ...returnObject, + pass: actual === undefined || actual === null, + } + }, toBe(expected: any) { return { ...returnObject, diff --git a/app/javascript/components/bootcamp/types/Matchers.d.ts b/app/javascript/components/bootcamp/types/Matchers.d.ts index 24f580ae3b..e2bf92f3f1 100644 --- a/app/javascript/components/bootcamp/types/Matchers.d.ts +++ b/app/javascript/components/bootcamp/types/Matchers.d.ts @@ -2,6 +2,7 @@ declare type AvailableMatchers = | 'toBe' | 'toBeTrue' | 'toExist' + | 'toNotExist' | 'toEqual' | 'toBeGreaterThanOrEqual' | 'toBeLessThanOrEqual' diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index b427d07242..40991e0c90 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -637,17 +637,25 @@ export class Executor } case 'MINUS': this.verifyNumberOperands(expression.operator, left.value, right.value) + + const minusValue = left.value - right.value + const minusValue2DP = Math.round(minusValue * 10) / 10 + return { ...result, - value: left.value - right.value, + value: minusValue2DP, } //> binary-plus case 'PLUS': - if (isNumber(left.value) && isNumber(right.value)) + if (isNumber(left.value) && isNumber(right.value)) { + const value = left.value + right.value + const value2DP = Math.round(value * 10) / 10 + return { ...result, - value: left.value + right.value, + value: value2DP, } + } if (isString(left.value) && isString(right.value)) return { ...result, @@ -665,18 +673,24 @@ export class Executor case 'SLASH': this.verifyNumberOperands(expression.operator, left.value, right.value) + const slashValue = left.value / right.value + const slashValue2DP = Math.round(slashValue * 10) / 10 return { ...result, - value: left.value / right.value, + value: slashValue2DP, } case 'STAR': this.verifyNumberOperands(expression.operator, left.value, right.value) + + const starValue = left.value / right.value + const starValue2DP = Math.round(starValue * 10) / 10 return { ...result, - value: left.value * right.value, + value: starValue2DP, } case 'PERCENT': this.verifyNumberOperands(expression.operator, left.value, right.value) + return { ...result, value: left.value % right.value, diff --git a/bootcamp_content/projects/drawing/config.json b/bootcamp_content/projects/drawing/config.json index b4fa1474da..67bfba5efa 100644 --- a/bootcamp_content/projects/drawing/config.json +++ b/bootcamp_content/projects/drawing/config.json @@ -2,5 +2,11 @@ "slug": "drawing", "title": "Drawing", "description": "With only a few basic shapes you can draw and animate almost anything.", - "exercises": ["penguin", "jumbled-house", "structured-house"] + "exercises": [ + "penguin", + "jumbled-house", + "structured-house", + "sunset", + "sprouting-flower" + ] } diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json new file mode 100644 index 0000000000..816f6ad584 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -0,0 +1,32 @@ +{ + "title": "Sprouting flower", + "description": "Make the flower sprout", + "project_type": "draw", + "level": 2, + "concepts": [], + "tests_type": "state", + "interpreter_options": { + "repeat_delay": 30 + }, + "readonly_ranges": [], + "tasks": [ + { + "name": "Draw the scene", + "tests": [ + { + "slug": "draw-scence", + "name": "Make the penguin symmetrical.", + "description_html": "Fix all the TODO comments to make the penguin symmetrical.", + "function": "main", + "checks": [ + { + "name": "getTriangleAt(46, 38, 54, 38, 50, 47)", + "matcher": "toExist", + "error_html": "The nose isn't right." + } + ] + } + ] + } + ] +} diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki b/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki new file mode 100644 index 0000000000..fbad8ea30b --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki @@ -0,0 +1,45 @@ +// Flower +set flower_cx to 50 +set flower_cy to 90 +set flower_radius to 0 + +// pistil (the middle bit) +set pistil_radius to 0 + +// Stem +set stem_height to 0 +set stem_width to 0 +set stem_left to 0 +set stem_bottom to 90 + +repeat 60 do + change stem_height to stem_height + 1 + change stem_width to stem_height / 10 + change stem_left to flower_cx - ( stem_width / 2) + change flower_radius to flower_radius + 0.4 + change flower_cy to stem_bottom - stem_height + change pistil_radius to pistil_radius + 0.1 + + // Sky + fill_color_hex("#ADD8E6") + rectangle(0, 0, 100, 90) + + // Ground + fill_color_hex("green") + rectangle(0, 90, 100, 30) + + // Draw stem + fill_color_hex("darkgreen") + rectangle(stem_left, flower_cy, stem_width, stem_height) + + // Left leaf + fill_color_hex("darkgreen") + ellipse(stem_left - (flower_radius / 2), flower_cy + (stem_height / 2), flower_radius / 2, flower_radius / 5) + ellipse(stem_left + (flower_radius / 2) + stem_width, flower_cy + (stem_height / 2), flower_radius / 2, flower_radius / 5) + + fill_color_hex("#d90166") + circle(flower_cx, flower_cy, flower_radius) + + fill_color_hex("yellow") + circle(flower_cx, flower_cy, pistil_radius) +end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md new file mode 100644 index 0000000000..21357e609f --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md @@ -0,0 +1,5 @@ +None of the individual things you need to do are hard. But putting them together may feel daunting and unfamiliar. Plan first. Then take each step at a time, and you'll get there. + +This is definitely a tricky exercise and will take time to get right! + +If you need help, please ask on the forum, and remember to give us lots of information about what's not working and why you think that's the case! diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki new file mode 100644 index 0000000000..634290500c --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki @@ -0,0 +1,21 @@ +// Sky +fill_color_hex("#ADD8E6") +rectangle(0, 0, 100, 100) + +// Ground +fill_color_hex("green") +rectangle(0, 90, 100, 30) + +// Stem +set stem_height to 10 +set stem_left to 58 +set stem_width to 4 +set stem_bottom to 90 + +repeat 40 do + clear() + + // Draw stem + fill_color_hex("darkgreen") + rectangle(stem_left, stem_bottom - stem_height, stem_width, stem_height) +end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md new file mode 100644 index 0000000000..601c75a18e --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md @@ -0,0 +1 @@ +Make the flower sprout! diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/config.json b/bootcamp_content/projects/drawing/exercises/structured-house/config.json index bc955a018d..f438de4089 100644 --- a/bootcamp_content/projects/drawing/exercises/structured-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/structured-house/config.json @@ -2,7 +2,7 @@ "title": "Structured House", "description": "Use variables instead of hard-coded values", "project_type": "draw", - "level": 1, + "level": 2, "concepts": [], "tests_type": "state", "tasks": [ diff --git a/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json new file mode 100644 index 0000000000..7f125b1732 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -0,0 +1,42 @@ +{ + "title": "Sunset", + "description": "Make the sun set", + "project_type": "draw", + "level": 2, + "concepts": [], + "tests_type": "state", + "interpreter_options": { + "repeat_delay": 20 + }, + "readonly_ranges": [], + "tasks": [ + { + "name": "Draw the scene", + "tests": [ + { + "slug": "draw-scene", + "name": "Make the sun set", + "description_html": "Animate the sun and the sky to make it look like the sun is setting.", + "function": "main", + "checks": [ + { + "name": "getCircleAt(50, 11, 5.2)", + "matcher": "toExist", + "error_html": "The sun seems wrong near the beginning." + }, + { + "name": "getCircleAt(50, 20, 7)", + "matcher": "toExist", + "error_html": "The sun seems wrong near the middle." + }, + { + "name": "getCircleAt(50, 109, 24.8)", + "matcher": "toExist", + "error_html": "The sun seems wrong near the end." + } + ] + } + ] + } + ] +} diff --git a/bootcamp_content/projects/drawing/exercises/sunset/example.jiki b/bootcamp_content/projects/drawing/exercises/sunset/example.jiki new file mode 100644 index 0000000000..a5b8c85b4f --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/example.jiki @@ -0,0 +1,35 @@ +set sun_radius to 5 +set sun_cy to 10 + +set sun_red to 255 +set sun_green to 237 +set sun_blue to 0 + +set sky_h to 210 +set sky_s to 70 +set sky_l to 60 + +repeat 100 do + + // The sky + //change sky_s to sky_s - 0.2 + //change sky_l to sky_l + 0.2 + change sky_h to sky_h + 0.4 + fill_color_hsl(sky_h, sky_s, sky_l) + rectangle(0,0,100,100) + + // The Sun + change sun_green to sun_green - 1 + change sun_cy to sun_cy + 1 + change sun_radius to sun_radius + 0.2 + fill_color_rgb(sun_red, sun_green, sun_blue) + circle(50, sun_cy, sun_radius) + + // The sea + fill_color_hex("#0308ce") + rectangle(0,85,100,5) + + // The sand + fill_color_hex("#C2B280") + rectangle(0,90,100,10) +end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sunset/introduction.md b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md new file mode 100644 index 0000000000..a85203dd12 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md @@ -0,0 +1,30 @@ +# Sunset + +Your task is to animate the following scene to make the sun set. + +The animation should last `100` iterations and look something like this. + + + +We've drawn the initial scene for you. You need to animate a few things: + +- The size of the sun. It should start with a radius of `5` and grow by `0.2` each iteration. +- The position of the sun. It has an initial center of `50, 10`, and should lower in the sky by `1` each iteration. +- The color of the sun from yellow to orange. You can use RGB or HSL. Use a color picker such as [this](https://htmlcolorcodes.com/color-picker/) to find a starting point and ending point you want to animate between. +- The color of the sky. Again, you can use RGB or HSL. It's not possible to animate a true set of sunset colors this way, but be creative and choose something you like. + +Remember to `set` the initial values **outside** of the `repeat` block, and then `change` them **inside** the block. +If you need a recap on how to animate things, make sure to watch the Level 2 live session back. + +The animation will flash a bit. That's expected. We'll learn how to fix that in a future lesson. + +The functions used in this exercise are: + +- `circle(center_x, center_y, radius)` +- `rectangle(x, y, width, height)` +- `fill_color_rgb(red, green, blue)` +- `fill_color_hsl(hue, saturation, luminosity)` + +If you need help remembering how to use any of these functions, you can watch back the video from week 1. + +None of the individual things you need to do are hard. But putting them together may feel daunting and unfamiliar. Plan first. Then take each step at a time, and you'll get there. If you need help, please ask on the forum, and remember to give us lots of information about what's not working and why you think that's the case! diff --git a/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki b/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki new file mode 100644 index 0000000000..d854574a4b --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki @@ -0,0 +1,20 @@ +set sun_radius to 5 +set sun_cy to 10 + +repeat 100 do + // The sky + fill_color_hsl(210, 70, 60) + rectangle(0,0,100,100) + + // The Sun + fill_color_rgb(255, 237, 0) + circle(50, sun_cy, sun_radius) + + // The sea + fill_color_hex("#0308ce") + rectangle(0,85,100,5) + + // The sand + fill_color_hex("#C2B280") + rectangle(0,90,100,10) +end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sunset/task-1.md b/bootcamp_content/projects/drawing/exercises/sunset/task-1.md new file mode 100644 index 0000000000..7d562d77a9 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/task-1.md @@ -0,0 +1 @@ +Make the sun set! diff --git a/bootcamp_content/projects/golf/config.json b/bootcamp_content/projects/golf/config.json new file mode 100644 index 0000000000..ab92745805 --- /dev/null +++ b/bootcamp_content/projects/golf/config.json @@ -0,0 +1,6 @@ +{ + "slug": "golf", + "title": "Golf", + "description": "Let's play golf!", + "exercises": ["rolling-ball"] +} diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json new file mode 100644 index 0000000000..d98e3b6f72 --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json @@ -0,0 +1,55 @@ +{ + "title": "Rolling Ball", + "description": "Make the ball roll to the hole", + "project_type": "draw", + "level": 2, + "concepts": [], + "tests_type": "state", + "interpreter_options": { + "repeat_delay": 20 + }, + "readonly_ranges": [], + "tasks": [ + { + "name": "Draw the scene", + "tests": [ + { + "slug": "draw-scene", + "name": "Make the ball roll to the hole", + "description_html": "Animate the ball's x coordinate to make it roll to the hole", + "function": "main", + "setup_functions": [ + [ + "setBackgroundImage", + [ + "https://assets.exercism.org/bootcamp/graphics/golf-rolling-ball.png" + ] + ] + ], + "checks": [ + { + "name": "getCircleAt(27, 75, 3)", + "matcher": "toNotExist", + "error_html": "The ball seems to go too far to the left." + }, + { + "name": "getCircleAt(29, 75, 3)", + "matcher": "toExist", + "error_html": "The ball doesn't seem to start in the right place." + }, + { + "name": "getCircleAt(87, 75, 3)", + "matcher": "toExist", + "error_html": "The ball doesn't seem to reach the hole." + }, + { + "name": "getCircleAt(88, 75, 3)", + "matcher": "toNotExist", + "error_html": "The ball seems to go too far to the right." + } + ] + } + ] + } + ] +} diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki new file mode 100644 index 0000000000..a5b8c85b4f --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -0,0 +1,35 @@ +set sun_radius to 5 +set sun_cy to 10 + +set sun_red to 255 +set sun_green to 237 +set sun_blue to 0 + +set sky_h to 210 +set sky_s to 70 +set sky_l to 60 + +repeat 100 do + + // The sky + //change sky_s to sky_s - 0.2 + //change sky_l to sky_l + 0.2 + change sky_h to sky_h + 0.4 + fill_color_hsl(sky_h, sky_s, sky_l) + rectangle(0,0,100,100) + + // The Sun + change sun_green to sun_green - 1 + change sun_cy to sun_cy + 1 + change sun_radius to sun_radius + 0.2 + fill_color_rgb(sun_red, sun_green, sun_blue) + circle(50, sun_cy, sun_radius) + + // The sea + fill_color_hex("#0308ce") + rectangle(0,85,100,5) + + // The sand + fill_color_hex("#C2B280") + rectangle(0,90,100,10) +end \ No newline at end of file diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/introduction.md b/bootcamp_content/projects/golf/exercises/rolling-ball/introduction.md new file mode 100644 index 0000000000..82d82a216e --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/introduction.md @@ -0,0 +1,24 @@ +# Rolling ball + +Your task to animate the ball to move ("roll") from the tee on the left to the hole on the right. + +Some details: + +- The ball has a radius of `3`. +- It sits on the grass at a `y` of `75`. +- It starts on the tee at `28` from the left. +- It should roll until it is `88` from the left + +You'll need to use the following functions to draw things: + +- `clear()` (Remember you need to use this at the start of each iteration) +- `circle(center_x, center_y, radius)` +- `fill_color_hex(hex)` + +You'll also need to use the `set`, `change`, and `repeat` keywords. + +You can use whatever color your like for the ball, but a bright blue might be helpful as it's a little small to see! + +- `fill_color_rgb(red, green, blue)` + +If you feel stuck or overwhelmed, watch the Level 2 video back. diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki new file mode 100644 index 0000000000..6d9b141324 --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki @@ -0,0 +1,14 @@ +// TODO: Change the initial position +set x to 0 + +// TODO: Change this to only repeat +// until the ball reaches the hole +repeat 10 do + clear() + + // TODO: Set the new position + + // TODO: Set your fill + + // TODO: Draw the ball (a circle) +end \ No newline at end of file diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/task-1.md b/bootcamp_content/projects/golf/exercises/rolling-ball/task-1.md new file mode 100644 index 0000000000..3528cd11cd --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/task-1.md @@ -0,0 +1 @@ +Make the ball roll to the hole! diff --git a/bootcamp_content/projects/golf/introduction.md b/bootcamp_content/projects/golf/introduction.md new file mode 100644 index 0000000000..3de004a308 --- /dev/null +++ b/bootcamp_content/projects/golf/introduction.md @@ -0,0 +1,7 @@ +# Two Fer + +## Overview + +With a little bit of code, you can draw almost anything! + +In this project, we'll explore drawing and animation, teaching you some of the key programming concepts, and getting you familiar with how to make your own graphics that you can use in your games, applications and websites. diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json index a2c55f25ae..c73343a8ff 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json @@ -2,7 +2,7 @@ "title": "Even or Odd", "description": "Determine if a number is even or odd", "concepts": ["strings-using", "conditionals"], - "level": 2, + "level": 3, "tasks": [ { "name": "Correctly identify even numbers", diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json index aad53410b9..37d4c11e4a 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json @@ -2,7 +2,7 @@ "title": "Positive, Negative or Zero", "description": "Determine if a number is positive, negative or zero", "concepts": ["strings-using", "conditionals"], - "level": 2, + "level": 3, "tasks": [ { "name": "Correctly identify positive numbers", diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json index 5b3127e1fa..dd140efc5a 100644 --- a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json +++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json @@ -2,7 +2,7 @@ "title": "Rock Paper Scissors", "description": "Calculate the correct result", "concepts": ["conditionals"], - "level": 2, + "level": 3, "tests_type": "io", "tasks": [ { diff --git a/bootcamp_content/projects/two-fer/exercises/basic/config.json b/bootcamp_content/projects/two-fer/exercises/basic/config.json index a86dd47f75..b16ac77984 100644 --- a/bootcamp_content/projects/two-fer/exercises/basic/config.json +++ b/bootcamp_content/projects/two-fer/exercises/basic/config.json @@ -2,7 +2,7 @@ "title": "Empty Strings", "description": "Return what you say when you hand the cookie other, using the person's name if you know it.", "concepts": ["conditionals"], - "level": 2, + "level": 3, "tests_type": "io", "readonly_ranges": [ { "from": 1, "to": 1 }, diff --git a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/introduction.md b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/introduction.md index 3c4cd22abd..ff1d2fabe6 100644 --- a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/introduction.md +++ b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/introduction.md @@ -9,9 +9,9 @@ Your shapes should sit just inside the lines. You'll need to use the following functions to draw things: -- `circle(center_x, center_y, radius)` -- `rectangle(x, y, width, height)` -- `ellipse(center_x, center_y, radius_x, radius_y)` +- `circle(x, y, radius)` +- `rectangle(x, y, height, width)` +- `ellipse(x, y, radius_x, radius_y)` You can use whatever colors your like for the various components, and you can change color using either of the `fill_color` functions to change color: diff --git a/db/bootcamp_seeds.rb b/db/bootcamp_seeds.rb index 0bfe8f5d38..b82e1f6404 100644 --- a/db/bootcamp_seeds.rb +++ b/db/bootcamp_seeds.rb @@ -56,6 +56,7 @@ def exercise_config_for(project_slug, maze wordle weather + golf ] projects.each do |project_slug| From e7565517b19be826ad10871355335aecbcce2a37 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Sun, 12 Jan 2025 23:37:57 +0000 Subject: [PATCH 03/16] Improve things --- .../golf/exercises/rolling-ball/config.json | 4 +- .../golf/exercises/rolling-ball/example.jiki | 42 ++++++------------- .../golf/exercises/rolling-ball/stub.jiki | 3 ++ 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json index d98e3b6f72..f04b2f2d5d 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json @@ -38,12 +38,12 @@ "error_html": "The ball doesn't seem to start in the right place." }, { - "name": "getCircleAt(87, 75, 3)", + "name": "getCircleAt(88, 75, 3)", "matcher": "toExist", "error_html": "The ball doesn't seem to reach the hole." }, { - "name": "getCircleAt(88, 75, 3)", + "name": "getCircleAt(89, 75, 3)", "matcher": "toNotExist", "error_html": "The ball seems to go too far to the right." } diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki index a5b8c85b4f..e43184241a 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -1,35 +1,17 @@ -set sun_radius to 5 -set sun_cy to 10 +// TODO: Change the initial position +set x to 27 +set y to 75 +set radius to 3 -set sun_red to 255 -set sun_green to 237 -set sun_blue to 0 +repeat 60 do + clear() -set sky_h to 210 -set sky_s to 70 -set sky_l to 60 + // TODO: Set the new position + change x to x + 1 -repeat 100 do + // TODO: Set your fill + fill_color_hex("blue") - // The sky - //change sky_s to sky_s - 0.2 - //change sky_l to sky_l + 0.2 - change sky_h to sky_h + 0.4 - fill_color_hsl(sky_h, sky_s, sky_l) - rectangle(0,0,100,100) - - // The Sun - change sun_green to sun_green - 1 - change sun_cy to sun_cy + 1 - change sun_radius to sun_radius + 0.2 - fill_color_rgb(sun_red, sun_green, sun_blue) - circle(50, sun_cy, sun_radius) - - // The sea - fill_color_hex("#0308ce") - rectangle(0,85,100,5) - - // The sand - fill_color_hex("#C2B280") - rectangle(0,90,100,10) + // TODO: Draw the ball (a circle) + circle(x, y, 3) end \ No newline at end of file diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki index 6d9b141324..ba801aedea 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki @@ -1,4 +1,7 @@ // TODO: Change the initial position +// Remember that if you update the position at the +// *start* of the repeat block, it'll be one greater than +// whatever you set this to when the first circle is drawn. set x to 0 // TODO: Change this to only repeat From da07a2d810e8d329fbb164fccce6346907c56821 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Sun, 12 Jan 2025 23:38:42 +0000 Subject: [PATCH 04/16] Improve example --- .../projects/golf/exercises/rolling-ball/example.jiki | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki index e43184241a..d48a0bbf0d 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -1,4 +1,3 @@ -// TODO: Change the initial position set x to 27 set y to 75 set radius to 3 @@ -6,12 +5,8 @@ set radius to 3 repeat 60 do clear() - // TODO: Set the new position change x to x + 1 - // TODO: Set your fill fill_color_hex("blue") - - // TODO: Draw the ball (a circle) circle(x, y, 3) end \ No newline at end of file From a619964812a44158160836b26e9eccb6566bd008 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 00:20:38 +0000 Subject: [PATCH 05/16] Simplify next exercise selection --- app/commands/bootcamp/select_next_exercise.rb | 20 ++---- .../api/bootcamp/solutions_controller.rb | 2 +- app/models/bootcamp/exercise.rb | 2 +- .../exercises/jumbled-house/config.json | 1 + .../drawing/exercises/penguin/config.json | 1 + .../exercises/sprouting-flower/config.json | 1 + .../exercises/structured-house/config.json | 1 + .../drawing/exercises/sunset/config.json | 1 + .../golf/exercises/rolling-ball/config.json | 1 + .../maze/exercises/manual-solve/config.json | 1 + .../exercises/cloud-rain-sun/config.json | 1 + .../weather/exercises/sunshine/config.json | 1 + .../bootcamp/select_next_exercise_test.rb | 67 +++---------------- 13 files changed, 24 insertions(+), 76 deletions(-) diff --git a/app/commands/bootcamp/select_next_exercise.rb b/app/commands/bootcamp/select_next_exercise.rb index 47bab5d052..e570ff7f31 100644 --- a/app/commands/bootcamp/select_next_exercise.rb +++ b/app/commands/bootcamp/select_next_exercise.rb @@ -1,28 +1,16 @@ class Bootcamp::SelectNextExercise include Mandate - initialize_with :user, project: nil + initialize_with :user def call - return next_user_project_exercise if next_user_project_exercise - - Bootcamp::Exercise.unlocked.where.not(project: user.bootcamp_projects). + Bootcamp::Exercise.unlocked. where.not(id: completed_exercise_ids).first end - def user_project - if project - user_project = Bootcamp::UserProject.for!(user, project) - return user_project if user_project.available? - end - - user.bootcamp_user_projects.where(status: :available).first - end - + private memoize - def next_user_project_exercise = user_project&.next_exercise - def completed_exercise_ids - user.bootcamp_solutions.where.not(completed_at: nil).select(:exercise_id) + user.bootcamp_solutions.completed.select(:exercise_id) end end diff --git a/app/controllers/api/bootcamp/solutions_controller.rb b/app/controllers/api/bootcamp/solutions_controller.rb index 414727c3b9..84e62eef3e 100644 --- a/app/controllers/api/bootcamp/solutions_controller.rb +++ b/app/controllers/api/bootcamp/solutions_controller.rb @@ -4,7 +4,7 @@ class API::Bootcamp::SolutionsController < API::Bootcamp::BaseController def complete Bootcamp::Solution::Complete.(@solution) - next_exercise = Bootcamp::SelectNextExercise.(current_user, project: @solution.project) + next_exercise = Bootcamp::SelectNextExercise.(current_user) render json: { next_exercise: SerializeBootcampExercise.(next_exercise) diff --git a/app/models/bootcamp/exercise.rb b/app/models/bootcamp/exercise.rb index 3c19f7b416..af316f6c4f 100644 --- a/app/models/bootcamp/exercise.rb +++ b/app/models/bootcamp/exercise.rb @@ -10,7 +10,7 @@ class Bootcamp::Exercise < ApplicationRecord has_many :exercise_concepts, dependent: :destroy, class_name: "Bootcamp::ExerciseConcept" has_many :concepts, through: :exercise_concepts - default_scope -> { order(:idx) } + default_scope -> { order(:level_idx, :idx) } scope :unlocked, -> { where('level_idx <= ?', Bootcamp::Settings.level_idx) } def to_param = slug diff --git a/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json b/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json index 4bfed78f5b..f19a37bd43 100644 --- a/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json @@ -1,4 +1,5 @@ { + "idx": 5, "title": "Jumbled House", "description": "Unjumble the shapes into a house", "project_type": "draw", diff --git a/bootcamp_content/projects/drawing/exercises/penguin/config.json b/bootcamp_content/projects/drawing/exercises/penguin/config.json index f759cf736a..6743558a2a 100644 --- a/bootcamp_content/projects/drawing/exercises/penguin/config.json +++ b/bootcamp_content/projects/drawing/exercises/penguin/config.json @@ -1,4 +1,5 @@ { + "idx": 4, "title": "Penguin", "description": "Make the penguin symmetrical", "project_type": "draw", diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json index 816f6ad584..4759df2259 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -3,6 +3,7 @@ "description": "Make the flower sprout", "project_type": "draw", "level": 2, + "idx": 4, "concepts": [], "tests_type": "state", "interpreter_options": { diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/config.json b/bootcamp_content/projects/drawing/exercises/structured-house/config.json index f438de4089..b2119e6fca 100644 --- a/bootcamp_content/projects/drawing/exercises/structured-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/structured-house/config.json @@ -3,6 +3,7 @@ "description": "Use variables instead of hard-coded values", "project_type": "draw", "level": 2, + "idx": 1, "concepts": [], "tests_type": "state", "tasks": [ diff --git a/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json index 7f125b1732..76afda382d 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/config.json +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -3,6 +3,7 @@ "description": "Make the sun set", "project_type": "draw", "level": 2, + "idx": 3, "concepts": [], "tests_type": "state", "interpreter_options": { diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json index f04b2f2d5d..2069031952 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json @@ -3,6 +3,7 @@ "description": "Make the ball roll to the hole", "project_type": "draw", "level": 2, + "idx": 2, "concepts": [], "tests_type": "state", "interpreter_options": { diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/config.json b/bootcamp_content/projects/maze/exercises/manual-solve/config.json index e9c586d472..114eb09aad 100644 --- a/bootcamp_content/projects/maze/exercises/manual-solve/config.json +++ b/bootcamp_content/projects/maze/exercises/manual-solve/config.json @@ -1,4 +1,5 @@ { + "idx": 1, "title": "Manually solve a maze", "description": "Solve a maze using some basic functions", "project_type": "maze", diff --git a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json index 984daec162..6dbae045ab 100644 --- a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json +++ b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json @@ -1,4 +1,5 @@ { + "idx": 6, "title": "Clouds, Rain, and Sun", "description": "Make a compound image for clouds, rain and sun", "project_type": "draw", diff --git a/bootcamp_content/projects/weather/exercises/sunshine/config.json b/bootcamp_content/projects/weather/exercises/sunshine/config.json index dda3eed3cf..fd7462a152 100644 --- a/bootcamp_content/projects/weather/exercises/sunshine/config.json +++ b/bootcamp_content/projects/weather/exercises/sunshine/config.json @@ -1,4 +1,5 @@ { + "idx": 2, "title": "Sunshine", "description": "Add the sun to its spikes", "project_type": "draw", diff --git a/test/commands/bootcamp/select_next_exercise_test.rb b/test/commands/bootcamp/select_next_exercise_test.rb index 477c13ce73..aef1e17836 100644 --- a/test/commands/bootcamp/select_next_exercise_test.rb +++ b/test/commands/bootcamp/select_next_exercise_test.rb @@ -15,11 +15,16 @@ def setup assert_equal exercise, actual end - test "returns exercise with lowest idx" do + test "returns exercise with lowest level and idx pair" do + (1..3).each { |idx| create :bootcamp_level, idx: } user = create :user, :with_bootcamp_data - create :bootcamp_exercise, idx: 2 - exercise = create :bootcamp_exercise, idx: 1 - create :bootcamp_exercise, idx: 3 + create :bootcamp_exercise, level_idx: 2, idx: 2 + create :bootcamp_exercise, level_idx: 2, idx: 1 + create :bootcamp_exercise, level_idx: 1, idx: 2 + exercise = create :bootcamp_exercise, level_idx: 1, idx: 1 + create :bootcamp_exercise, level_idx: 1, idx: 3 + create :bootcamp_exercise, level_idx: 3, idx: 2 + create :bootcamp_exercise, level_idx: 3, idx: 1 actual = Bootcamp::SelectNextExercise.(user) assert_equal exercise, actual @@ -36,58 +41,4 @@ def setup actual = Bootcamp::SelectNextExercise.(user) assert_equal exercise, actual end - - test "doesn't return completed exercise for user_project" do - user = create :user, :with_bootcamp_data - project = create :bootcamp_project - solved_exercise = create(:bootcamp_exercise, idx: 1, project:) - exercise = create(:bootcamp_exercise, idx: 2, project:) - create :bootcamp_user_project, user:, project:, status: :available - - create :bootcamp_solution, :completed, user:, exercise: solved_exercise - - actual = Bootcamp::SelectNextExercise.(user) - assert_equal exercise, actual - end - - test "prefers exercise from existing user project" do - user = create :user, :with_bootcamp_data - create :bootcamp_exercise, idx: 1 - exercise = create :bootcamp_exercise, idx: 2 - create :bootcamp_user_project, user:, project: exercise.project, status: :available - - actual = Bootcamp::SelectNextExercise.(user) - assert_equal exercise, actual - end - - test "doesn't take exercise from locked user project" do - user = create :user, :with_bootcamp_data - locked = create :bootcamp_exercise, idx: 1 - exercise = create :bootcamp_exercise, idx: 2 - create :bootcamp_user_project, user:, project: locked.project, status: :locked - - actual = Bootcamp::SelectNextExercise.(user) - assert_equal exercise, actual - end - - test "honours passed in project" do - user = create :user, :with_bootcamp_data - other = create :bootcamp_exercise, idx: 1 - exercise = create :bootcamp_exercise, idx: 2 - create :bootcamp_user_project, user:, project: other.project, status: :available - create :bootcamp_user_project, user:, project: exercise.project, status: :available - - actual = Bootcamp::SelectNextExercise.(user, project: exercise.project) - assert_equal exercise, actual - end - - test "copes with locked project passed in project" do - user = create :user, :with_bootcamp_data - locked = create :bootcamp_exercise, idx: 1 - exercise = create :bootcamp_exercise, idx: 2 - create :bootcamp_user_project, user:, project: locked.project, status: :locked - - actual = Bootcamp::SelectNextExercise.(user, project: locked.project) - assert_equal exercise, actual - end end From b8c57c10a6c4efb557462d28cba717caeea5366c Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 00:24:08 +0000 Subject: [PATCH 06/16] Use 2DP and fix multiplication --- app/javascript/interpreter/executor.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index 40991e0c90..32b93e643b 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -639,7 +639,7 @@ export class Executor this.verifyNumberOperands(expression.operator, left.value, right.value) const minusValue = left.value - right.value - const minusValue2DP = Math.round(minusValue * 10) / 10 + const minusValue2DP = Math.round(minusValue * 100) / 100 return { ...result, @@ -648,12 +648,12 @@ export class Executor //> binary-plus case 'PLUS': if (isNumber(left.value) && isNumber(right.value)) { - const value = left.value + right.value - const value2DP = Math.round(value * 10) / 10 + const plusValue = left.value + right.value + const plusValue2DP = Math.round(plusValue * 100) / 100 return { ...result, - value: value2DP, + value: plusValue2DP, } } if (isString(left.value) && isString(right.value)) @@ -674,7 +674,7 @@ export class Executor case 'SLASH': this.verifyNumberOperands(expression.operator, left.value, right.value) const slashValue = left.value / right.value - const slashValue2DP = Math.round(slashValue * 10) / 10 + const slashValue2DP = Math.round(slashValue * 100) / 100 return { ...result, value: slashValue2DP, @@ -682,8 +682,8 @@ export class Executor case 'STAR': this.verifyNumberOperands(expression.operator, left.value, right.value) - const starValue = left.value / right.value - const starValue2DP = Math.round(starValue * 10) / 10 + const starValue = left.value * right.value + const starValue2DP = Math.round(starValue * 100) / 100 return { ...result, value: starValue2DP, From d38fea8abafe969b9a033936004ae9b21af1c0f7 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 14:19:30 +0000 Subject: [PATCH 07/16] WIP --- .../bootcamp/exercise/available_for_user.rb | 29 +++++ app/commands/bootcamp/solution/create.rb | 2 +- app/models/bootcamp/user_project.rb | 14 --- bootcamp_content/projects/drawing/config.json | 1 + .../drawing/exercises/rainbow/config.json | 110 ++++++++++++++++++ .../drawing/exercises/rainbow/example.jiki | 43 +++++++ .../drawing/exercises/rainbow/introduction.md | 23 ++++ .../drawing/exercises/rainbow/stub.jiki | 58 +++++++++ .../drawing/exercises/rainbow/task-1.md | 5 + .../exercises/automated-solve/config.json | 3 +- .../maze/exercises/manual-solve/config.json | 2 +- .../exercises/even-or-odd/config.json | 1 + .../positive-negative-or-zero/config.json | 1 + .../exercises/basic/config.json | 1 + .../two-fer/exercises/basic/config.json | 1 + .../exercises/process-guess/config.json | 1 + db/bootcamp_seeds.rb | 6 +- .../exercise/available_for_user_test.rb | 23 ++++ 18 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 app/commands/bootcamp/exercise/available_for_user.rb create mode 100644 bootcamp_content/projects/drawing/exercises/rainbow/config.json create mode 100644 bootcamp_content/projects/drawing/exercises/rainbow/example.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/rainbow/introduction.md create mode 100644 bootcamp_content/projects/drawing/exercises/rainbow/stub.jiki create mode 100644 bootcamp_content/projects/drawing/exercises/rainbow/task-1.md create mode 100644 test/commands/bootcamp/exercise/available_for_user_test.rb diff --git a/app/commands/bootcamp/exercise/available_for_user.rb b/app/commands/bootcamp/exercise/available_for_user.rb new file mode 100644 index 0000000000..9c8c6ff77c --- /dev/null +++ b/app/commands/bootcamp/exercise/available_for_user.rb @@ -0,0 +1,29 @@ +class Bootcamp::Exercise::AvailableForUser + include Mandate + + initialize_with :exercise, :user + + def call + # If the exercise is gloabally locked, it's locked + return false if exercise.locked? + + # The first exercise is always available + return true if exercise.idx == 1 + + # Otherwise the previous solution must be completed + previous_exercises_completed? + end + + private + delegate :project, to: :exercise + + def previous_exercises_completed? + previous_exercises = project.exercises.to_a.select do |prev_ex| + prev_ex.level <= exercise.level || + (prev_ex.level == exercise.level && prev_ex.idx < exercise.idx) + end + + completed_exercise_ids = user.bootcamp_solutions.completed.where(exercise_id: exercises.map(&:id)).pluck(:exercise_id) + previous_exercises.all? { |ex| completed_exercise_ids.include?(ex.id) } + end +end diff --git a/app/commands/bootcamp/solution/create.rb b/app/commands/bootcamp/solution/create.rb index ee2a5b1018..f59a3bc223 100644 --- a/app/commands/bootcamp/solution/create.rb +++ b/app/commands/bootcamp/solution/create.rb @@ -22,7 +22,7 @@ def call private def guard! - raise ExerciseLockedError unless user_project.exercise_available?(exercise) + raise ExerciseLockedError unless Bootcamp::Exercise::AvailableForUser.(exercise, user) end def code diff --git a/app/models/bootcamp/user_project.rb b/app/models/bootcamp/user_project.rb index a8772b0c8c..89d4dac679 100644 --- a/app/models/bootcamp/user_project.rb +++ b/app/models/bootcamp/user_project.rb @@ -39,20 +39,6 @@ def next_exercise project.exercises.reject(&:locked?).reject { |e| completed_exercise_ids.include?(e.id) }.first end - def exercise_available?(exercise) - # If the project's locked, all the exercises are locked - return false if locked? - - # If the exercise is gloabally locked, it's locked - return false if exercise.locked? - - # The first exercise is always available - return true if exercise.idx == 1 - - # Otherwise the previous solution must be completed - solutions.find { |s| s.exercise.idx == exercise.idx - 1 }&.completed? - end - def exercise_status(exercise, solution) solution ||= solutions.find { |s| s.exercise_id == exercise.id } diff --git a/bootcamp_content/projects/drawing/config.json b/bootcamp_content/projects/drawing/config.json index 67bfba5efa..1914c8057c 100644 --- a/bootcamp_content/projects/drawing/config.json +++ b/bootcamp_content/projects/drawing/config.json @@ -6,6 +6,7 @@ "penguin", "jumbled-house", "structured-house", + "rainbow", "sunset", "sprouting-flower" ] diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/config.json b/bootcamp_content/projects/drawing/exercises/rainbow/config.json new file mode 100644 index 0000000000..c5d4fd71f6 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/config.json @@ -0,0 +1,110 @@ +{ + "title": "Rainbow", + "description": "Make the penguin symmetrical", + "project_type": "draw", + "level": 2, + "idx": 3, + "concepts": [], + "tests_type": "state", + "readonly_ranges": [], + "tasks": [ + { + "name": "Draw the scene", + "tests": [ + { + "slug": "draw-scence", + "name": "Make the penguin symmetrical.", + "description_html": "Fix all the TODO comments to make the penguin symmetrical.", + "function": "main", + "checks": [ + { + "name": "getRectangleAt(0, 0, 100, 100)", + "matcher": "toExist", + "error_html": "The sky has gone wrong." + }, + { + "name": "getRectangleAt(0, 70, 100, 30)", + "matcher": "toExist", + "error_html": "The ground has gone wrong." + }, + { + "name": "getEllipseAt(28, 55, 10, 25)", + "matcher": "toExist", + "error_html": "The left wing doesn't seem right." + }, + { + "name": "getEllipseAt(72, 55, 10, 25)", + "matcher": "toExist", + "error_html": "The right wing doesn't seem right." + }, + { + "name": "getEllipseAt(50, 53, 25, 40)", + "matcher": "toExist", + "error_html": "The outer body has gone wrong." + }, + { + "name": "getEllipseAt(50, 50, 21, 39)", + "matcher": "toExist", + "error_html": "The inner body has gone wrong." + }, + { + "name": "getCircleAt(50, 31, 23)", + "matcher": "toExist", + "error_html": "The head has gone wrong." + }, + { + "name": "getEllipseAt(41, 32, 11, 14)", + "matcher": "toExist", + "error_html": "The left side of the face doesn't look right." + }, + { + "name": "getEllipseAt(59, 32, 11, 14)", + "matcher": "toExist", + "error_html": "The right side of the face doesn't look right." + }, + { + "name": "getEllipseAt(50, 40, 16, 11)", + "matcher": "toExist", + "error_html": "The lower part of the face doesn't look right." + }, + { + "name": "getCircleAt(42, 33, 3)", + "matcher": "toExist", + "error_html": "The left eye seems off." + }, + { + "name": "getCircleAt(43, 34, 1)", + "matcher": "toExist", + "error_html": "The left iris seems off." + }, + { + "name": "getCircleAt(58, 33, 3)", + "matcher": "toExist", + "error_html": "The right eye seems off." + }, + { + "name": "getCircleAt(57, 34, 1)", + "matcher": "toExist", + "error_html": "The right iris seems off." + }, + { + "name": "getEllipseAt(40, 93, 7, 4)", + "matcher": "toExist", + "error_html": "The left foot's gone astray." + }, + { + "name": "getEllipseAt(60, 93, 7, 4)", + "matcher": "toExist", + "error_html": "The right foot's not right." + }, + { + "name": "getTriangleAt(46, 38, 54, 38, 50, 47)", + "matcher": "toExist", + "error_html": "The nose isn't right." + } + ] + } + ] + } + ] +} diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki b/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki new file mode 100644 index 0000000000..9d57b7b4bd --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki @@ -0,0 +1,43 @@ +// Light blue background +fill_color_hex("#ADD8E6") +rectangle(0, 0, 100, 100) + +// Ground +fill_color_hex("#ffffff") // Ice ground +rectangle(0, 70, 100, 30) // Ice ground + +// Penguin wings +fill_color_hex("#000000") // Black +ellipse(28, 55, 10, 25) // Left wing +ellipse(72, 55, 10, 25) // Right wing + +// Penguin body +fill_color_hex("#000000") // Black for the body +ellipse(50, 53, 25, 40) // Outer body (oval shape) +fill_color_hex("#ffffff") // White for the belly +ellipse(50, 50, 21, 39) // Inner belly (oval shape) + +// Penguin head +fill_color_hex("#000000") // Black +circle(50, 31, 23) // Head (circle) +fill_color_hex("#ffffff") // White for the face +ellipse(41, 32, 11, 14) // Left part of the face +ellipse(59, 32, 11, 14) // Right part of the face +ellipse(50, 40, 16, 11) // Lower part of the face + +// Penguin eyes +fill_color_hex("#000000") // Black +circle(42, 33, 3) // Left eye +fill_color_hex("#ffffff") // White +circle(43, 34, 1) // Left iris + +fill_color_hex("#000000") // Black +circle(58, 33, 3) // Right eye +fill_color_hex("#ffffff") // White +circle(57, 34, 1) // Right iris + +// Feet +fill_color_hex("#FFA500") +ellipse(40, 93, 7, 4) +ellipse(60, 93, 7, 4) +triangle(46, 38, 54, 38, 50, 47) \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md b/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md new file mode 100644 index 0000000000..da3c5f2632 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md @@ -0,0 +1,23 @@ +# The Penguin + +Your task is to make the penguin symmetrical. + +It should look like this: + + + +We've drawn the left hand side for you, and added `TODO` comments for each of the things you need to do. + +You'll need to think about setting the right colors before drawing things. + +For the nose, you should **change** the middle coordinates of the triangle. Don't add a new triangle. + +The functions used in this exercise are: + +- `circle(center_x, center_y, radius)` +- `rectangle(x, y, width, height)` +- `ellipse(center_x, center_y, radius_x, radius_y)` +- `triangle(x1,y1, x2,y2, x3,y3)` +- `fill_color_hex(hex)` + +If you need help remembering how to use any of these functions, you can watch back the video from week 1. diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/stub.jiki b/bootcamp_content/projects/drawing/exercises/rainbow/stub.jiki new file mode 100644 index 0000000000..aec40e98c7 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/stub.jiki @@ -0,0 +1,58 @@ +// Sky +fill_color_hex("#ADD8E6") +rectangle(0, 0, 100, 100) + +// Ground +fill_color_hex("#ffffff") +rectangle(0, 70, 100, 30) + +// Left Wing +fill_color_hex("#000000") +ellipse(28, 55, 10, 25) + +// +// TODO: Add the Right wing +// + +// Body +fill_color_hex("#000000") +ellipse(50, 53, 25, 40) +fill_color_hex("#ffffff") +ellipse(50, 50, 21, 39) + +// Head +fill_color_hex("#000000") +circle(50, 31, 23) + +// Left side of face +fill_color_hex("#ffffff") +ellipse(41, 32, 11, 14) + +// +// TODO: Add the right part of the face +// + +// Lower part of face +ellipse(50, 40, 16, 11) // Lower part of the face + +// Left eye +fill_color_hex("#000000") +circle(42, 33, 3) +fill_color_hex("#ffffff") +circle(43, 34, 1) + +// +// TODO: Add the right eye +// + +// Nose +fill_color_hex("#FFA500") +triangle(46, 38, 50, 38, 50, 47) // TODO: Change the nose to be symmetrical. + +// Left Foot +fill_color_hex("#FFA500") +ellipse(40, 93, 7, 4) + +// +// TODO: Add the right foot +// diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md b/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md new file mode 100644 index 0000000000..c9fc02faac --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md @@ -0,0 +1,5 @@ +Follow the `TODO` comments to make the penguin symmetrical. + +As you get each bit right, tidy up the comments by removing the `TODO` and replacing it with a nicer comment. + +Use the "Check Scenarios" button regularly, and have fun! diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/config.json b/bootcamp_content/projects/maze/exercises/automated-solve/config.json index 07435d2f26..2574b12b85 100644 --- a/bootcamp_content/projects/maze/exercises/automated-solve/config.json +++ b/bootcamp_content/projects/maze/exercises/automated-solve/config.json @@ -1,7 +1,8 @@ { "title": "Programatically solve a maze", "description": "Programatically solve a maze", - "level": 2, + "level": 3, + "idx": 1, "concepts": ["Conditionals", "loops-repeat"], "project_type": "maze", "tests_type": "state", diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/config.json b/bootcamp_content/projects/maze/exercises/manual-solve/config.json index 114eb09aad..a5c58c8724 100644 --- a/bootcamp_content/projects/maze/exercises/manual-solve/config.json +++ b/bootcamp_content/projects/maze/exercises/manual-solve/config.json @@ -1,9 +1,9 @@ { - "idx": 1, "title": "Manually solve a maze", "description": "Solve a maze using some basic functions", "project_type": "maze", "level": 1, + "idx": 1, "available_functions": ["move", "turnLeft", "turnRight"], "tests_type": "state", "concepts": ["functions-using"], diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json index c73343a8ff..2cb93d8bc9 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json @@ -3,6 +3,7 @@ "description": "Determine if a number is even or odd", "concepts": ["strings-using", "conditionals"], "level": 3, + "idx": 1, "tasks": [ { "name": "Correctly identify even numbers", diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json index 37d4c11e4a..2a4cce8ef9 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json @@ -3,6 +3,7 @@ "description": "Determine if a number is positive, negative or zero", "concepts": ["strings-using", "conditionals"], "level": 3, + "idx": 1, "tasks": [ { "name": "Correctly identify positive numbers", diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json index dd140efc5a..a4fea860af 100644 --- a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json +++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json @@ -3,6 +3,7 @@ "description": "Calculate the correct result", "concepts": ["conditionals"], "level": 3, + "idx": 1, "tests_type": "io", "tasks": [ { diff --git a/bootcamp_content/projects/two-fer/exercises/basic/config.json b/bootcamp_content/projects/two-fer/exercises/basic/config.json index b16ac77984..aed6b3a61c 100644 --- a/bootcamp_content/projects/two-fer/exercises/basic/config.json +++ b/bootcamp_content/projects/two-fer/exercises/basic/config.json @@ -3,6 +3,7 @@ "description": "Return what you say when you hand the cookie other, using the person's name if you know it.", "concepts": ["conditionals"], "level": 3, + "idx": 1, "tests_type": "io", "readonly_ranges": [ { "from": 1, "to": 1 }, diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/config.json b/bootcamp_content/projects/wordle/exercises/process-guess/config.json index 850421801f..b33fe3138d 100644 --- a/bootcamp_content/projects/wordle/exercises/process-guess/config.json +++ b/bootcamp_content/projects/wordle/exercises/process-guess/config.json @@ -5,6 +5,7 @@ "available_functions": [], "concepts": ["conditionals", "arrays"], "level": 3, + "idx": 1, "tests_type": "state", "tasks": [ { diff --git a/db/bootcamp_seeds.rb b/db/bootcamp_seeds.rb index b82e1f6404..714a75ee1d 100644 --- a/db/bootcamp_seeds.rb +++ b/db/bootcamp_seeds.rb @@ -71,16 +71,16 @@ def exercise_config_for(project_slug, description: project_config[:description], introduction_markdown: project_intro_for(project_slug) ) - project_config[:exercises].each.with_index do |exercise_slug, idx| + project_config[:exercises].each do |exercise_slug| exercise_config = exercise_config_for(project_slug, exercise_slug) exercise = project.exercises.find_or_create_by!(slug: exercise_slug) do |e| - e.idx = idx + 1 + e.idx = exercise_config[:idx] e.title = "" e.description = "" e.level_idx = exercise_config[:level] end exercise.update!( - idx: idx + 1, + idx: exercise_config[:idx], title: exercise_config[:title], description: exercise_config[:description], level_idx: exercise_config[:level], diff --git a/test/commands/bootcamp/exercise/available_for_user_test.rb b/test/commands/bootcamp/exercise/available_for_user_test.rb new file mode 100644 index 0000000000..e53c4a2dce --- /dev/null +++ b/test/commands/bootcamp/exercise/available_for_user_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class Bootcamp::Exercise::AvailableForUserTest < ActiveSupport::TestCase + test "return true if first exercise" do + create :bootcamp_level, idx: 1 + exercise = create :bootcamp_exercise, level_idx: 1 + user = create :user + + Bootcamp::Settings.instance.update(level_idx: 1) + + assert Bootcamp::Exercise::AvailableForUser.(exercise, user) + end + + test "return false if level not reached" do + (1..2).each { |idx| create :bootcamp_level, idx: } + exercise = create :bootcamp_exercise, level_idx: 2 + user = create :user + + Bootcamp::Settings.instance.update(level_idx: 1) + + refute Bootcamp::Exercise::AvailableForUser.(exercise, user) + end +end From 86dcb293908c256b572c18e6a207ea4c2340ad61 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 14:58:08 +0000 Subject: [PATCH 08/16] Improve unlocking logic --- .../bootcamp/exercise/available_for_user.rb | 12 ++- app/models/bootcamp/user_project.rb | 2 +- .../exercise/available_for_user_test.rb | 76 +++++++++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/app/commands/bootcamp/exercise/available_for_user.rb b/app/commands/bootcamp/exercise/available_for_user.rb index 9c8c6ff77c..3c40cb01c7 100644 --- a/app/commands/bootcamp/exercise/available_for_user.rb +++ b/app/commands/bootcamp/exercise/available_for_user.rb @@ -7,9 +7,6 @@ def call # If the exercise is gloabally locked, it's locked return false if exercise.locked? - # The first exercise is always available - return true if exercise.idx == 1 - # Otherwise the previous solution must be completed previous_exercises_completed? end @@ -18,12 +15,13 @@ def call delegate :project, to: :exercise def previous_exercises_completed? - previous_exercises = project.exercises.to_a.select do |prev_ex| - prev_ex.level <= exercise.level || - (prev_ex.level == exercise.level && prev_ex.idx < exercise.idx) + previous_exercises = project.exercises.where.not(id: exercise.id).select do |prev_ex| + prev_ex.level_idx <= exercise.level_idx || + (prev_ex.level_idx == exercise.level_idx && prev_ex.idx < exercise.idx) end - completed_exercise_ids = user.bootcamp_solutions.completed.where(exercise_id: exercises.map(&:id)).pluck(:exercise_id) + completed_exercise_ids = user.bootcamp_solutions.completed.where(exercise_id: previous_exercises.map(&:id)).pluck(:exercise_id) + previous_exercises.all? { |ex| completed_exercise_ids.include?(ex.id) } end end diff --git a/app/models/bootcamp/user_project.rb b/app/models/bootcamp/user_project.rb index 89d4dac679..f20ae8aa6b 100644 --- a/app/models/bootcamp/user_project.rb +++ b/app/models/bootcamp/user_project.rb @@ -44,7 +44,7 @@ def exercise_status(exercise, solution) if solution solution.status - elsif exercise_available?(exercise) + elsif Bootcamp::Exercise::AvailableForUser.(exercise, user) :available else :locked diff --git a/test/commands/bootcamp/exercise/available_for_user_test.rb b/test/commands/bootcamp/exercise/available_for_user_test.rb index e53c4a2dce..221a3f114e 100644 --- a/test/commands/bootcamp/exercise/available_for_user_test.rb +++ b/test/commands/bootcamp/exercise/available_for_user_test.rb @@ -20,4 +20,80 @@ class Bootcamp::Exercise::AvailableForUserTest < ActiveSupport::TestCase refute Bootcamp::Exercise::AvailableForUser.(exercise, user) end + + test "return false if previous exercise not started" do + create :bootcamp_level, idx: 1 + project = create :bootcamp_project + create(:bootcamp_exercise, level_idx: 1, idx: 1, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + user = create :user + + Bootcamp::Settings.instance.update(level_idx: 1) + + refute Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end + + test "return false if previous exercise not completed" do + create :bootcamp_level, idx: 1 + project = create :bootcamp_project + first_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 1, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + user = create :user + create(:bootcamp_solution, exercise: first_exercise, user:) + + Bootcamp::Settings.instance.update(level_idx: 1) + + refute Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end + + test "return true if previous exercise completed" do + create :bootcamp_level, idx: 1 + project = create :bootcamp_project + first_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 1, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + user = create :user + create(:bootcamp_solution, :completed, exercise: first_exercise, user:) + + Bootcamp::Settings.instance.update(level_idx: 1) + + assert Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end + + test "return false if previous level exercise not started" do + (1..2).each { |idx| create :bootcamp_level, idx: } + project = create :bootcamp_project + create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 2, idx: 1, project:) + user = create :user + + Bootcamp::Settings.instance.update(level_idx: 2) + + refute Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end + + test "return false if previous level exercise not completed" do + (1..2).each { |idx| create :bootcamp_level, idx: } + project = create :bootcamp_project + first_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 2, idx: 1, project:) + user = create :user + create(:bootcamp_solution, exercise: first_exercise, user:) + + Bootcamp::Settings.instance.update(level_idx: 2) + + refute Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end + + test "return true if previous level exercise completed" do + (1..2).each { |idx| create :bootcamp_level, idx: } + project = create :bootcamp_project + first_exercise = create(:bootcamp_exercise, level_idx: 1, idx: 2, project:) + second_exercise = create(:bootcamp_exercise, level_idx: 2, idx: 1, project:) + user = create :user + create(:bootcamp_solution, :completed, exercise: first_exercise, user:) + + Bootcamp::Settings.instance.update(level_idx: 2) + + assert Bootcamp::Exercise::AvailableForUser.(second_exercise, user) + end end From cf3452d0b889eba42e53ad97d7ffb2fa0b42e328 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 15:12:14 +0000 Subject: [PATCH 09/16] Fix tests --- test/commands/bootcamp/solution/create_test.rb | 2 +- test/controllers/api/bootcamp/solutions_controller_test.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/commands/bootcamp/solution/create_test.rb b/test/commands/bootcamp/solution/create_test.rb index d30c5a774b..118d10e394 100644 --- a/test/commands/bootcamp/solution/create_test.rb +++ b/test/commands/bootcamp/solution/create_test.rb @@ -58,7 +58,7 @@ class Bootcamp::Solution::CreateTest < ActiveSupport::TestCase create(:bootcamp_user_project, user:, project:) exercise = create(:bootcamp_exercise, project:) - Bootcamp::UserProject.any_instance.expects(:exercise_available?).with(exercise).returns(false) + Bootcamp::Exercise::AvailableForUser.expects(:call).with(exercise, user).returns(false) assert_raises ExerciseLockedError do Bootcamp::Solution::Create.(user, exercise) diff --git a/test/controllers/api/bootcamp/solutions_controller_test.rb b/test/controllers/api/bootcamp/solutions_controller_test.rb index 2b389ed17b..31ddb30e68 100644 --- a/test/controllers/api/bootcamp/solutions_controller_test.rb +++ b/test/controllers/api/bootcamp/solutions_controller_test.rb @@ -7,8 +7,6 @@ class API::Bootcamp::SolutionsControllerTest < API::BaseTestCase solution = create(:bootcamp_solution, user:) create :bootcamp_user_project, user:, project: solution.project - Bootcamp::Solution::Complete.expects(:call).with(solution) - setup_user(user) patch complete_api_bootcamp_solution_url(solution), headers: @headers From 18ee8de6a617448fcd8c15b493897859dae560a7 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 15:15:15 +0000 Subject: [PATCH 10/16] Update idx --- .../projects/drawing/exercises/sprouting-flower/config.json | 2 +- bootcamp_content/projects/drawing/exercises/sunset/config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json index 4759df2259..d80453058b 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -3,7 +3,7 @@ "description": "Make the flower sprout", "project_type": "draw", "level": 2, - "idx": 4, + "idx": 5, "concepts": [], "tests_type": "state", "interpreter_options": { diff --git a/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json index 76afda382d..bb7f09646e 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/config.json +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -3,7 +3,7 @@ "description": "Make the sun set", "project_type": "draw", "level": 2, - "idx": 3, + "idx": 4, "concepts": [], "tests_type": "state", "interpreter_options": { From 93af545ba480804e79d16e7e80b79924c8853a26 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 16:44:05 +0000 Subject: [PATCH 11/16] WIP --- .../bootcamp/exercise/available_for_user.rb | 2 +- app/commands/bootcamp/solution/create.rb | 6 ++ .../generateExpects.ts | 1 + app/javascript/interpreter/executor.ts | 4 +- .../languages/jikiscript/parser.ts | 2 +- .../languages/jikiscript/scanner.ts | 1 + .../interpreter/languages/jikiscript/token.ts | 1 + .../interpreter/locales/en/translation.json | 1 + app/views/bootcamp/projects/show.html.haml | 2 +- .../bootcamp/exercise_widget.html.haml | 1 - .../drawing/exercises/rainbow/config.json | 9 ++- .../drawing/exercises/rainbow/example.jiki | 49 ++----------- .../drawing/exercises/rainbow/introduction.md | 29 +++++--- .../drawing/exercises/rainbow/task-1.md | 6 +- .../exercises/sprouting-flower/config.json | 54 +++++++++++++- .../exercises/sprouting-flower/example.jiki | 8 +- .../sprouting-flower/introduction.md | 73 ++++++++++++++++++- .../exercises/sprouting-flower/stub.jiki | 23 +++--- .../exercises/sprouting-flower/task-1.md | 2 +- .../drawing/exercises/sunset/introduction.md | 8 +- .../drawing/exercises/sunset/stub.jiki | 4 +- .../golf/exercises/rolling-ball/example.jiki | 5 +- .../golf/exercises/rolling-ball/stub.jiki | 12 +-- 23 files changed, 201 insertions(+), 102 deletions(-) diff --git a/app/commands/bootcamp/exercise/available_for_user.rb b/app/commands/bootcamp/exercise/available_for_user.rb index 3c40cb01c7..282bb3be7a 100644 --- a/app/commands/bootcamp/exercise/available_for_user.rb +++ b/app/commands/bootcamp/exercise/available_for_user.rb @@ -16,7 +16,7 @@ def call def previous_exercises_completed? previous_exercises = project.exercises.where.not(id: exercise.id).select do |prev_ex| - prev_ex.level_idx <= exercise.level_idx || + prev_ex.level_idx < exercise.level_idx || (prev_ex.level_idx == exercise.level_idx && prev_ex.idx < exercise.idx) end diff --git a/app/commands/bootcamp/solution/create.rb b/app/commands/bootcamp/solution/create.rb index f59a3bc223..ec0dbfbfce 100644 --- a/app/commands/bootcamp/solution/create.rb +++ b/app/commands/bootcamp/solution/create.rb @@ -4,6 +4,8 @@ class Bootcamp::Solution::Create initialize_with :user, :exercise def call + return existing_solution if existing_solution + guard! begin @@ -21,6 +23,10 @@ def call end private + def existing_solution + Bootcamp::Solution.find_by(user:, exercise:) + end + def guard! raise ExerciseLockedError unless Bootcamp::Exercise::AvailableForUser.(exercise, user) end diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts index 1b1712e494..8a4d8a32c2 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts @@ -66,6 +66,7 @@ function generateExpectsForStateTests( : argsString.split(',').map((arg) => safe_eval(arg.trim())) // And then we get the function and call it. + console.log(fnName) const fn = exercise[fnName] actual = fn.bind(exercise).call(exercise, interpreterResult, ...args) } diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index 32b93e643b..ea7284c4c5 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -275,8 +275,8 @@ export class Executor count-- // Delay repeat for things like animations - if (this.languageFeatures.repeatDelay) { - this.time += this.languageFeatures.repeatDelay + if (this.languageFeatures?.repeatDelay) { + this.time += this.languageFeatures?.repeatDelay || 0 } } } diff --git a/app/javascript/interpreter/languages/jikiscript/parser.ts b/app/javascript/interpreter/languages/jikiscript/parser.ts index 7692c57bd9..2e6b14ed46 100644 --- a/app/javascript/interpreter/languages/jikiscript/parser.ts +++ b/app/javascript/interpreter/languages/jikiscript/parser.ts @@ -330,7 +330,7 @@ export class Parser implements GenericParser { private repeatStatement(): Statement { const begin = this.previous() const condition = this.expression() - + this.consume('TIMES', 'MissingTimesInRepeat') this.consume('DO', 'MissingDoToStartBlock', { type: 'repeat' }) this.consumeEndOfLine() diff --git a/app/javascript/interpreter/languages/jikiscript/scanner.ts b/app/javascript/interpreter/languages/jikiscript/scanner.ts index 6a92ec4855..99cf9044f9 100644 --- a/app/javascript/interpreter/languages/jikiscript/scanner.ts +++ b/app/javascript/interpreter/languages/jikiscript/scanner.ts @@ -57,6 +57,7 @@ export class Scanner { set: 'SET', to: 'TO', true: 'TRUE', + times: 'TIMES', while: 'WHILE', with: 'WITH', } diff --git a/app/javascript/interpreter/languages/jikiscript/token.ts b/app/javascript/interpreter/languages/jikiscript/token.ts index 66d50ae1c0..d61f3b58f7 100644 --- a/app/javascript/interpreter/languages/jikiscript/token.ts +++ b/app/javascript/interpreter/languages/jikiscript/token.ts @@ -52,6 +52,7 @@ export type TokenType = | 'RETURN' | 'SET' | 'TO' + | 'TIMES' | 'TRUE' | 'WHILE' | 'WITH' diff --git a/app/javascript/interpreter/locales/en/translation.json b/app/javascript/interpreter/locales/en/translation.json index 738c76941e..355304bc49 100644 --- a/app/javascript/interpreter/locales/en/translation.json +++ b/app/javascript/interpreter/locales/en/translation.json @@ -44,6 +44,7 @@ "MissingWithBeforeParameters": "Did you forget the `with` keyword before your parameters?", "MissingStringAsKey": "Expect string as key.", "MissingVariableName": "Expect variable name.", + "MissingTimesInRepeat": "Did you forget to write `times` after the number of times you want to repeat?", "DuplicateParameterName": "Did you accidently use the name `{{parameter}}` twice in your function parameters.", "MissingWhileBeforeDoWhileCondition": "Expected 'while' to start 'while' condition", "NumberContainsAlpha": "A number cannot contain letters. Did you mean `{{suggestion}}`?", diff --git a/app/views/bootcamp/projects/show.html.haml b/app/views/bootcamp/projects/show.html.haml index 24f0e4ce6c..d37a2ffe04 100644 --- a/app/views/bootcamp/projects/show.html.haml +++ b/app/views/bootcamp/projects/show.html.haml @@ -24,7 +24,7 @@ = link_to concept.title, concept, class: 'bubble' .lg-container - .flex.gap-48 + .flex.gap-48.pb-40 .lhs .introduction.c-prose = raw @project.introduction_html diff --git a/app/views/components/bootcamp/exercise_widget.html.haml b/app/views/components/bootcamp/exercise_widget.html.haml index cd5800ad2a..e0133163ca 100644 --- a/app/views/components/bootcamp/exercise_widget.html.haml +++ b/app/views/components/bootcamp/exercise_widget.html.haml @@ -8,6 +8,5 @@ .project-title= project.title .tag{ class: status.to_s } .exercise-title - #{exercise.idx}. = exercise.title .description= exercise.description diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/config.json b/bootcamp_content/projects/drawing/exercises/rainbow/config.json index c5d4fd71f6..cd90f589a3 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/config.json +++ b/bootcamp_content/projects/drawing/exercises/rainbow/config.json @@ -1,20 +1,23 @@ { "title": "Rainbow", - "description": "Make the penguin symmetrical", + "description": "Draw a rainbow pattern", "project_type": "draw", "level": 2, "idx": 3, "concepts": [], "tests_type": "state", "readonly_ranges": [], + "interpreter_options": { + "repeat_delay": 10 + }, "tasks": [ { "name": "Draw the scene", "tests": [ { "slug": "draw-scence", - "name": "Make the penguin symmetrical.", - "description_html": "Fix all the TODO comments to make the penguin symmetrical.", + "name": "The the rainbow.", + "description_html": "Paint 100 beautiful rectangles", "function": "main", "checks": [ { diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki b/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki index 9d57b7b4bd..311ed7795f 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki +++ b/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki @@ -1,43 +1,10 @@ -// Light blue background -fill_color_hex("#ADD8E6") -rectangle(0, 0, 100, 100) +set x to 0 +set hue to 0 -// Ground -fill_color_hex("#ffffff") // Ice ground -rectangle(0, 70, 100, 30) // Ice ground +repeat 100 times do + change x to x + 1 + change hue to hue + 3 -// Penguin wings -fill_color_hex("#000000") // Black -ellipse(28, 55, 10, 25) // Left wing -ellipse(72, 55, 10, 25) // Right wing - -// Penguin body -fill_color_hex("#000000") // Black for the body -ellipse(50, 53, 25, 40) // Outer body (oval shape) -fill_color_hex("#ffffff") // White for the belly -ellipse(50, 50, 21, 39) // Inner belly (oval shape) - -// Penguin head -fill_color_hex("#000000") // Black -circle(50, 31, 23) // Head (circle) -fill_color_hex("#ffffff") // White for the face -ellipse(41, 32, 11, 14) // Left part of the face -ellipse(59, 32, 11, 14) // Right part of the face -ellipse(50, 40, 16, 11) // Lower part of the face - -// Penguin eyes -fill_color_hex("#000000") // Black -circle(42, 33, 3) // Left eye -fill_color_hex("#ffffff") // White -circle(43, 34, 1) // Left iris - -fill_color_hex("#000000") // Black -circle(58, 33, 3) // Right eye -fill_color_hex("#ffffff") // White -circle(57, 34, 1) // Right iris - -// Feet -fill_color_hex("#FFA500") -ellipse(40, 93, 7, 4) -ellipse(60, 93, 7, 4) -triangle(46, 38, 54, 38, 50, 47) \ No newline at end of file + fill_color_hsl(hue, 50, 50) + rectangle(x, 0, 1, 100) +end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md b/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md index da3c5f2632..596d7f225d 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md +++ b/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md @@ -1,23 +1,28 @@ -# The Penguin +# Rainbow -Your task is to make the penguin symmetrical. +Your task is to make a beautiful rainbow like this: -It should look like this: + - +The rainbow is made up of lots of bars. -We've drawn the left hand side for you, and added `TODO` comments for each of the things you need to do. +**Before reading any more of the instructions**, take a few minutes to work out conceptually how to achieve this. Write down the steps you think you need to follow on a piece of paper. -You'll need to think about setting the right colors before drawing things. +**Once you've got a solution** you're happy with (or given up), **scroll down** to see the instructions... -For the nose, you should **change** the middle coordinates of the triangle. Don't add a new triangle. +
+ +## How to solve it... + +- The rainbow is made up of `100` bars, each with a width of `1`, starting at the top and being `100` high. +- You need to set variables for `x` and for the `hue` of the color (both starting at `0`) +- You need to write a repeat loop that repeats 100 times. +- In each iteration of the repeat loop you need to increase `x` by 1 and increase the hue by `3`. +- You then need to use the `fill_color_hsl` (with saturation and luminance set around 50), and `rectangle` functions to draw. The functions used in this exercise are: -- `circle(center_x, center_y, radius)` - `rectangle(x, y, width, height)` -- `ellipse(center_x, center_y, radius_x, radius_y)` -- `triangle(x1,y1, x2,y2, x3,y3)` -- `fill_color_hex(hex)` +- `fill_color_hsl(hue, saturation, luminance)` -If you need help remembering how to use any of these functions, you can watch back the video from week 1. +If you need help remembering how to use any of these functions, you can watch back the video from week 1. If you need help with variables or animation, watch back the video from week 2! diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md b/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md index c9fc02faac..e52f9709b3 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md +++ b/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md @@ -1,5 +1 @@ -Follow the `TODO` comments to make the penguin symmetrical. - -As you get each bit right, tidy up the comments by removing the `TODO` and replacing it with a nicer comment. - -Use the "Check Scenarios" button regularly, and have fun! +This is an exercise in thinking carefully. Take your time. Have fun! diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json index d80453058b..80a3a6236d 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -16,14 +16,60 @@ "tests": [ { "slug": "draw-scence", - "name": "Make the penguin symmetrical.", - "description_html": "Fix all the TODO comments to make the penguin symmetrical.", + "name": "Make the flower sprout.", + "description_html": "Take it one step at a time!", "function": "main", "checks": [ { - "name": "getTriangleAt(46, 38, 54, 38, 50, 47)", + "name": "getCircleAt(50, 89, 0.4)", "matcher": "toExist", - "error_html": "The nose isn't right." + "error_html": "First Flower" + }, + { + "name": "getCircleAt(50, 30, 24)", + "matcher": "toExist", + "error_html": "Final Flower" + }, + + { + "name": "getCircleAt(50, 89, 0.1)", + "matcher": "toExist", + "error_html": "First Pistil" + }, + { + "name": "getCircleAt(50, 30, 6)", + "matcher": "toExist", + "error_html": "Final Pistil" + }, + { + "name": "getRectangleAt(49.95, 89, 0.1, 1)", + "matcher": "toExist", + "error_html": "First Stem" + }, + { + "name": "getRectangleAt(47, 30, 6, 60)", + "matcher": "toExist", + "error_html": "Final Stem" + }, + { + "name": "getEllipseAt(49.75, 89.5, 0.2, 0.08)", + "matcher": "toExist", + "error_html": "First Left Leaf" + }, + { + "name": "getEllipseAt(35, 60, 12, 4.8)", + "matcher": "toExist", + "error_html": "Final Left Leaf" + }, + { + "name": "getEllipseAt(50.25, 89.5, 0.2, 0.08)", + "matcher": "toExist", + "error_html": "First Right Leaf" + }, + { + "name": "getEllipseAt(65, 60, 12, 4.8)", + "matcher": "toExist", + "error_html": "Final Right Leaf" } ] } diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki b/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki index fbad8ea30b..e204bed951 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/example.jiki @@ -6,18 +6,18 @@ set flower_radius to 0 // pistil (the middle bit) set pistil_radius to 0 -// Stem +// Stem Variables set stem_height to 0 set stem_width to 0 set stem_left to 0 set stem_bottom to 90 -repeat 60 do - change stem_height to stem_height + 1 +repeat 60 times do + change flower_cy to flower_cy - 1 + change stem_height to stem_bottom -flower_cy change stem_width to stem_height / 10 change stem_left to flower_cx - ( stem_width / 2) change flower_radius to flower_radius + 0.4 - change flower_cy to stem_bottom - stem_height change pistil_radius to pistil_radius + 0.1 // Sky diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md index 21357e609f..a76442ab3d 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md @@ -1,5 +1,72 @@ -None of the individual things you need to do are hard. But putting them together may feel daunting and unfamiliar. Plan first. Then take each step at a time, and you'll get there. +# Sprouting Flower -This is definitely a tricky exercise and will take time to get right! +Your task is to make a flower that grows. -If you need help, please ask on the forum, and remember to give us lots of information about what's not working and why you think that's the case! +The animation should last `60` iterations and look something like this. + + + +The key to this exercise is to build relationships between the different elements. This is a key skill in programming. + +**Before reading any more of the instructions**, take a few minutes to work out conceptually how to achieve this. Write down the steps you think you need to follow on a piece of paper. + +**Once you've got a solution** you're happy with (or given up), **scroll down** to see the instructions... + +
+ +## How to solve it... + +The key component of this is the center of the flower. Everything else can be calculated off that center point. On each iteration of the loop, the center point should move up by `1` (before drawing). + +Here are some other things you need to know: + +- The `flower_radius` starts at `0` and should increase by `0.4` on each iteration (before drawing) +- The `pistil_radius` starts at `0` and should increase by `0.1` on each iteration (before drawing). +- The `stem_width` is 10% of the `stem_height` (so `stem_height / 10`). +- Everything is centered on the horizontal axis. +- The `x_radius` of the leaves is 50% the radius of the flower. +- The `y_radius` of the leaves is 20% of the radius of the flower. + +The functions you'll use are: + +- `circle(center_x, center_y, radius)` +- `ellipse(center_x, center_y, radius_x, radius_y)` +- `rectangle(x, y, width, height)` +- `fill_color_hex(hex)` + +It is **essential** to work on one thing at a time: + +- Start by drawing the pink flower and getting it to move up. +- Then get it to grow. +- Add the smaller yellow center.. +- Add the stem. +- Add the left leaf. +- Add the right leaf. + +Use the scrubber bar to scroll through the code and work out where things are going wrong. + +### The final flower + +If something's not working, here are some values you can check against (toggle the switch on the scrubber bar to get information): + +First drawn flower: + +- Flower: `circle(50, 89, 0.4)` +- Pistil: `circle(50, 89, 0.1)` +- Stem: `rectangle(49.95, 89, 0.1, 1)` +- Left leaf: `ellipse(49.75, 89.5, 0.2, 0.08)` +- Right leaf: `ellipse(50.25, 89.5, 0.2, 0.08)` + +Final drawn flower: + +- Flower: `circle(50, 30, 24)` +- Pistil: `circle(50, 30, 6)` +- Stem: `rectangle(47, 30, 6, 60)` +- Left leaf: `ellipse(35, 60, 12, 4.8)` +- Right leaf: `ellipse(65, 60, 12, 4.8)` + +### This is a tough exercise! + +This is a challenging exercise. Take your time, and if you get really stuck, ask for help on the forum, and remember to give us lots of information about what's not working and why you think that's the case! + +Remember, the learning is in the struggle! Every time you get something wrong and solve it, you're becoming a coder, and eventually it will feel easy. Just keep going! diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki index 634290500c..74a5138482 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki @@ -1,21 +1,18 @@ -// Sky -fill_color_hex("#ADD8E6") -rectangle(0, 0, 100, 100) - -// Ground -fill_color_hex("green") -rectangle(0, 90, 100, 30) - -// Stem +// Stem Variables set stem_height to 10 set stem_left to 58 set stem_width to 4 set stem_bottom to 90 -repeat 40 do +repeat 40 times do clear() - // Draw stem - fill_color_hex("darkgreen") - rectangle(stem_left, stem_bottom - stem_height, stem_width, stem_height) + // Sky + fill_color_hex("#ADD8E6") + rectangle(0, 0, 100, 90) + + // Ground + fill_color_hex("green") + rectangle(0, 90, 100, 30) + end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md index 601c75a18e..feb70f9b6f 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md @@ -1 +1 @@ -Make the flower sprout! +Draw a beautiful rainbow! diff --git a/bootcamp_content/projects/drawing/exercises/sunset/introduction.md b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md index a85203dd12..9caaebb200 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/introduction.md +++ b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md @@ -13,7 +13,7 @@ We've drawn the initial scene for you. You need to animate a few things: - The color of the sun from yellow to orange. You can use RGB or HSL. Use a color picker such as [this](https://htmlcolorcodes.com/color-picker/) to find a starting point and ending point you want to animate between. - The color of the sky. Again, you can use RGB or HSL. It's not possible to animate a true set of sunset colors this way, but be creative and choose something you like. -Remember to `set` the initial values **outside** of the `repeat` block, and then `change` them **inside** the block. +Remember to `set` the initial values **outside** of the `repeat` block, and then `change` them **inside** the block **before** you call the drawing functions. If you need a recap on how to animate things, make sure to watch the Level 2 live session back. The animation will flash a bit. That's expected. We'll learn how to fix that in a future lesson. @@ -27,4 +27,10 @@ The functions used in this exercise are: If you need help remembering how to use any of these functions, you can watch back the video from week 1. +### Checking colors + +We only check to see if you've got the position and the radius of the sun right. Although you can complete the exercise without doing so, **make sure you also animate the colors before moving on.** + +## Remember... + None of the individual things you need to do are hard. But putting them together may feel daunting and unfamiliar. Plan first. Then take each step at a time, and you'll get there. If you need help, please ask on the forum, and remember to give us lots of information about what's not working and why you think that's the case! diff --git a/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki b/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki index d854574a4b..fdd4d7755f 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki +++ b/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki @@ -1,7 +1,9 @@ set sun_radius to 5 set sun_cy to 10 -repeat 100 do +repeat 100 times o + // TODO: Update the variables here. + // The sky fill_color_hsl(210, 70, 60) rectangle(0,0,100,100) diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki index d48a0bbf0d..6742e2a0ec 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -2,11 +2,12 @@ set x to 27 set y to 75 set radius to 3 -repeat 60 do +fill_color_hex("blue") + +repeat 60 times do clear() change x to x + 1 - fill_color_hex("blue") circle(x, y, 3) end \ No newline at end of file diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki index ba801aedea..c361662351 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki @@ -4,14 +4,14 @@ // whatever you set this to when the first circle is drawn. set x to 0 -// TODO: Change this to only repeat -// until the ball reaches the hole -repeat 10 do - clear() +// TODO: Set your fill color - // TODO: Set the new position +// TODO: Change this to only repeat as many times as +// needed for the ball to reach the hole +repeat 10 times do + clear() - // TODO: Set your fill + // TODO: Increase the x position by 1 // TODO: Draw the ball (a circle) end \ No newline at end of file From 20627190ec1ba9e2e1bde484e5bf039b9f53d0cc Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 23:45:54 +0000 Subject: [PATCH 12/16] Updates --- app/commands/bootcamp/update_user_level.rb | 6 +- app/controllers/bootcamp/base_controller.rb | 5 + .../bootcamp/dashboard_controller.rb | 5 +- app/css/bootcamp/pages/dashboard.css | 151 +++++++--------- app/views/bootcamp/dashboard/index.html.haml | 166 ++++++------------ app/views/bootcamp/levels/show.html.haml | 6 +- bootcamp_content/concepts/config.json | 2 +- bootcamp_content/levels/2.md | 8 +- bootcamp_content/levels/config.json | 4 +- test/commands/bootcamp/update_level_test.rb | 22 +-- 10 files changed, 154 insertions(+), 221 deletions(-) diff --git a/app/commands/bootcamp/update_user_level.rb b/app/commands/bootcamp/update_user_level.rb index c4061e849b..06ca9fe14b 100644 --- a/app/commands/bootcamp/update_user_level.rb +++ b/app/commands/bootcamp/update_user_level.rb @@ -11,20 +11,20 @@ def call max = level_idx end - user.bootcamp_data.update!(level_idx: max) + user.bootcamp_data.update!(level_idx: max + 1) end memoize def exercise_ids_by_level_idx Bootcamp::Exercise.pluck(:level_idx, :id). group_by(&:first). - transform_values { |v| v.map(&:last) }. + transform_values { |v| v.map(&:last).sort }. sort.to_h end def solved_exercise_ids_by_level_idx user.bootcamp_solutions.completed.joins(:exercise).pluck(:level_idx, :exercise_id). group_by(&:first). - transform_values { |v| v.map(&:last) } + transform_values { |v| v.map(&:last).sort } end end diff --git a/app/controllers/bootcamp/base_controller.rb b/app/controllers/bootcamp/base_controller.rb index d324344438..7c7b573b76 100644 --- a/app/controllers/bootcamp/base_controller.rb +++ b/app/controllers/bootcamp/base_controller.rb @@ -1,6 +1,7 @@ class Bootcamp::BaseController < ApplicationController layout "bootcamp-ui" before_action :redirect_unless_attendee! + before_action :setup_bootcamp_data! private def redirect_unless_attendee! @@ -20,6 +21,10 @@ def redirect_unless_attendee! redirect_to bootcamp_path end + def setup_bootcamp_data! + current_user.bootcamp_data || current_user.create_bootcamp_data! + end + def use_project @project = Bootcamp::Project.find_by!(slug: params[:project_slug]) end diff --git a/app/controllers/bootcamp/dashboard_controller.rb b/app/controllers/bootcamp/dashboard_controller.rb index 3901c4bac4..00e581571f 100644 --- a/app/controllers/bootcamp/dashboard_controller.rb +++ b/app/controllers/bootcamp/dashboard_controller.rb @@ -2,6 +2,9 @@ class Bootcamp::DashboardController < Bootcamp::BaseController def index @exercise = Bootcamp::SelectNextExercise.(current_user) @solution = current_user.bootcamp_solutions.find_by(exercise: @exercise) - @level = Bootcamp::Level.find_by!(idx: 1) + + level_idx = [Bootcamp::Settings.level_idx, current_user.bootcamp_data.level_idx].min + level_idx = 1 if level_idx.zero? + @level = Bootcamp::Level.find_by!(idx: level_idx) end end diff --git a/app/css/bootcamp/pages/dashboard.css b/app/css/bootcamp/pages/dashboard.css index d6d40b2a21..0a0203c64d 100644 --- a/app/css/bootcamp/pages/dashboard.css +++ b/app/css/bootcamp/pages/dashboard.css @@ -3,108 +3,83 @@ body.namespace-bootcamp.controller-dashboard.action-index { } #page-bootcamp-dashboard { - section.intro { - @apply pt-24; - h1 { - @apply text-[36px] leading-140; - @apply font-bold text-textColor1; - @apply mb-4; - } - h2 { - @apply text-22 leading-150; - @apply font-semibold text-textColor1; - @apply mt-20 mb-4; - } - p.large { - @apply text-20 leading-150; - @apply mb-8; - } + @apply pt-24; + .exercise { + @paply w-full; + @apply py-12 px-16 rounded-8 border-1 border-borderColor5 block; + @apply shadow-base flex flex-col items-stretch; + @apply bg-white; + } + h1 { + @apply text-[36px] leading-140; + @apply font-bold text-textColor1; + @apply mb-4; + } + .level-content { p, ul { - @apply text-16 leading-150; + @apply text-20 leading-150; @apply mb-8; } - ul { - @apply list-disc pl-20; - } } + h2 { + @apply text-23 leading-150; + @apply font-semibold text-textColor1; + } + h3 { + @apply text-20 leading-150; + @apply mb-4; + @apply font-semibold; + } + h4 { + @apply text-16 leading-150; + @apply mb-4; + @apply font-semibold; + } + .tag { + @apply flex items-center; + @apply border-1 rounded-100; + @apply font-semibold leading-170; + @apply px-12 py-4 ml-auto; + @apply whitespace-nowrap; - section.normal { - @apply pt-24; - .exercise { - @paply w-full; - @apply py-12 px-16 rounded-8 border-1 border-borderColor5 block; - @apply shadow-base flex flex-col items-stretch; - @apply bg-white; - } - h1 { - @apply text-[36px] leading-140; - @apply font-bold text-textColor1; - @apply mb-4; + &.completed { + background: #e7fdf6; + border-color: #43b593; + color: #43b593; } - p.large { - @apply text-20 leading-150; - @apply mb-8; - } - h2 { - @apply text-23 leading-150; - @apply font-semibold text-textColor1; + } + p { + @apply text-16 leading-140; + } + + .c-youtube-container { + @apply mt-20; + iframe { + @apply w-full; } - h3 { - @apply text-20 leading-150; - @apply mb-4; - @apply font-semibold; + } +} +.rhs { + .section-link { + @apply border-1 border-borderColor5 block rounded-5 px-20 py-12 bg-white shadow-sm; + @apply flex gap-16; + img { + @apply w-[40px] h-[40px]; } h4 { - @apply text-16 leading-150; - @apply mb-4; - @apply font-semibold; - } - .tag { - @apply flex items-center; - @apply border-1 rounded-100; - @apply font-semibold leading-170; - @apply px-12 py-4 ml-auto; - @apply whitespace-nowrap; - - &.completed { - background: #e7fdf6; - border-color: #43b593; - color: #43b593; - } + @apply text-18 font-semibold mb-2; } p { - @apply text-16 leading-140; - } - - .c-youtube-container { - @apply mt-20; - iframe { - @apply w-full; - } + @apply text-15 leading-140; } } - .rhs { - .section-link { - @apply border-1 border-borderColor5 block rounded-5 px-20 py-12 bg-white shadow-sm; - @apply flex gap-16; - img { - @apply w-[40px] h-[40px]; - } - h4 { - @apply text-18 font-semibold mb-2; - } - p { - @apply text-15 leading-140; - } - } - .level-number { - @apply rounded-circle text-[24px] leading-140; - @apply grid place-items-center flex-shrink-0; - @apply border-3 border-purple; - @apply text-purple font-bold; - @apply w-[48px] h-[48px]; - } + .level-number { + @apply rounded-circle text-[24px] leading-140; + @apply grid place-items-center flex-shrink-0; + @apply border-3 border-purple; + @apply text-purple font-bold; + @apply w-[48px] h-[48px]; } } diff --git a/app/views/bootcamp/dashboard/index.html.haml b/app/views/bootcamp/dashboard/index.html.haml index 00c3633d9b..b8178efd89 100644 --- a/app/views/bootcamp/dashboard/index.html.haml +++ b/app/views/bootcamp/dashboard/index.html.haml @@ -1,118 +1,64 @@ #page-bootcamp-dashboard - - if Bootcamp::Settings.level_idx.zero? - %section.intro - .lg-container - %div{ class: 'max-w-[720px] mx-auto' } - %h1 Welcome to the Exercism Bootcamp! - %p.large Thanks so much for joining the Bootcamp. We're really excited to share our passion for programming with you! - %h2 Introductory Session - %p.text-18 - Our kick-off session is at 18:00 UTC on Saturday Jan 11th. - We'll introduce you to the Bootcamp, explain how it works, and answer any questions you have. - Then we'll dig into the first steps on your coding journey. - %p.text-18 - Go to - = link_to 'https://youtube.com/live/bOAL_EIFwhg', 'https://youtube.com/live/bOAL_EIFwhg', class: 'text-linkColor font-semibold' - to watch the session. + .lg-container + .flex.mb-40 + .lhs.mr-48{ class: 'w-full max-w-[800px]' } + %h1 Welcome to Level #{@level.idx}! + .level-content + = raw @level.content_html + - if @level.youtube_id + = render ReactComponents::Common::YoutubePlayer.new(@level.youtube_id, 'community') - %h2 Forum & Discord - %p.text-18 - We will be using Exercism's Forum and Discord server throughout this course to get unstuck and hang out. - Both have dedicated bootcamp areas which - %strong.font-medium you can access now. - %ul - %li - %strong.font-semibold The forum (#{link_to 'exercism.org/r/forum', 'https://exercism.org/r/forum', class: 'text-linkColor'}) - is the space to get help or ask questions. There is a dedicated Bootcamp category. Log in with your Exercism account and read - = link_to 'this post', 'https://forum.exercism.org/t/welcome-to-the-bootcamp-forum-space/14389', class: 'text-linkColor font-semibold' - to get started. - %li - %strong.font-semibold Our Discord Server (#{link_to 'exercism.org/r/discord', 'https://exercism.org/r/discord', class: 'text-linkColor'}) - is the place to go to chat and hang out with other participants. There is a dedicated #bootcamp channel reserved for you. - %strong.font-semibold Please use threads - on Discord to keep things ordered (don't be offended if you forget and we move your messages!). - To get access to the Bootcamp Channels, make sure your Exercism and Discord accounts #{link_to 'are connected here', 'https://exercism.org/settings/integrations', class: 'text-linkColor font-semibold'}. + %hr.border-borderColor5.my-20 + - if @exercise + - if @solution + %h2.mb-4 Continue where you left off + .text-18.mb-20 You have an exercise in progress. - %h2 Weekly Rhythm - %p.text-18 - We've designed each week to follow a similar pattern. - %ul.text-18 - %li Monday at 18:00 UTC: Introduction to the week's content (Live Session). - %li Tuesday to Friday: Work on the week's exercises. - %li Saturday at 18:00 UTC: Deep dive into the week's exercises (Live Session). - %p.text-18 - The live sessions will be streamed on our YouTube channel and will be available to watch later if you can't make it. + = render ViewComponents::Bootcamp::ExerciseWidget.new(@exercise, solution: @solution, size: "large") + - else + %h2.mb-4 Start new exercise + .text-18.mb-20 You have a new exercise available to work on. + = render ViewComponents::Bootcamp::ExerciseWidget.new(@exercise, size: "large") + - else + %h2.mb-8 All exercises completed 🎉 + .text-18.mb-20 Congratulations - you have completed all the exercises currently available to you. - %h2 Am I signed up correctly? - %p.text-18.pb-20 - Yes. If you're seeing this page, then you're signed up and will be able to access the content after the 11th. + .rhs.ml-auto{ class: 'max-w-[400px]' } + %h2.mb-4 Upcoming Live Sessions + %ul.text-16.leading-140.list-disc.ml-16.mb-20 + %li.mb-4 + %strong Teaching: + Monday 13th Jan at 18:00 UTC + %li + %strong Labs: + Saturday 18th Jan at 18:00 UTC + %li.mb-4 + %strong Teaching: + Monday 20th Jan at 18:00 UTC + %li + %strong Labs: + Saturday 25th Jan at 18:00 UTC - - else - %section.normal - .lg-container - .flex.mb-40 - .lhs.mr-48{ class: 'w-full max-w-[800px]' } - %h1 Welcome to Level #{@level.idx}! - %p.large - These first few days are mainly introductory - ensuring you're connected on Discord and the Forum, and understand how everything works. - But you'll also be taking your first steps in the world of programming - using code to draw. - %p.large - Watch the introductory video session, solve the Level 1 exercises and get connected to the other students. - - if @level.youtube_id - = render ReactComponents::Common::YoutubePlayer.new(@level.youtube_id, 'community') + %h2.mb-2 Make the most of the Bootcamp + %p.mb-16 The more you take part in all areas of the Bootcamp, the more you'll learn. + .flex.flex-col.gap-16 + = link_to bootcamp_level_path(@level), class: "section-link" do + .level-number= @level.idx + .text + %h4 Level #{@level.idx} + %p All the information from Level 1 in one place. Videos, concepts and exercises. - %hr.border-borderColor5.my-20 + = link_to "https://exercism.org/r/discord", class: "section-link" do + = graphical_icon 'external-site-discord-blue' + .text + %h4 Chat on Discord + %p Use the exclusive #bootcamp area on Exercism's Discord server to chat to other Bootcamp students and mentors. - - if @exercise - - if @solution - %h2.mb-4 Continue where you left off - .text-18.mb-20 You have an exercise in progress. - - = render ViewComponents::Bootcamp::ExerciseWidget.new(@exercise, solution: @solution, size: "large") - - else - %h2.mb-4 Start new exercise - .text-18.mb-20 You have a new exercise available to work on. - = render ViewComponents::Bootcamp::ExerciseWidget.new(@exercise, size: "large") - - else - %h2.mb-8 All exercises completed 🎉 - .text-18.mb-20 Congratulations - you have completed all the exercises currently available to you. - - .rhs.ml-auto{ class: 'max-w-[400px]' } - %h2.mb-4 Upcoming Live Sessions - %ul.text-16.leading-140.list-disc.ml-16.mb-20 - %li.mb-4 - %strong Teaching: - Monday 13th Jan at 18:00 UTC - %li - %strong Labs: - Saturday 18th Jan at 18:00 UTC - %li.mb-4 - %strong Teaching: - Monday 20th Jan at 18:00 UTC - %li - %strong Labs: - Saturday 25th Jan at 18:00 UTC - - %h2.mb-2 Make the most of the Bootcamp - %p.mb-16 The more you take part in all areas of the Bootcamp, the more you'll learn. - .flex.flex-col.gap-16 - = link_to bootcamp_level_path(@level), class: "section-link" do - .level-number= @level.idx - .text - %h4 Level #{@level.idx} - %p All the information from Level 1 in one place. Videos, concepts and exercises. - - = link_to "https://exercism.org/r/discord", class: "section-link" do - = graphical_icon 'external-site-discord-blue' - .text - %h4 Chat on Discord - %p Use the exclusive #bootcamp area on Exercism's Discord server to chat to other Bootcamp students and mentors. - - = link_to "https://forum.exercism.org/c/bootcamp/661", class: "section-link" do - = graphical_icon 'discourser' - .text - %h4 Ask questions on the Forum - %p - Use the Bootcamp Category on the forum to ask questions and get unstuck. - Read the pinned post to learn more. + = link_to "https://forum.exercism.org/c/bootcamp/661", class: "section-link" do + = graphical_icon 'discourser' + .text + %h4 Ask questions on the Forum + %p + Use the Bootcamp Category on the forum to ask questions and get unstuck. + Read the pinned post to learn more. diff --git a/app/views/bootcamp/levels/show.html.haml b/app/views/bootcamp/levels/show.html.haml index e419d548f7..0852f6ebc7 100644 --- a/app/views/bootcamp/levels/show.html.haml +++ b/app/views/bootcamp/levels/show.html.haml @@ -26,10 +26,11 @@ .lg-container .flex.pb-40 .lhs.pr-40 - - if @level.youtube_id - = render ReactComponents::Common::YoutubePlayer.new(@level.youtube_id, 'community') .content.c-prose = raw @level.content_html + - if @level.youtube_id + .mt-24 + = render ReactComponents::Common::YoutubePlayer.new(@level.youtube_id, 'community') %hr.border-borderColor5.my-32 @@ -41,6 +42,7 @@ = link_to bootcamp_concept_path(concept) do .text-20.leading-140.font-semibold.mb-2= concept.title .text-18.leading-140.text-textColor5.font-normal= concept.description + .rhs .c-rhs-list %h2 Exercises diff --git a/bootcamp_content/concepts/config.json b/bootcamp_content/concepts/config.json index 9672acb58e..4eaf8b627f 100644 --- a/bootcamp_content/concepts/config.json +++ b/bootcamp_content/concepts/config.json @@ -80,7 +80,7 @@ "slug": "conditionals", "title": "Conditionals", "description": "", - "level": 2 + "level": 3 }, { "slug": "loops-repeat", diff --git a/bootcamp_content/levels/2.md b/bootcamp_content/levels/2.md index 6a13d568e1..f8bd282449 100644 --- a/bootcamp_content/levels/2.md +++ b/bootcamp_content/levels/2.md @@ -1,7 +1,9 @@ # Level 2 -Welcome to Level 2! +Last week we touched on the absolute basics of coding - using functions. Hopefully you had lots of fun testing your drawing abilities, and getting creative with colors. -Last week we touched on the absolute basics of coding - using functions. Hopefully you had lots of fun testing your drawing abilities, and getting creative. +In Level 2, we're going to build on what we learned last week, and look at when code moves beyond a linear list of commands, exploring variables and `repeat` blocks. -In Level 2, we're going to build on what we learned last week, and look at when code moves beyond a linear list of commands, exploring loops, conditionals, variables, and writing our own functions. This is the most full content-rich week of the whole Bootcamp, and we'll cover quite a lot of ground, so take it slowly and make sure you understand each part thoroughly. +I think this is one of the most challenge weeks of the whole bootcamp. Stick with it - you'll get through it. Just keep watching back the video if you need a reminder, keep asking yourself what each line of code does and visualise Jiki as you do so. + +Good luck, and use our support on Discord and the forum if you get stuck! diff --git a/bootcamp_content/levels/config.json b/bootcamp_content/levels/config.json index 3ae7122d7b..f5ffe60099 100644 --- a/bootcamp_content/levels/config.json +++ b/bootcamp_content/levels/config.json @@ -4,8 +4,8 @@ "description": "Learn what coding is and how to write your first lines of code." }, { - "title": "Conditionals and Returns", - "description": "" + "title": "Variables and Repeats", + "description": "Learn how to create, update and use variables, and learn repeats." }, { "title": "Loops", diff --git a/test/commands/bootcamp/update_level_test.rb b/test/commands/bootcamp/update_level_test.rb index a552c2afdc..c9ee9cc2b0 100644 --- a/test/commands/bootcamp/update_level_test.rb +++ b/test/commands/bootcamp/update_level_test.rb @@ -1,24 +1,24 @@ require 'test_helper' class Bootcamp::UpdateUserLevelTest < ActiveSupport::TestCase - test "sets to 0 with no exercises" do + test "sets to 1 with no exercises" do user = create :user, :with_bootcamp_data Bootcamp::UpdateUserLevel.(user) - assert_equal 0, user.bootcamp_data.level_idx + assert_equal 1, user.bootcamp_data.level_idx end - test "sets to 0 with no completed exercises" do + test "sets to 1 with no completed exercises" do user = create :user, :with_bootcamp_data create :bootcamp_exercise Bootcamp::UpdateUserLevel.(user) - assert_equal 0, user.bootcamp_data.level_idx + assert_equal 1, user.bootcamp_data.level_idx end - test "sets to level when completed" do + test "sets to next level when completed" do user = create :user, :with_bootcamp_data create :bootcamp_level, idx: 1 exercise = create :bootcamp_exercise, level_idx: 1 @@ -26,10 +26,10 @@ class Bootcamp::UpdateUserLevelTest < ActiveSupport::TestCase Bootcamp::UpdateUserLevel.(user) - assert_equal 1, user.bootcamp_data.level_idx + assert_equal 2, user.bootcamp_data.level_idx end - test "does not set when not all completed" do + test "sets to current level when not all completed" do user = create :user, :with_bootcamp_data create :bootcamp_level, idx: 1 create :bootcamp_solution, user:, exercise: create(:bootcamp_exercise, level_idx: 1) @@ -37,10 +37,10 @@ class Bootcamp::UpdateUserLevelTest < ActiveSupport::TestCase Bootcamp::UpdateUserLevel.(user) - assert_equal 0, user.bootcamp_data.level_idx + assert_equal 1, user.bootcamp_data.level_idx end - test "sets to highest level when multiple completed" do + test "sets to next possible level when multiple completed" do user = create :user, :with_bootcamp_data create :bootcamp_level, idx: 1 create :bootcamp_level, idx: 2 @@ -51,7 +51,7 @@ class Bootcamp::UpdateUserLevelTest < ActiveSupport::TestCase Bootcamp::UpdateUserLevel.(user) - assert_equal 2, user.bootcamp_data.level_idx + assert_equal 3, user.bootcamp_data.level_idx end test "requires consecutive levels" do @@ -68,6 +68,6 @@ class Bootcamp::UpdateUserLevelTest < ActiveSupport::TestCase Bootcamp::UpdateUserLevel.(user) - assert_equal 1, user.bootcamp_data.level_idx + assert_equal 2, user.bootcamp_data.level_idx end end From a31aa02fd7e924532306dc3814804daaa70461fa Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 13 Jan 2025 23:47:34 +0000 Subject: [PATCH 13/16] Add golf introduction --- bootcamp_content/projects/golf/introduction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootcamp_content/projects/golf/introduction.md b/bootcamp_content/projects/golf/introduction.md index 3de004a308..8556e87038 100644 --- a/bootcamp_content/projects/golf/introduction.md +++ b/bootcamp_content/projects/golf/introduction.md @@ -2,6 +2,6 @@ ## Overview -With a little bit of code, you can draw almost anything! +In this project, we're going to learn use animation, functions and conditonals to build some basic game mechanics. -In this project, we'll explore drawing and animation, teaching you some of the key programming concepts, and getting you familiar with how to make your own graphics that you can use in your games, applications and websites. +We'll start by animating a simple ball, then work out when it drops into the hole, and add scoring. From 9758e189c57fc55e53c8728a6b0c1ad594170857 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Tue, 14 Jan 2025 00:06:47 +0000 Subject: [PATCH 14/16] Double check instructions --- .../exercises/draw/DrawExercise.tsx | 30 ++++++- .../drawing/exercises/rainbow/config.json | 86 ++----------------- .../sprouting-flower/introduction.md | 9 +- .../exercises/sprouting-flower/stub.jiki | 11 +-- .../drawing/exercises/sunset/config.json | 10 +++ .../drawing/exercises/sunset/introduction.md | 4 - .../golf/exercises/rolling-ball/example.jiki | 2 +- 7 files changed, 57 insertions(+), 95 deletions(-) diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx index 4a5e329fea..9f2a5babeb 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/draw/DrawExercise.tsx @@ -21,6 +21,7 @@ class Rectangle extends Shape { public y: number, public width: number, public height: number, + public fillColor: FillColor, element: SVGElement ) { super(element) @@ -32,6 +33,7 @@ class Circle extends Shape { public cx: number, public cy: number, public radius: number, + public fillColor: FillColor, element: SVGElement ) { super(element) @@ -260,6 +262,30 @@ export default class DrawExercise extends Exercise { }) } + public checkUniqueColoredRectangles(_: InterpretResult, count: number) { + let colors = new Set() + this.shapes.forEach((shape) => { + if (!(shape instanceof Rectangle)) { + return + } + + colors.add(`${shape.fillColor.type}-${shape.fillColor.color.toString()}`) + }) + return colors.size >= count + } + + public checkUniqueColoredCircles(_: InterpretResult, count: number) { + let colors = new Set() + this.shapes.forEach((shape) => { + if (!(shape instanceof Circle)) { + return + } + + colors.add(`${shape.fillColor.type}-${shape.fillColor.color.toString()}`) + }) + return colors.size >= count + } + public assertAllArgumentsAreVariables(interpreterResult: InterpretResult) { return interpreterResult.frames.every((frame: Frame) => { if (!(frame.context instanceof ExpressionStatement)) { @@ -315,7 +341,7 @@ export default class DrawExercise extends Exercise { ) this.canvas.appendChild(elem) - const rect = new Rectangle(x, y, width, height, elem) + const rect = new Rectangle(x, y, width, height, this.fillColor, elem) this.shapes.push(rect) this.visibleShapes.push(rect) this.animateElement(executionCtx, elem, absX, absY) @@ -339,7 +365,7 @@ export default class DrawExercise extends Exercise { ) this.canvas.appendChild(elem) - const circle = new Circle(x, y, radius, elem) + const circle = new Circle(x, y, radius, this.fillColor, elem) this.shapes.push(circle) this.visibleShapes.push(circle) this.animateElement(executionCtx, elem, absX, absY) diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/config.json b/bootcamp_content/projects/drawing/exercises/rainbow/config.json index cd90f589a3..d25381b828 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/config.json +++ b/bootcamp_content/projects/drawing/exercises/rainbow/config.json @@ -16,94 +16,24 @@ "tests": [ { "slug": "draw-scence", - "name": "The the rainbow.", + "name": "Draw the rainbow.", "description_html": "Paint 100 beautiful rectangles", "function": "main", "checks": [ { - "name": "getRectangleAt(0, 0, 100, 100)", + "name": "getRectangleAt(1, 0, undefined, 100)", "matcher": "toExist", - "error_html": "The sky has gone wrong." + "error_html": "The first rectangle is missing" }, { - "name": "getRectangleAt(0, 70, 100, 30)", + "name": "getRectangleAt(100, 0, undefined, 100)", "matcher": "toExist", - "error_html": "The ground has gone wrong." + "error_html": "The last rectangle is missing" }, { - "name": "getEllipseAt(28, 55, 10, 25)", - "matcher": "toExist", - "error_html": "The left wing doesn't seem right." - }, - { - "name": "getEllipseAt(72, 55, 10, 25)", - "matcher": "toExist", - "error_html": "The right wing doesn't seem right." - }, - { - "name": "getEllipseAt(50, 53, 25, 40)", - "matcher": "toExist", - "error_html": "The outer body has gone wrong." - }, - { - "name": "getEllipseAt(50, 50, 21, 39)", - "matcher": "toExist", - "error_html": "The inner body has gone wrong." - }, - { - "name": "getCircleAt(50, 31, 23)", - "matcher": "toExist", - "error_html": "The head has gone wrong." - }, - { - "name": "getEllipseAt(41, 32, 11, 14)", - "matcher": "toExist", - "error_html": "The left side of the face doesn't look right." - }, - { - "name": "getEllipseAt(59, 32, 11, 14)", - "matcher": "toExist", - "error_html": "The right side of the face doesn't look right." - }, - { - "name": "getEllipseAt(50, 40, 16, 11)", - "matcher": "toExist", - "error_html": "The lower part of the face doesn't look right." - }, - { - "name": "getCircleAt(42, 33, 3)", - "matcher": "toExist", - "error_html": "The left eye seems off." - }, - { - "name": "getCircleAt(43, 34, 1)", - "matcher": "toExist", - "error_html": "The left iris seems off." - }, - { - "name": "getCircleAt(58, 33, 3)", - "matcher": "toExist", - "error_html": "The right eye seems off." - }, - { - "name": "getCircleAt(57, 34, 1)", - "matcher": "toExist", - "error_html": "The right iris seems off." - }, - { - "name": "getEllipseAt(40, 93, 7, 4)", - "matcher": "toExist", - "error_html": "The left foot's gone astray." - }, - { - "name": "getEllipseAt(60, 93, 7, 4)", - "matcher": "toExist", - "error_html": "The right foot's not right." - }, - { - "name": "getTriangleAt(46, 38, 54, 38, 50, 47)", - "matcher": "toExist", - "error_html": "The nose isn't right." + "name": "checkUniqueColoredRectangles(100)", + "matcher": "toBeTrue", + "error_html": "There are not 100 different colored rectangles." } ] } diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md index a76442ab3d..cbb006e5b8 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md @@ -20,10 +20,13 @@ The key component of this is the center of the flower. Everything else can be ca Here are some other things you need to know: -- The `flower_radius` starts at `0` and should increase by `0.4` on each iteration (before drawing) -- The `pistil_radius` starts at `0` and should increase by `0.1` on each iteration (before drawing). -- The `stem_width` is 10% of the `stem_height` (so `stem_height / 10`). +- The radius of the flower starts at `0` and should increase by `0.4` on each iteration (before drawing) +- The radius of the pistil (the middle yellow bit of the flower) starts at `0` and should increase by `0.1` on each iteration (before drawing). +- The stem should start at the center of the flower and reach the ground. +- The stem's width is 10% of the stem's height (so `stem_height / 10`). - Everything is centered on the horizontal axis. +- The leaves sit flush against the stalk on each side. +- The leaves sit half way down the stem. - The `x_radius` of the leaves is 50% the radius of the flower. - The `y_radius` of the leaves is 20% of the radius of the flower. diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki index 74a5138482..4dd874fb51 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki @@ -1,11 +1,7 @@ -// Stem Variables -set stem_height to 10 -set stem_left to 58 -set stem_width to 4 -set stem_bottom to 90 +// TODO: Set your initial variables here -repeat 40 times do - clear() +repeat 60 times do + // TODO: Update your variables here // Sky fill_color_hex("#ADD8E6") @@ -15,4 +11,5 @@ repeat 40 times do fill_color_hex("green") rectangle(0, 90, 100, 30) + // TODO: Draw the flower here end \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json index bb7f09646e..926ec55662 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/config.json +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -34,6 +34,16 @@ "name": "getCircleAt(50, 109, 24.8)", "matcher": "toExist", "error_html": "The sun seems wrong near the end." + }, + { + "name": "checkUniqueColoredRectangles(10)", + "matcher": "toBeTrue", + "error_html": "The sky doesn't seem to be changing color" + }, + { + "name": "checkUniqueColoredCircles(10)", + "matcher": "toBeTrue", + "error_html": "The sun doesn't seem to be changing color" } ] } diff --git a/bootcamp_content/projects/drawing/exercises/sunset/introduction.md b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md index 9caaebb200..b1648cd5ec 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/introduction.md +++ b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md @@ -27,10 +27,6 @@ The functions used in this exercise are: If you need help remembering how to use any of these functions, you can watch back the video from week 1. -### Checking colors - -We only check to see if you've got the position and the radius of the sun right. Although you can complete the exercise without doing so, **make sure you also animate the colors before moving on.** - ## Remember... None of the individual things you need to do are hard. But putting them together may feel daunting and unfamiliar. Plan first. Then take each step at a time, and you'll get there. If you need help, please ask on the forum, and remember to give us lots of information about what's not working and why you think that's the case! diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki index 6742e2a0ec..4af819e876 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -4,7 +4,7 @@ set radius to 3 fill_color_hex("blue") -repeat 60 times do +repeat 61 times do clear() change x to x + 1 From c7403514a152c2d574eb48f63efe668b9d2f73d0 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Tue, 14 Jan 2025 00:24:48 +0000 Subject: [PATCH 15/16] Don't show locked exercises --- app/views/bootcamp/concepts/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/bootcamp/concepts/show.html.haml b/app/views/bootcamp/concepts/show.html.haml index 113f42fa61..418fd0163b 100644 --- a/app/views/bootcamp/concepts/show.html.haml +++ b/app/views/bootcamp/concepts/show.html.haml @@ -31,10 +31,10 @@ .font-semibold.text-primary-blue.text-20.mb-2= concept.title .text-18.leading-150= concept.description .rhs - - if @concept.exercises.present? + - if @concept.exercises.unlocked.present? .c-rhs-list %h2.mb-12 Exercises %p These exercises have been designed to help you practice this concept. %ul - - @concept.exercises.each do |exercise| + - @concept.exercises.unlocked.each do |exercise| %li= render ViewComponents::Bootcamp::ExerciseWidget.new(exercise) From 280069130350afc3121d65e189a4c1d491fd8a58 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Tue, 14 Jan 2025 12:04:32 +0000 Subject: [PATCH 16/16] Green --- app/views/bootcamp/dashboard/index.html.haml | 14 +++++--------- .../languages/jikiscript/interpreter.test.ts | 8 ++++---- .../languages/jikiscript/parser.test.ts | 2 +- .../languages/jikiscript/syntaxErrors.test.ts | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/views/bootcamp/dashboard/index.html.haml b/app/views/bootcamp/dashboard/index.html.haml index 22a21b16c7..6cbb20dd8c 100644 --- a/app/views/bootcamp/dashboard/index.html.haml +++ b/app/views/bootcamp/dashboard/index.html.haml @@ -1,12 +1,11 @@ #page-bootcamp-dashboard .lg-container .flex.mb-40 - .lhs.mr-48{ class: 'w-full max-w-[800px]' } + .lhs.mr-48{ class: 'w-full max-w-[780px]' } %h1 Welcome to Level #{@level.idx}! .level-content = raw @level.content_html - - if @level.youtube_id - = render ReactComponents::Common::YoutubePlayer.new(@level.youtube_id, 'community') + = link_to "Go to Level 2", bootcamp_level_path(@level), class: "btn-l btn-primary mt-20 max-w-[200px]" %hr.border-borderColor5.my-20 @@ -28,9 +27,6 @@ %h2.mb-4 Upcoming Live Sessions %ul.text-16.leading-140.list-disc.ml-16.mb-20 %li.mb-4 - %strong Teaching: - Monday 13th Jan at 18:00 UTC - %li %strong Labs: Saturday 18th Jan at 18:00 UTC %li.mb-4 @@ -51,17 +47,17 @@ .text %h4 Level #{@level.idx} %p All the information from Level 1 in one place. Videos, concepts and exercises. - + = link_to "https://exercism.org/r/discord", class: "section-link" do = graphical_icon 'external-site-discord-blue' .text %h4 Chat on Discord %p Use the exclusive #bootcamp area on Exercism's Discord server to chat to other Bootcamp students and mentors. - + = link_to "https://forum.exercism.org/c/bootcamp/661", class: "section-link" do = graphical_icon 'discourser' .text %h4 Ask questions on the Forum %p Use the Bootcamp Category on the forum to ask questions and get unstuck. - Read the pinned post to learn more. \ No newline at end of file + Read the pinned post to learn more. diff --git a/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts b/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts index 7b9464293a..5d7df64f76 100644 --- a/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts +++ b/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts @@ -457,7 +457,7 @@ describe('statements', () => { test('declared variable can be used in blocks', () => { const { error, frames } = interpret(` set pos to 10 - repeat(5) do + repeat 5 times do change pos to pos + 10 end `) @@ -468,7 +468,7 @@ describe('statements', () => { test('declared variable is persisted after repeat', () => { const { error, frames } = interpret(` set pos to 10 - repeat(5) do + repeat 5 times do change pos to pos + 10 end change pos to pos + 10 @@ -601,7 +601,7 @@ describe('statements', () => { test('once', () => { const { error, frames } = interpret(` set x to 0 - repeat 1 do + repeat 1 times do change x to x + 1 end `) @@ -617,7 +617,7 @@ describe('statements', () => { test('multiple times', () => { const { frames } = interpret(` set x to 0 - repeat 3 do + repeat 3 times do change x to x + 1 end `) diff --git a/test/javascript/interpreter/languages/jikiscript/parser.test.ts b/test/javascript/interpreter/languages/jikiscript/parser.test.ts index 794f52e84a..a7f75de430 100644 --- a/test/javascript/interpreter/languages/jikiscript/parser.test.ts +++ b/test/javascript/interpreter/languages/jikiscript/parser.test.ts @@ -808,7 +808,7 @@ describe('if', () => { describe('repeat', () => { test('with number literal', () => { const stmts = parse(` - repeat 3 do + repeat 3 times do set x to 1 end `) diff --git a/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts b/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts index 0aef419a6f..a2b5289301 100644 --- a/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts +++ b/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts @@ -248,7 +248,7 @@ describe('syntax errors', () => { test('repeat', () => { expect(() => parse(` - repeat 5 + repeat 5 times end `) ).toThrow('MissingDoToStartBlock: type: repeat') @@ -268,7 +268,7 @@ describe('syntax errors', () => { test('repeat', () => { expect(() => parse(` - repeat 5 do + repeat 5 times do `) ).toThrow('MissingEndAfterBlock: type: repeat') })