From 66bab68e246809bd74962690b7bdbbb95be2ca9f Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 3 Aug 2024 15:55:29 -0700 Subject: [PATCH] simultaneous labs --- src/components/snake/canvas.ts | 141 +++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 58 deletions(-) diff --git a/src/components/snake/canvas.ts b/src/components/snake/canvas.ts index 6efbdd1..78bbf3c 100644 --- a/src/components/snake/canvas.ts +++ b/src/components/snake/canvas.ts @@ -1,4 +1,4 @@ -import { Engine, Keys, randint, UI, Vec2, vec2 } from './game-engine'; +import { Engine, randint, UI, Vec2, vec2 } from './game-engine'; import { SnakeBrain } from './snake-brain'; import { SGSHashSet, Snake, SnakeGameState, SnakeGameStateWithHistory } from './types'; @@ -10,8 +10,27 @@ const CENTER_Y = BOARD_SIZE / 2; export const BOARD_SQUARES = BOARD_SIZE / SQUARE_SIZE; +interface SnakeGameTrainerLab { + id: number; + brain: SnakeBrain; + state: SnakeGameStateWithHistory; +} + +interface SnakeGameTrainerState { + labs: SnakeGameTrainerLab[]; +} + // general functions ----------------------------------------------------------- +function getStartSnakeGameState(): SnakeGameStateWithHistory { + const dead = false; + const snake = [vec2(1, 1), vec2(2, 1), vec2(3, 1)]; + const apple = getRandApplePos(); + const history = new SGSHashSet(); + + return { dead, snake, apple, history }; +} + function shallowCopySGS(state: SnakeGameState): SnakeGameState { return { dead: state.dead, @@ -28,62 +47,58 @@ function getSnakeNextSquare(snake: Snake, dir: Vec2) { return snake[snake.length - 1]!.add(dir); } -function getStartSnakeGameState(): SnakeGameStateWithHistory { - const dead = false; - const snake = [vec2(1, 1), vec2(2, 1), vec2(3, 1)]; - const apple = getRandApplePos(); - const history = new SGSHashSet(); - - return { dead, snake, apple, history }; -} - // control code ---------------------------------------------------------------- export default function runCanvas(canvas: HTMLCanvasElement) { const ui = new UI(canvas); - const keys = new Keys(); const engine = new Engine({ updateDelay: 50 }); // game logic -------------------------------------------------------------- - const brain = SnakeBrain.fromRandom({ hiddenLayerNodes: 12 }); - const state = getStartSnakeGameState(); + const trainer: SnakeGameTrainerState = { + labs: Array.from({ length: 4 }).map((_, idx) => ({ + id: idx + 1, + state: getStartSnakeGameState(), + brain: SnakeBrain.fromRandom({ hiddenLayerNodes: 12 }), + })), + }; function update() { - const { dead, snake, apple, history } = state; + for (const lab of trainer.labs) { + const { state, brain } = lab; + const { dead, snake, apple, history } = state; - if (dead) return; + if (dead) continue; - // kill cycling snakes - if (history.has(state)) { - state.dead = true; - return; + // kill cycling snakes + if (history.has(state)) { + state.dead = true; + continue; + } + history.add(shallowCopySGS(state)); + + // perform ai + const dir = brain.think(state); + + // NOTE: brain.think handles out-of-bounds/tail intersect checking when it identifies + // valid move options. it will return 'dead' if there are no valid moves + if (dir === 'dead') { + state.dead = true; + continue; + } + + const nextHead = getSnakeNextSquare(snake, dir); + + // check for snake hitting apple + if (nextHead.eq(apple)) { + state.apple = getRandApplePos(); + } else { + snake.shift(); + } + + // move the snake + snake.push(nextHead); } - history.add(shallowCopySGS(state)); - - // perform ai - const dir = brain.think(state); - - // NOTE: brain.think handles out-of-bounds/tail intersect checking when it identifies - // valid move options. it will return 'dead' if there are no valid moves - if (dir === 'dead') { - state.dead = true; - return; - } - - const nextHead = getSnakeNextSquare(snake, dir); - - // check for snake hitting apple - if (nextHead.eq(apple)) { - state.apple = getRandApplePos(); - } else { - snake.shift(); - } - - // move the snake - snake.push(nextHead); - - keys.update(); } // rendering --------------------------------------------------------------- @@ -93,23 +108,34 @@ export default function runCanvas(canvas: HTMLCanvasElement) { } function render() { - const { dead, snake, apple } = state; - ui.setFillStyle('#333333'); ui.clearCanvas(); - ui.setFillStyle('#277edb'); - for (const square of snake.slice(0, snake.length - 1)) { - drawSquare(square); + for (const lab of trainer.labs) { + const { state, brain } = lab; + const { dead, snake, apple } = state; + + // tail + ui.setFillStyle('#277edb'); + for (const square of snake.slice(0, snake.length - 1)) { + drawSquare(square); + } + + // head + if (dead) { + ui.setFillStyle('#ff0000'); + } else { + ui.setFillStyle('#27b4db'); + } + drawSquare(snake[snake.length - 1]!); + + // apple + ui.setFillStyle('#e01851'); + drawSquare(apple); } - ui.setFillStyle('#29d17a'); - drawSquare(snake[snake.length - 1]!); - ui.setFillStyle('#e01851'); - drawSquare(apple); - - if (dead) { - const text = 'You Died'; + if (trainer.labs.findIndex(l => !l.state.dead) === -1) { + const text = 'All Died'; ui.setFont('40px sans-serif'); const m = ui.measureText(text); @@ -122,10 +148,9 @@ export default function runCanvas(canvas: HTMLCanvasElement) { ui.setFillStyle('#cc0000'); ui.setTextAlign('center'); ui.setTextBaseline('middle'); - ui.fillText('You Died', BOARD_SIZE / 2, BOARD_SIZE / 2); + ui.fillText(text, BOARD_SIZE / 2, BOARD_SIZE / 2); } } - keys.bindKeys(); engine.run(update, render); }