From 2dab2d42db5700a172d816a4a6b11808368c7759 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Tue, 14 Jan 2025 12:12:59 +0000 Subject: [PATCH] Add check for variable only arguments (#7295) * Add check for variable only arguemnts * More content * Improve things * Improve example * Simplify next exercise selection * Use 2DP and fix multiplication * WIP * Improve unlocking logic * Fix tests * Update idx * WIP * Updates * Add golf introduction * Double check instructions * Don't show locked exercises * Green --- .../bootcamp/exercise/available_for_user.rb | 27 +++ app/commands/bootcamp/select_next_exercise.rb | 20 +- app/commands/bootcamp/solution/create.rb | 8 +- app/commands/bootcamp/update_user_level.rb | 6 +- .../api/bootcamp/solutions_controller.rb | 2 +- app/controllers/bootcamp/base_controller.rb | 5 + .../bootcamp/dashboard_controller.rb | 5 +- app/css/bootcamp/pages/dashboard.css | 151 ++++++-------- .../AnimationTimeline/AnimationTimeline.ts | 1 + .../extensions/placeholder-widget.ts | 1 - .../SolveExercisePage/Scrubber/useScrubber.ts | 10 +- .../exercises/draw/DrawExercise.tsx | 112 +++++++++-- .../exercises/draw/shapes.ts | 7 +- .../SolveExercisePage/test-runner/expect.ts | 13 ++ .../execGenericTest.ts | 11 +- .../execProjectTest.ts | 11 +- .../generateExpects.ts | 21 +- .../components/bootcamp/types/Matchers.d.ts | 2 + app/javascript/interpreter/executor.ts | 36 ++-- app/javascript/interpreter/frames.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/models/bootcamp/exercise.rb | 2 +- app/models/bootcamp/user_project.rb | 16 +- app/views/bootcamp/concepts/show.html.haml | 4 +- app/views/bootcamp/dashboard/index.html.haml | 186 ++++++------------ app/views/bootcamp/levels/show.html.haml | 6 +- app/views/bootcamp/projects/show.html.haml | 2 +- .../bootcamp/exercise_widget.html.haml | 1 - bootcamp_content/concepts/config.json | 2 +- bootcamp_content/levels/2.md | 8 +- bootcamp_content/levels/config.json | 4 +- bootcamp_content/projects/drawing/config.json | 9 +- .../exercises/jumbled-house/config.json | 1 + .../drawing/exercises/penguin/config.json | 1 + .../drawing/exercises/rainbow/config.json | 43 ++++ .../drawing/exercises/rainbow/example.jiki | 10 + .../drawing/exercises/rainbow/introduction.md | 28 +++ .../drawing/exercises/rainbow/stub.jiki | 58 ++++++ .../drawing/exercises/rainbow/task-1.md | 1 + .../exercises/sprouting-flower/config.json | 79 ++++++++ .../exercises/sprouting-flower/example.jiki | 45 +++++ .../sprouting-flower/introduction.md | 75 +++++++ .../exercises/sprouting-flower/stub.jiki | 15 ++ .../exercises/sprouting-flower/task-1.md | 1 + .../exercises/structured-house/config.json | 58 ++++++ .../exercises/structured-house/example.jiki | 85 ++++++++ .../structured-house/introduction.md | 38 ++++ .../exercises/structured-house/stub.jiki | 45 +++++ .../exercises/structured-house/task-1.md | 18 ++ .../drawing/exercises/sunset/config.json | 53 +++++ .../drawing/exercises/sunset/example.jiki | 35 ++++ .../drawing/exercises/sunset/introduction.md | 32 +++ .../drawing/exercises/sunset/stub.jiki | 22 +++ .../drawing/exercises/sunset/task-1.md | 1 + bootcamp_content/projects/golf/config.json | 6 + .../golf/exercises/rolling-ball/config.json | 56 ++++++ .../golf/exercises/rolling-ball/example.jiki | 13 ++ .../exercises/rolling-ball/introduction.md | 24 +++ .../golf/exercises/rolling-ball/stub.jiki | 17 ++ .../golf/exercises/rolling-ball/task-1.md | 1 + .../projects/golf/introduction.md | 7 + .../exercises/automated-solve/config.json | 3 +- .../maze/exercises/manual-solve/config.json | 1 + .../exercises/even-or-odd/config.json | 3 +- .../positive-negative-or-zero/config.json | 3 +- .../exercises/basic/config.json | 3 +- .../two-fer/exercises/basic/config.json | 3 +- .../exercises/cloud-rain-sun/config.json | 1 + .../exercises/cloud-rain-sun/introduction.md | 6 +- .../weather/exercises/sunshine/config.json | 1 + .../exercises/process-guess/config.json | 1 + db/bootcamp_seeds.rb | 56 +++--- .../exercise/available_for_user_test.rb | 99 ++++++++++ .../bootcamp/select_next_exercise_test.rb | 67 +------ .../commands/bootcamp/solution/create_test.rb | 2 +- test/commands/bootcamp/update_level_test.rb | 22 +-- .../api/bootcamp/solutions_controller_test.rb | 2 - .../languages/jikiscript/interpreter.test.ts | 8 +- .../languages/jikiscript/parser.test.ts | 2 +- .../languages/jikiscript/syntaxErrors.test.ts | 4 +- 83 files changed, 1436 insertions(+), 417 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 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/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 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 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..282bb3be7a --- /dev/null +++ b/app/commands/bootcamp/exercise/available_for_user.rb @@ -0,0 +1,27 @@ +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? + + # Otherwise the previous solution must be completed + previous_exercises_completed? + end + + private + delegate :project, to: :exercise + + 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.idx < exercise.idx) + end + + 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/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/commands/bootcamp/solution/create.rb b/app/commands/bootcamp/solution/create.rb index ee2a5b1018..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,8 +23,12 @@ def call end private + def existing_solution + Bootcamp::Solution.find_by(user:, exercise:) + end + 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/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/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/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/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/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/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 26a5b82fef..9f2a5babeb 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) {} @@ -13,6 +21,7 @@ class Rectangle extends Shape { public y: number, public width: number, public height: number, + public fillColor: FillColor, element: SVGElement ) { super(element) @@ -24,6 +33,7 @@ class Circle extends Shape { public cx: number, public cy: number, public radius: number, + public fillColor: FillColor, element: SVGElement ) { super(element) @@ -59,10 +69,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' } @@ -146,10 +158,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 +196,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 +222,7 @@ export default class DrawExercise extends Exercise { }) } public getTriangleAt( + _: InterpretResult, x1: number, y1: number, x2: number, @@ -232,15 +262,63 @@ export default class DrawExercise extends Exercise { }) } - public changePenColor(executionCtx: ExecutionContext, color: string) { + 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)) { + 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(_: 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, @@ -263,8 +341,9 @@ 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) return rect } @@ -286,8 +365,9 @@ 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) return circle } @@ -313,6 +393,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 } @@ -349,6 +430,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 } @@ -414,13 +496,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, @@ -430,9 +510,8 @@ export default class DrawExercise extends Exercise { offset: executionCtx.getCurrentTime(), }) }) - executionCtx.fastForward(duration) - this.shapes = [] + this.visibleShapes = [] } public setBackgroundImage(imageUrl: string) { @@ -504,5 +583,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 eb45843dce..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, @@ -37,6 +43,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..8a4d8a32c2 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() @@ -56,8 +66,9 @@ function generateExpectsForStateTests(exercise: Exercise, testData: TaskTest) { : 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, ...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..e2bf92f3f1 100644 --- a/app/javascript/components/bootcamp/types/Matchers.d.ts +++ b/app/javascript/components/bootcamp/types/Matchers.d.ts @@ -1,6 +1,8 @@ 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 b7daa5e8c6..ea7284c4c5 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 } @@ -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 } } } @@ -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( @@ -639,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 * 100) / 100 + 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 plusValue = left.value + right.value + const plusValue2DP = Math.round(plusValue * 100) / 100 + return { ...result, - value: left.value + right.value, + value: plusValue2DP, } + } if (isString(left.value) && isString(right.value)) return { ...result, @@ -667,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 * 100) / 100 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 * 100) / 100 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, @@ -938,7 +950,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 +965,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/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/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/app/models/bootcamp/user_project.rb b/app/models/bootcamp/user_project.rb index a8772b0c8c..f20ae8aa6b 100644 --- a/app/models/bootcamp/user_project.rb +++ b/app/models/bootcamp/user_project.rb @@ -39,26 +39,12 @@ 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 } if solution solution.status - elsif exercise_available?(exercise) + elsif Bootcamp::Exercise::AvailableForUser.(exercise, user) :available else :locked 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) diff --git a/app/views/bootcamp/dashboard/index.html.haml b/app/views/bootcamp/dashboard/index.html.haml index 6b4fdf5ae4..6cbb20dd8c 100644 --- a/app/views/bootcamp/dashboard/index.html.haml +++ b/app/views/bootcamp/dashboard/index.html.haml @@ -1,125 +1,63 @@ #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. - - %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'}. - - - %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. - - %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. - - - else - %section.normal - .lg-container - .flex.mb-40 - .lhs.mr-48{ class: 'w-full max-w-[800px]' } - .text-18.font-medium - Level 2 Session: - = link_to "https://youtube.com/live/eYiPZEQFf-I", "https://youtube.com/live/eYiPZEQFf-I", class: "text-linkColor font-semibold" - %hr.border-borderColor5.my-20 - %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') - - %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. - - = 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: - = link_to "Monday 13th Jan at 18:00 UTC", "https://youtube.com/live/eYiPZEQFf-I", class: "text-linkColor font-semibold" - %li.mb-4 - %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 - - .mb-32 - = render ReactComponents::Settings::BootcampAffiliateCouponForm.new("bootcamp") - - %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. + .lg-container + .flex.mb-40 + .lhs.mr-48{ class: 'w-full max-w-[780px]' } + %h1 Welcome to Level #{@level.idx}! + .level-content + = raw @level.content_html + = 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 + + - 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 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 + + .mb-32 + = render ReactComponents::Settings::BootcampAffiliateCouponForm.new("bootcamp") + + %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. 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/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/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/bootcamp_content/projects/drawing/config.json b/bootcamp_content/projects/drawing/config.json index b540545951..1914c8057c 100644 --- a/bootcamp_content/projects/drawing/config.json +++ b/bootcamp_content/projects/drawing/config.json @@ -2,5 +2,12 @@ "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", + "rainbow", + "sunset", + "sprouting-flower" + ] } 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/rainbow/config.json b/bootcamp_content/projects/drawing/exercises/rainbow/config.json new file mode 100644 index 0000000000..d25381b828 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/config.json @@ -0,0 +1,43 @@ +{ + "title": "Rainbow", + "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": "Draw the rainbow.", + "description_html": "Paint 100 beautiful rectangles", + "function": "main", + "checks": [ + { + "name": "getRectangleAt(1, 0, undefined, 100)", + "matcher": "toExist", + "error_html": "The first rectangle is missing" + }, + { + "name": "getRectangleAt(100, 0, undefined, 100)", + "matcher": "toExist", + "error_html": "The last rectangle is missing" + }, + { + "name": "checkUniqueColoredRectangles(100)", + "matcher": "toBeTrue", + "error_html": "There are not 100 different colored rectangles." + } + ] + } + ] + } + ] +} 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..311ed7795f --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/example.jiki @@ -0,0 +1,10 @@ +set x to 0 +set hue to 0 + +repeat 100 times do + change x to x + 1 + change hue to hue + 3 + + 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 new file mode 100644 index 0000000000..596d7f225d --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/introduction.md @@ -0,0 +1,28 @@ +# Rainbow + +Your task is to make a beautiful rainbow like this: + + + +The rainbow is made up of lots of bars. + +**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 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: + +- `rectangle(x, y, width, height)` +- `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 with variables or animation, watch back the video from week 2! 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..e52f9709b3 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/rainbow/task-1.md @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..80a3a6236d --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -0,0 +1,79 @@ +{ + "title": "Sprouting flower", + "description": "Make the flower sprout", + "project_type": "draw", + "level": 2, + "idx": 5, + "concepts": [], + "tests_type": "state", + "interpreter_options": { + "repeat_delay": 30 + }, + "readonly_ranges": [], + "tasks": [ + { + "name": "Draw the scene", + "tests": [ + { + "slug": "draw-scence", + "name": "Make the flower sprout.", + "description_html": "Take it one step at a time!", + "function": "main", + "checks": [ + { + "name": "getCircleAt(50, 89, 0.4)", + "matcher": "toExist", + "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 new file mode 100644 index 0000000000..e204bed951 --- /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 Variables +set stem_height to 0 +set stem_width to 0 +set stem_left to 0 +set stem_bottom to 90 + +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 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..cbb006e5b8 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/introduction.md @@ -0,0 +1,75 @@ +# Sprouting Flower + +Your task is to make a flower that grows. + +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 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. + +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 new file mode 100644 index 0000000000..4dd874fb51 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/stub.jiki @@ -0,0 +1,15 @@ +// TODO: Set your initial variables here + +repeat 60 times do + // TODO: Update your variables here + + // Sky + fill_color_hex("#ADD8E6") + rectangle(0, 0, 100, 90) + + // Ground + 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/sprouting-flower/task-1.md b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md new file mode 100644 index 0000000000..feb70f9b6f --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/task-1.md @@ -0,0 +1 @@ +Draw a beautiful rainbow! 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..b2119e6fca --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/structured-house/config.json @@ -0,0 +1,58 @@ +{ + "title": "Structured House", + "description": "Use variables instead of hard-coded values", + "project_type": "draw", + "level": 2, + "idx": 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/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json new file mode 100644 index 0000000000..926ec55662 --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -0,0 +1,53 @@ +{ + "title": "Sunset", + "description": "Make the sun set", + "project_type": "draw", + "level": 2, + "idx": 4, + "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." + }, + { + "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/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..b1648cd5ec --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/introduction.md @@ -0,0 +1,32 @@ +# 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 **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. + +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. + +## 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 new file mode 100644 index 0000000000..fdd4d7755f --- /dev/null +++ b/bootcamp_content/projects/drawing/exercises/sunset/stub.jiki @@ -0,0 +1,22 @@ +set sun_radius to 5 +set sun_cy to 10 + +repeat 100 times o + // TODO: Update the variables here. + + // 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..2069031952 --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json @@ -0,0 +1,56 @@ +{ + "title": "Rolling Ball", + "description": "Make the ball roll to the hole", + "project_type": "draw", + "level": 2, + "idx": 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(88, 75, 3)", + "matcher": "toExist", + "error_html": "The ball doesn't seem to reach the hole." + }, + { + "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 new file mode 100644 index 0000000000..4af819e876 --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/example.jiki @@ -0,0 +1,13 @@ +set x to 27 +set y to 75 +set radius to 3 + +fill_color_hex("blue") + +repeat 61 times do + clear() + + change x to x + 1 + + circle(x, y, 3) +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..c361662351 --- /dev/null +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/stub.jiki @@ -0,0 +1,17 @@ +// 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: Set your fill color + +// TODO: Change this to only repeat as many times as +// needed for the ball to reach the hole +repeat 10 times do + clear() + + // TODO: Increase the x position by 1 + + // 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..8556e87038 --- /dev/null +++ b/bootcamp_content/projects/golf/introduction.md @@ -0,0 +1,7 @@ +# Two Fer + +## Overview + +In this project, we're going to learn use animation, functions and conditonals to build some basic game mechanics. + +We'll start by animating a simple ball, then work out when it drops into the hole, and add scoring. 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 e9c586d472..a5c58c8724 100644 --- a/bootcamp_content/projects/maze/exercises/manual-solve/config.json +++ b/bootcamp_content/projects/maze/exercises/manual-solve/config.json @@ -3,6 +3,7 @@ "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 a2c55f25ae..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 @@ -2,7 +2,8 @@ "title": "Even or Odd", "description": "Determine if a number is even or odd", "concepts": ["strings-using", "conditionals"], - "level": 2, + "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 aad53410b9..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 @@ -2,7 +2,8 @@ "title": "Positive, Negative or Zero", "description": "Determine if a number is positive, negative or zero", "concepts": ["strings-using", "conditionals"], - "level": 2, + "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 5b3127e1fa..a4fea860af 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,8 @@ "title": "Rock Paper Scissors", "description": "Calculate the correct result", "concepts": ["conditionals"], - "level": 2, + "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 a86dd47f75..aed6b3a61c 100644 --- a/bootcamp_content/projects/two-fer/exercises/basic/config.json +++ b/bootcamp_content/projects/two-fer/exercises/basic/config.json @@ -2,7 +2,8 @@ "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, + "idx": 1, "tests_type": "io", "readonly_ranges": [ { "from": 1, "to": 1 }, 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/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/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/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 7ea767f749..714a75ee1d 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], @@ -54,21 +56,31 @@ def exercise_config_for(project_slug, maze wordle weather + golf ] 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| + project_config[:exercises].each do |exercise_slug| exercise_config = exercise_config_for(project_slug, exercise_slug) - project.exercises.create!( - slug: exercise_slug, - idx: idx + 1, + exercise = project.exercises.find_or_create_by!(slug: exercise_slug) do |e| + e.idx = exercise_config[:idx] + e.title = "" + e.description = "" + e.level_idx = exercise_config[:level] + end + exercise.update!( + idx: exercise_config[:idx], title: exercise_config[:title], description: exercise_config[:description], level_idx: exercise_config[:level], @@ -80,5 +92,3 @@ def exercise_config_for(project_slug, end # Bootcamp::UserProject::CreateAll.(User.find_by!(handle: 'iHiD')) - -# rubocop:enable Layout/LineLength 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..221a3f114e --- /dev/null +++ b/test/commands/bootcamp/exercise/available_for_user_test.rb @@ -0,0 +1,99 @@ +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 + + 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 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 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/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 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 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') })