diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/space_invaders/SpaceInvadersExercise.tsx b/app/javascript/components/bootcamp/SolveExercisePage/exercises/space_invaders/SpaceInvadersExercise.tsx index adae680419..cb26f998c2 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/space_invaders/SpaceInvadersExercise.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/space_invaders/SpaceInvadersExercise.tsx @@ -1,11 +1,14 @@ import React from 'react' import { Exercise } from '../Exercise' import { ExecutionContext } from '@/interpreter/executor' +import { random } from 'lodash' +import { d } from '@codemirror/legacy-modes/mode/d' type GameStatus = 'running' | 'won' | 'lost' type AlienStatus = 'alive' | 'dead' class Alien { public status: AlienStatus + public lastKilledAt?: number public constructor( public elem: HTMLElement, @@ -16,6 +19,7 @@ class Alien { this.status = 'alive' } } + export default class SpaceInvadersExercise extends Exercise { private gameStatus: GameStatus = 'running' private moveDuration = 200 @@ -30,6 +34,7 @@ export default class SpaceInvadersExercise extends Exercise { (_, idx) => this.laserStart + idx * this.laserStep ) private laserPosition = 0 + private features = { reanimation: false } public constructor() { super('space-invaders') @@ -50,6 +55,10 @@ export default class SpaceInvadersExercise extends Exercise { return { gameStatus: this.gameStatus } } + public enableReanimation() { + this.features.reanimation = true + } + private addAliens(rows) { this.aliens = rows.map((row, rowIdx) => { return row.map((type, colIdx) => { @@ -77,6 +86,86 @@ export default class SpaceInvadersExercise extends Exercise { return new Alien(alien, row, col, type) } + private killAlien( + executionCtx: ExecutionContext, + alien: Alien, + shot: HTMLElement + ) { + const deathTime = executionCtx.getCurrentTime() + this.shotDuration + + alien.status = 'dead' + alien.lastKilledAt = deathTime + ;[ + ['tl', -10, -10, -180], + ['tr', 10, -10, 180], + ['bl', -10, 10, -180], + ['br', 10, 10, 180], + ].forEach(([pos, x, y, rotate]) => { + this.addAnimation({ + targets: `#${this.view.id} #${alien.elem.id} .${pos}`, + duration: 300, + transformations: { + translateX: x, + translateY: y, + rotate: rotate, + opacity: 0, + }, + offset: deathTime, + }) + }) + this.addAnimation({ + targets: `#${this.view.id} #${shot.id}`, + duration: 1, + transformations: { opacity: 0 }, + offset: deathTime, + }) + } + + private reanimateRandomAlien(executionCtx: ExecutionContext) { + if (!this.features.reanimation) { + return + } + + const deadAliens = this.aliens + .flat() + .filter( + (alien) => + alien !== null && + alien.status === 'dead' && + alien.lastKilledAt && + alien.lastKilledAt < executionCtx.getCurrentTime() + this.shotDuration + ) + + // Skip 80% of the time + if (Math.random() > 0.2) { + return + } + + // Choose random dead alien from this.aliens + const alien = deadAliens[Math.floor(Math.random() * deadAliens.length)] + if (alien == null) { + return + } + + alien.status = 'alive' + const renamationTime = executionCtx.getCurrentTime() + + ;['tl', 'tr', 'bl', 'br'].forEach((pos) => { + this.addAnimation({ + targets: `#${this.view.id} #${alien.elem.id} .${pos}`, + duration: 1, + transformations: { translateX: 0, translateY: 0, rotate: 0 }, + offset: renamationTime, + }) + this.addAnimation({ + targets: `#${this.view.id} #${alien.elem.id} .${pos}`, + duration: 100, + transformations: { opacity: 1 }, + offset: renamationTime, + }) + }) + } + private moveLaser(executionCtx: ExecutionContext) { this.addAnimation({ targets: `#${this.view.id} .laser`, @@ -115,6 +204,13 @@ export default class SpaceInvadersExercise extends Exercise { } public shoot(executionCtx: ExecutionContext) { + if (this.lastShotAt > executionCtx.getCurrentTime() - 50) { + executionCtx.logicError( + 'Oh no! Your laser canon overheated from shooting too fast! You need to move before you can shoot a second time.' + ) + } + this.lastShotAt = executionCtx.getCurrentTime() + let targetRow = null let targetAlien: Alien | null = null this.aliens.forEach((row, rowIdx) => { @@ -167,42 +263,14 @@ export default class SpaceInvadersExercise extends Exercise { this.gameStatus = 'lost' executionCtx.updateState('gameOver', true) } else { - const alien = targetAlien as Alien - alien.status = 'dead' - ;[ - ['tl', -10, -10, -180], - ['tr', 10, -10, 180], - ['bl', -10, 10, -180], - ['br', 10, 10, 180], - ].forEach(([pos, x, y, rotate]) => { - this.addAnimation({ - targets: `#${this.view.id} #${alien.elem.id} .${pos}`, - duration: 300, - transformations: { - translateX: x, - translateY: y, - rotate: rotate, - opacity: 0, - }, - offset: executionCtx.getCurrentTime() + duration, - }) - }) - this.addAnimation({ - targets: `#${this.view.id} #${shot.id}`, - duration: 1, - transformations: { opacity: 0 }, - offset: executionCtx.getCurrentTime() + duration, - }) + this.killAlien(executionCtx, targetAlien, shot) + this.reanimateRandomAlien(executionCtx) + + // Why do we do this? executionCtx.fastForward(30) this.checkForWin(executionCtx) } - - // const target = this.aliens[3][this.laserPosition] - // target = this.aliens[0][0] - // this.laserLeft -= this.moveStep - // if (this.laserLeft < 0) { - // } } public moveLeft(executionCtx: ExecutionContext) { diff --git a/app/javascript/interpreter/frames.ts b/app/javascript/interpreter/frames.ts index afff4f24ef..f622ebebef 100644 --- a/app/javascript/interpreter/frames.ts +++ b/app/javascript/interpreter/frames.ts @@ -51,9 +51,6 @@ export function describeFrame( frame: Frame, externalFunctions: ExternalFunction[] ): string { - try { - console.log(process.env.NODE_ENV) - } catch {} try { // These need to come from the exercise. const functionDescriptions: Record = diff --git a/bootcamp_content/projects/space-invaders/exercises/scroll-and-shoot/introduction.md b/bootcamp_content/projects/space-invaders/exercises/scroll-and-shoot/introduction.md index b6d13dab4e..d26dcee9a1 100644 --- a/bootcamp_content/projects/space-invaders/exercises/scroll-and-shoot/introduction.md +++ b/bootcamp_content/projects/space-invaders/exercises/scroll-and-shoot/introduction.md @@ -8,6 +8,9 @@ You can move the laser left and right using the `move_left()` and `move_right()` As you move, you need to check whether there's an alien above you using the `is_alien_above()` function and then `shoot()` it if so. If you shoot when there's not an alien, you'll lose the game - wasting ammo is not allowed! +The laser canon easily overheats. +You need to move between shoots to keep it cool. + Once all the aliens have been shot down, you win! ### Reference