-
-
Save cfreshman/715c15667a25849df5b6444db85de77b to your computer and use it in GitHub Desktop.
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset=utf-8> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>snake ultra hydra</title> | |
| <link rel="icon" href="https://i.guim.co.uk/img/media/993cc4a2107b870f78d1228874906ad9646fb204/0_144_2160_1296/master/2160.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=708569290282f1d206da9d72d5c2046c"> | |
| <style>body{display:flex;flex-direction:column;margin:.5em;font-family:system-ui}</style> | |
| <style> | |
| #game-canvas { | |
| image-rendering: pixelated; | |
| /* 30rem by default, but shrink to window height w/ .5em margin */ | |
| width: min(30rem, calc(100vh - 1em)); | |
| height: min(30rem, calc(100vh - 1em)); | |
| } | |
| </style> | |
| <script> | |
| // COMMON CODE | |
| // truthy: interpret the value as a boolean | |
| const truthy = x => Boolean(x) | |
| // V2: a two-dimensional vector value | |
| // - add: add to another vector | |
| // - equals: check if this vector equals another vector (e.g. same position) | |
| class V2 { | |
| constructor(x_or_vector, y) { | |
| let x, vector | |
| if (typeof x_or_vector === 'number') { | |
| x = x_or_vector | |
| } else { | |
| vector = x_or_vector | |
| } | |
| if (vector) { | |
| // allow copying another vector | |
| this.x = vector.x | |
| this.y = vector.y | |
| } else { | |
| this.x = x | |
| this.y = y | |
| } | |
| } | |
| add(other) { | |
| return new V2(this.x + other.x, this.y + other.y) | |
| } | |
| equals(other) { | |
| return this.x === other.x && this.y === other.y | |
| } | |
| } | |
| // ListNode: linked list implementation | |
| // store reference to first node to access entire list | |
| class ListNode { | |
| constructor(value, next) { | |
| this.value = value | |
| this.next = next | |
| } | |
| map(func) { | |
| let curr = this | |
| const results = [] | |
| while (curr) { | |
| results.push(func(curr.value)) | |
| curr = curr.next | |
| } | |
| return results | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div> | |
| WASD or ARROW KEYS to move. any other key to reset | |
| </div> | |
| <canvas id="game-canvas"></canvas> | |
| <div> | |
| (DESKTOP ONLY) | |
| </div> | |
| <script> | |
| // board size 20x20 pixels | |
| const SIZE = 20 | |
| // cardinal directions the snake can move in | |
| const DIRS = { | |
| RIGHT: new V2(1, 0), | |
| UP: new V2(0, -1), | |
| LEFT: new V2(-1, 0), | |
| DOWN: new V2(0, 1), | |
| } | |
| // set the canvas to SIZE width and height | |
| const canvas = document.querySelector('#game-canvas') | |
| canvas.width = canvas.height = SIZE | |
| // get the canvas graphics context for drawing the game pixels | |
| const ctx = canvas.getContext('2d') | |
| // game colors | |
| const COLORS = { | |
| BACKGROUND: '#000000', | |
| SNAKE: '#34eb7d', | |
| APPLE: '#ff0000', | |
| } | |
| // Pixel: a drawable position | |
| // - draw: draw a colored square at this position | |
| // - randomize: move pixel to random position | |
| class Pixel extends V2 { | |
| constructor(x, y, color) { | |
| super(x, y) | |
| this.color = color | |
| } | |
| draw(ctx, color) { | |
| ctx.fillStyle = color | |
| ctx.fillRect(this.x, this.y, 1, 1) | |
| return this | |
| } | |
| randomize() { | |
| this.x = Math.floor(Math.random() * SIZE) | |
| this.y = Math.floor(Math.random() * SIZE) | |
| return this | |
| } | |
| } | |
| // snake behaviors/functions | |
| function snake_move(head, dir, do_grow=false) { | |
| // moving the snake: | |
| // body nodes don't actually change | |
| // a new head is added to the front (in input direction) | |
| // if the snake isn't growing, the last node is removed | |
| const new_head = new ListNode(new Pixel(head.value.x + dir.x, head.value.y + dir.y, head.color), head) | |
| if (!do_grow) { | |
| let curr = head | |
| while (curr.next.next) curr = curr.next | |
| curr.next = undefined | |
| } | |
| return new_head | |
| } | |
| function snake_intersects(head, position) { | |
| // true if any body parts are placed at the position | |
| return head ? head.map(part => part.equals(position)).some(truthy) : false | |
| } | |
| // === GAME STATE === | |
| // start the snake heading towards the right | |
| let head = new ListNode(new Pixel(SIZE/4, SIZE/2, COLORS.SNAKE)) | |
| let direction = DIRS.RIGHT | |
| // how many new segments to grow | |
| let growth = 2 | |
| // place the apple at a random position | |
| const apple = new Pixel(0, 0, COLORS.APPLE).randomize() | |
| // is the game over | |
| let gameover = false | |
| // last is used to avoid letting the snake do a 180 | |
| let last = direction | |
| // === GAME LOOP === | |
| function loop() { | |
| if (gameover) return | |
| // we check against 'last' in the input handler to prevent a 180 | |
| // 'last' allows us to update direction immediately but always check the new direction against the actual last direction | |
| last = direction | |
| // move the snake | |
| // gameover if snake intersects itself or went out of bounds | |
| head = snake_move(head, direction, growth) | |
| if (growth > 0) growth-- | |
| if (snake_intersects(head.next, head.value) | |
| || head.value.x < 0 || head.value.x > SIZE-1 | |
| || head.value.y < 0 || head.value.y > SIZE-1) { | |
| gameover = true | |
| return | |
| } | |
| // check if the head is at the same position as the apple | |
| // if so, grow the body and move the apple to a new location | |
| if (head.value.equals(apple)) { | |
| growth += 1 | |
| do { | |
| apple.randomize() | |
| } while (snake_intersects(head, apple)) | |
| } | |
| // draw the scene: | |
| // 1) cover the entire scene with black | |
| // 2) fill each snake segment with green | |
| // 3) fill the apple position with red | |
| ctx.fillStyle = COLORS.BACKGROUND | |
| ctx.fillRect(0, 0, SIZE, SIZE) | |
| head.map(part => part.draw(ctx, COLORS.SNAKE)) | |
| apple.draw(ctx, COLORS.APPLE) | |
| } | |
| // === GAME INPUT === | |
| // listen for pressed keys to re-direct the snake | |
| window.addEventListener('keydown', e => { | |
| switch (e.key) { | |
| case 'w': case 'ArrowUp': | |
| if (last !== DIRS.DOWN) direction = DIRS.UP | |
| break | |
| case 'a': case 'ArrowLeft': | |
| if (last !== DIRS.RIGHT) direction = DIRS.LEFT | |
| break | |
| case 's': case 'ArrowDown': | |
| if (last !== DIRS.UP) direction = DIRS.DOWN | |
| break | |
| case 'd': case 'ArrowRight': | |
| if (last !== DIRS.LEFT) direction = DIRS.RIGHT | |
| break | |
| default: | |
| if (gameover) location.reload() | |
| break | |
| } | |
| }) | |
| // run loop() 10 times per second (every 100ms) (snake moves 10 cells/sec) | |
| setInterval(loop, 100) | |
| </script> | |
| </body> | |
| </html> |
steermouse has completely transformed how I navigate software solutions on this website. The detailed guides, clean downloads, and responsive user layout make exploring complex utility tools an absolute breeze. I consistently rely on this resource for accurate tech tutorials and seamless navigation, making it a favorite bookmark in my digital workflow.
I am truly impressed by how thoroughly flashingtech is discussed on your platform. The article delivers clear, reliable information that helps readers navigate technical setup steps with confidence. Your website continues to be a trusted hub for modern technology guides.
As someone who regularly explores advanced software configurations and technical utilities, I am thoroughly impressed by your website's comprehensive coverage of exp soundboard. The depth of information provided alongside seamless navigation makes it a go-to destination for reliable tech insights. Thank you for maintaining such a professional, helpful, and high quality digital resource for users worldwide.
Amazing tool that makes creative designs simple and enjoyable for everyone. Minecraft font generator provides unique styles, easy customization, and impressive results that enhance gaming projects, artwork, and personal creations with a professional appearance. It is truly helpful for fans.