{"id":18853149,"url":"https://github.com/luxedo/spacewar-almost-from-scratch","last_synced_at":"2025-04-14T10:23:15.742Z","repository":{"id":97349379,"uuid":"70358678","full_name":"luxedo/spacewar-almost-from-scratch","owner":"luxedo","description":" This is an attempt of reproducing the game spacewar! using modern programming languages","archived":false,"fork":false,"pushed_at":"2024-01-14T12:40:57.000Z","size":1190,"stargazers_count":9,"open_issues_count":0,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-27T23:41:46.417Z","etag":null,"topics":["game","game-development","hackathon","javascript-game","old-games","ship","spacewar"],"latest_commit_sha":null,"homepage":"https://luxedo.github.io/spacewar-almost-from-scratch/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/luxedo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-10-08T21:35:10.000Z","updated_at":"2024-03-24T01:11:45.000Z","dependencies_parsed_at":"2024-11-08T03:43:17.892Z","dependency_job_id":"e098a8b9-47ca-48b7-9eb3-ea81adae7e8e","html_url":"https://github.com/luxedo/spacewar-almost-from-scratch","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luxedo%2Fspacewar-almost-from-scratch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luxedo%2Fspacewar-almost-from-scratch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luxedo%2Fspacewar-almost-from-scratch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luxedo%2Fspacewar-almost-from-scratch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luxedo","download_url":"https://codeload.github.com/luxedo/spacewar-almost-from-scratch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248860432,"owners_count":21173435,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["game","game-development","hackathon","javascript-game","old-games","ship","spacewar"],"created_at":"2024-11-08T03:43:06.683Z","updated_at":"2025-04-14T10:23:15.720Z","avatar_url":"https://github.com/luxedo.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SPACEWAR ALMOST FROM SCRATCH\nThis is an attempt of reproducing the game [spacewar!](https://en.wikipedia.org/wiki/Spacewar!) using modern programming languages. The idea is to track the progress and time each stage of development in this document. If possible, I want to finish this project in under 24h.\n\n#### Check it out [here](https://luxedo.github.io/spacewar-almost-from-scratch/)\n\nThe game is based in `html5 canvas`, `CSS` and `ES6 javascript`. No extra libraries or engines will be used.\n\nSince I've already worked on a project to reproduce [pong-almost-from-scratch](https://luxedo.github.io/pong-almost-from-scratch/), I'll be using much of it in here.\n\n## GOALS\n* ~~Add `LICENSE.md` and `README.md`~~\n* ~~Create `html/canvas` base~~\n* ~~Host somewhere~~\n* ~~Create the gameloop~~\n* ~~Create rendering functions~~\n* ~~Design board~~\n* ~~Create `Ship` class~~\n* ~~Create `Shot` class~~\n* ~~Create `blackhole` sprite~~\n* ~~Implement gravity mechanics~~\n* ~~Implement collision mechanics~~\n  * ~~Collision with the black hole~~\n  * ~~Collision with the borders~~\n  * ~~Collision between Ships~~\n  * ~~Collision between Shots~~\n  * ~~Collision Ship-Shots~~\n* ~~Create game over screen~~\n* ~~Create start screen~~\n* ~~Create credits screen~~\n* ~~Create enemy AI~~\n* ~~Add sounds~~\n* ~~Improve webpage~~\n* ~~Get playtesters feedback~~\n* ~~List requests/bugs~~\n* ~~Fix requests/bugs~~\n* ~~Finished!~~\n\n## Progress Reports\n00:00 - Start! This project started October 6th, 2016 at 17:50 (BRT). I'll be timing each step and will be placing the time it took from the beginning along with the achieved goal.\n\n## 00:10 - LICENSE and README\nThis project is under a [GNU GPL3](https://www.gnu.org/licenses/gpl-3.0.en.html) license. Have fun! :wink:\n\n## 00:15 - Host somewhere\nFor now, I'll be hosting it in [github pages](https://pages.github.com/) since it's easy deploy. Check it out [here](https://luxedo.github.io/spacewar-almost-from-scratch/)\n\n## 00:40 - `html/canvas` base + gameloop\nI'll be borrowing the gameloop and the base from my other project [pong-almost-from-scratch](https://luxedo.github.io/pong-almost-from-scratch/).\nThe favicon was made with GIMP.\n\n![favicon](report-assets/favicon.png \"favicon\")\n\nAnd here is the webpage!!\n#### Hello World Again\n![hello world](report-assets/hello-world.png \"hello world\")\n\n## 04:00 - Create rendering functions\nTo stay true to the game origins, I'll draw only with vectors using `ctx.lineTo` method. One function was created that receives an `Array` of coordinates and draws the lines on screen.\n\n```javascript\nfunction drawArray(array, width=1, color=\"#FFF\") {\n  // setup style\n  Game.context.lineWidth = width;\n  Game.context.strokeStyle = color;\n  // go to starting position\n  Game.context.beginPath();\n  Game.context.moveTo(...array[0]);\n  array.shift();\n  // draw line\n  array.forEach((value) =\u003e Game.context.lineTo(...value));\n  Game.context.stroke();\n}\n```\n\nIt would be really boring to draw characters made of vectors by hand, luckly I've found a set of characters made of vectors, called [Hershey Vector Font](http://paulbourke.net/dataformats/hershey/). This character set was invented in 1967, 5 years after `Spacewar`.\n\n![Hershey Vector Font](report-assets/simplex1.gif \"Hershey Vector Font\")\n\nIn this set, each letter in the alphabeth is a series of characters that corresponds to coordinates. Eg: `M=-5`, `N=-4`, `O=-3` ...\n\n```javascript\nalphabeth = {\n  \"A\": \"9MWRMNV RRMVV RPSTS\",\n  \"B\": \"16MWOMOV ROMSMUNUPSQ ROQSQURUUSVOV\",\n  \"C\": \"11MXVNTMRMPNOPOSPURVTVVU\",\n  \"D\": \"12MWOMOV ROMRMTNUPUSTURVOV\",\n  \"E\": \"12MWOMOV ROMUM ROQSQ ROVUV\",\n  \"F\": \"9MVOMOV ROMUM ROQSQ\"\n  ...\n}\n```\nI then made a parser for this character set to transform it's language into the vectors to be drawn.\nWith that, I created a function that receives `strings` and writes them in the screen. Also two more functions to make it easier to draw stuff: `drawCircle`, `drawPoint`.\n\n![drawing functions](report-assets/drawing-functions.png \"drawing functions\")\n\n## 04:20 - Board Design\nThe board in this game is just some stars in the background. I made the play area round, so I'm drawing a circle to show that. The stars are generated randomly in each round.\n\n![board design](report-assets/board-design.png \"board design\")\n\n## 08:20 - `Ship` class\nThe ship class is a sprite that has it's own `draw` and `update` methods. They're called in the gameloop. The key bindings were partially done and easy to implement, since I'm using a helper object `Key` in the gameloop. This class ended up bigger than I expected, and I still need to implement somethings that are not ready yet.\n\nThe vectors were hand drawn and based on the original game. I changed a little the sprite of `player2`. In the original game it has a slimmer profile, then it's a little harder to hit it.\n```javascript\nlet player1Vectors = [\n  [[8, 0], [1, 2], [-1, 2], [-8, 1], [-8, -1], [-1, -2], [1, -2], [8, 0]],\n  [[-1,  2], [-6,  4], [-8,  4], [-5,  1.5]],\n  [[-1, -2], [-6, -4], [-8, -4], [-5, -1.5]]\n]\n```\n\n![ship sprites](report-assets/ship-sprites.png \"ship sprites\")\n\nIt was quite tricky to rotate all the vectors in the sprite around a center. For that I created a method `updateRotation` that have an optional argument `angle` to set the property in the object and perform a rotation around the center of the sprite.\n\nWhen the thrusters (`keyDown`) are activated, one vector shoots out of the rear of the ships with a random length for each frame. This effect ended up very similar to the original one.\n\n![ship in the game](report-assets/ship-in-the-game.gif \"ship in the game\")\n\n## 10:40 - `Shot` class\nThe `Shot` class is much simpler than `Ship`. It has just to start somewhere, move in the correct direction and end after a certain distance. It is created when the player presses the `keyUp` in the `Ship` class.\n\n![shot](report-assets/shot.gif \"shot\")\n\n## 11:00 - `Blackhole` class\nThe blackhole is a simple sprite in the middle of the screen. It generates two random numbers every frame: One for the length of the vector and one for the angle.\n\n![blackhole](report-assets/blackhole.gif \"blackhole\")\n\n## 11:20 - Add gravity mechanics\nA function was created to handle the gravity mechanics. It's called in the gameloop's update method passing the `Ship` instances, the center of pull an the gravity as arguments.\n```javascript\nfunction addGravity(element, cx, cy, gravity) {\n  // F = Gm1m2/r^2 = gravity/r^2\n  let dx = element.x-cx;\n  let dy = element.y-cy;\n  let F = gravity/(Math.pow(dx, 2)+Math.pow(dy, 2));\n  let angle = Math.atan2(dx, dy)\n  let fx = -F*Math.cos(angle);\n  let fy = -F*Math.sin(angle);\n  element.speedX += (fx\u003cMAXACCEL?fx:MAXACCEL);\n  element.speedY += (fy\u003cMAXACCEL?fy:MAXACCEL);\n}\n```\n\n![gravity](report-assets/gravity.gif \"gravity\")\n\n## Implement collision mechanics\n### 11:35 - Collision with the black hole\nThe black hole spawns any Ship that reaches its position to a random position in the board with `speed=0`. This is checked in the object's update method.\n### 12:20 - Collision with the borders\nThe game board wraps around itself, making it infinite. So, whenever a player or shot reaches the borders, they're spawned back in the other side of the board. This also happens for the `Shot` class. The collision is checked in the object's update method.\n### 14:00 - Collision between `Ships`\nWhen the two players collide, there is an explosion and the game should end. This check is made in the gameloop's update method.\nThe collision is calculated using [Separating Axis Theorem](https://en.wikipedia.org/wiki/Hyperplane_separation_theorem). It ended up in a function with 15 constants and a single if statement to tell whether the ships have collided.\n\n```javascripthttps://github.com/Sanscripter\ncheckCollision = function(sprite1, sprite2) {\n  // Limits of the sprite\n  const p1c = sprite1.corners;\n  const p2c = sprite2.corners;\n  // Translate sprites to make p1c[0] the origin\n  const p1cT = sprite1.corners.map(val =\u003e [val[0]-p1c[0][0], val[1]-p1c[0][1]]);\n  const p2cT = sprite2.corners.map(val =\u003e [val[0]-p1c[0][0], val[1]-p1c[0][1]]);\n  // Calculate the rotation to align the p1 bounding box\n  const angle = Math.atan2(p1cT[2][1], p1cT[2][0]);\n  // Rotate vetcors to align\n  const p1cTR = p1cT.map(val =\u003e versusScreen.rotateVector(val, angle));\n  const p2cTR = p2cT.map(val =\u003e versusScreen.rotateVector(val, angle));\n  // Calculate extreme points of the bounding boxes\n  const p1left = Math.min(...p1cTR.map(value =\u003e value[0]))\n  const p1right = Math.max(...p1cTR.map(value =\u003e value[0]))\n  const p1top = Math.min(...p1cTR.map(value =\u003e value[1]))\n  const p1bottom = Math.max(...p1cTR.map(value =\u003e value[1]))\n  const p2left = Math.min(...p2cTR.map(value =\u003e value[0]))\n  const p2right = Math.max(...p2cTR.map(value =\u003e value[0]))\n  const p2top = Math.min(...p2cTR.map(value =\u003e value[1]))\n  const p2bottom = Math.max(...p2cTR.map(value =\u003e value[1]))\n  // Check if shadows overlap in both axes\n  if (p2left \u003c p1right \u0026\u0026 p1left \u003c p2right \u0026\u0026 p2top \u003c p1bottom \u0026\u0026 p1top \u003c p2bottom) return true;\n  return false;\n}\n```\n\nI also created an `explode` method in the `Ship` class so when they collide, it shows a satisfying explosion. The explosion have 4 frames of dots generated randomly with varying radius.\n\n![explosion](report-assets/explosion.gif \"explosion\")\n\n### 15:00 - Collision between `Shots`\nThe shots uses the same check Collision function and two nested loops to iterate over the shots array. To destroy the shot after a collision, I set the property `distance` to be close to it's maximum distance, then it's automatically destroyed in the `Ship` update method.\n\n### 15:10 - Collision between `Ships` and `Shots`\nSince all the functions methods for collision were ready, it was quite easy to calculate the collision between `Ships` and `Shots`. It took 5 minutes to do so.\n\n## 16:10 - Game over screen\nWhen one ship is destroyed, the player goes to the `Game Over screen`. This check is made in the end of the update method in the gameloop, if any player has the property `dead` then the `Game.changeState` method is called after a timeout leading to the `Game Over screen`.\n\n![game over](report-assets/game-over.gif \"game over\")\n\nThe code for the cursor is basically an extended `Ship` class that has the functionalities of `Cursor` class in my [PONG](https://luxedo.github.io/pong-almost-from-scratch/) game.\n\n## 17:00 - Start screen/Credits screen\nThe `Start screen` copies a lot of code from `Game Over screen`, it just replaces some text and the screen that is called when pressing enter\n\n![start screen](report-assets/start-screen.gif \"start screen\")\n\nThe credits screen is even easier to draw, since it does not contains moving parts.\n\n![credits](report-assets/credits.png \"credits\")\n\n## 18:30 - Enemy AI\nI didn't want to make a very complicated AI, otherwise it would take too much time. I came up with a simple solution. The enemy always tries to face `player1`, it fires it's weapon when the player is closer than 200px of distance and pointing in the right direction, and it will fire it's thrusters when the player shot more than two shots or if the player is too far.\n```javascript\n// basic vectors\nlet p1dx = Game.player1.x-Game.player2.x;\nlet p1dy = Game.player1.y-Game.player2.y;\nlet p1r = Math.hypot(p1dx, p1dy);\n// player1 angle in relation to player2\nlet angleDelta = (Math.atan2(p1dy, p1dx)-Game.player2.rotation)%(Math.PI*2)\n// Adjust angles and limit to ROTATION_SPEED\nangleDelta = (angleDelta\u003cMath.PI?angleDelta:angleDelta-2*Math.PI)\nangleDelta = (angleDelta\u003c-Math.PI?angleDelta+2*Math.PI:angleDelta)\nangleDelta = (Math.abs(angleDelta)\u003cROTATION_SPEED?angleDelta:Math.sign(angleDelta)*ROTATION_SPEED);\n// Apply actions\nGame.player2.updateRotation(Game.player2.rotation+angleDelta);\nif (p1r \u003c SHOT_DISTANCE*1.5 \u0026\u0026 angleDelta\u003cROTATION_SPEED) {\n  Game.player2.fire();\n}\nif (Game.player1.shots.length \u003e 2 || (p1r\u003eSHOT_DISTANCE*2 \u0026\u0026 angleDelta\u003cROTATION_SPEED)) {\n  Game.player2.fireThrusters()\n}\n```\n\n## 19:10 - Sounds\n\nThe original game had no sounds, I added some 8-bit sounds just to make it more interesting. The sounds are some free `wav` files I've found at [Freesound.org](https://www.freesound.org).\n\nI used `HTMLMediaElements` to play the sounds. With the aid of one function, I just pass some parameters and can call the sound anywhere in the game.\n\n```javascript\n// sound factory\nfunction soundFactory(audio, start, stop) {\n  return () =\u003e {\n    if (audio.paused) {\n      audio.play();\n      setTimeout(()=\u003e{\n        audio.pause();\n        audio.currentTime = start;\n      }, stop);\n    }\n  }\n}\nsoundX = new Audio(soundXURL);\nGame.playSoundX = soundFactory(soundX, 0, 500);\nGame.playSoundX();\n```\nThanks to `meroleroman7`, `Shaun105`, `jeremysykes` and `ProjectsU012` for the sound assets.\n\n## 19:30 - Improve webpage\nI added some [Open Graph](http://ogp.me/) tags to make it more pleasing when sharing. Also, changed the base color to green, more similar to the old CRT displays. I can't think of anything else to change right now.\n\n## 20:30 - List requests/bugs\n* ~~Emphasis in the control keys in startScreen - `00jknight`, `Baino`, `Maria`~~\n* ~~Make the game more dynamic - `00jknight`, `Baino`, `Maria`~~\n* ~~Make the AI more unpredictable in the beginning - `Thiago Harry`~~\n\n## 21:00 - Fix requests/bugs\nNow the game is much more dynamic and fun. Since the thrusters are 5 times more potent, I added a maximum speed to stop players from getting uncontrollably fast. The shots are fired in a smaller interval and the gravity is stronger. I also changed the layout of the start screen to make the key indications more visible.\n\n## 21:15 - Finished!\nWell, that was fun. Again.\nThere's still 2:45h left to complete 24h, so I'll be making a single patch if there's enough feedback. The lessons that I liked were:\n* There was a character set from the 60's (Hershey Vector Font) based on vectors, nice.\n* I had to draw the pointy brackets in Hershey Vector Font's notation.\n* The thing in the center of the board should be a star, not a black hole.\n* Separating Axis Theorem was surprisingly easy to implement.\n* Working with vectors were surprisingly hard to implement, I should have prepared more linear algebra functions instead of making lots of transformations manually.\n* The physics were very simple, but it took me some time to understand `Math.atan2`, it always did the opposite of what I wanted.\n\nThanks again to `meroleroman7`, `Shaun105`, `jeremysykes` and `ProjectsU012` for the sound assets\n\nThanks to the playtesters `00jknight`, `Baino`, `Maria` and `Thiago Harry`.\n\nThanks for the support of `Kaska`, `rgk` and `8Observer8`, `Igor Georgiev` and `StorytellerVR`.\n\nThanks to `Lee Reilly` for the PR fixing a typo\n\nThats it for now.\n#### Bye\n\n## 24:00 - Update patch\nI added an `evade` method in the `Ship` class that rotates to a random position (-90° to 90°) and fires it's thrusters for half second. This prevents the player from killing the enemy AI easily in the beginning of the round.\n\nAlso, as `00jknight` pointed out, I changed the commands of `fire torpedos` and `fire thrusters`.\n\nThats it for ever.\n#### Bye\n\n## 70128:00+ - Controller support\n\nSpecial thanks to [Sanscripter](https://github.com/Sanscripter) for adding controller support.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluxedo%2Fspacewar-almost-from-scratch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluxedo%2Fspacewar-almost-from-scratch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluxedo%2Fspacewar-almost-from-scratch/lists"}