Alan Colon 7 years ago
parent
commit
bb13472473
10 changed files with 186 additions and 14 deletions
  1. 1 1
      README.md
  2. BIN
      Super Mario Bros. (World).nes
  3. 1 1
      app/index.html
  4. 60 8
      app/main.vue
  5. 1 1
      package-lock.json
  6. 4 1
      package.json
  7. 67 0
      server/game.js
  8. 21 0
      server/server.js
  9. 2 2
      webpack.config.js
  10. 29 0
      yarn.lock

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# **CHANGEME**
+# **nes-ai**
 
 ## Development
 

BIN
Super Mario Bros. (World).nes


+ 1 - 1
app/index.html

@@ -1,6 +1,6 @@
 <html>
 <head>
-  <title>CHANGEME</title>
+  <title>Super Mario Bros</title>
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
   <link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons' rel="stylesheet">
 </head>

+ 60 - 8
app/main.vue

@@ -1,21 +1,73 @@
+<script>
+const Fifo = require('fifo')
+const SCREEN_WIDTH = 256;
+const SCREEN_HEIGHT = 240;
+const BUFFER_FRAMES = 120;
+const SAMPLE_REDUCTION = 4;
+
+export default {
+  mounted() {
+    const buffer = new Fifo()
+    const url = new URL('game', location.href)
+    url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
+    this.ws = new WebSocket(url)
+    this.ws.binaryType = 'arraybuffer'
+    const canvas = this.$refs.canvas
+    const context = canvas.getContext('2d')
+    const imageData = context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
+    const buf = new ArrayBuffer(imageData.data.length);
+    // Get the canvas buffer in 8bit and 32bit
+    const buf8 = new Uint8ClampedArray(buf);
+    const buf32 = new Uint32Array(buf);
+
+    let timer
+
+    this.ws.addEventListener('message', msg => {
+      buffer.push(msg.data)
+      if (buffer.length > BUFFER_FRAMES && !timer) {
+        timer = setInterval(renderFrame, (1000 / 60))
+      }
+    })
+
+    const renderFrame = () => {
+      const data = buffer.shift()
+      if (data) {
+        const palette = new Uint32Array(data, 0, 256 * 4)
+        const pixels = new Uint8Array(data, 256 * 4, SCREEN_WIDTH * SCREEN_HEIGHT)
+        for (let i = 0; i < pixels.length; i++) {
+          buf32[i] = 0xff000000 | palette[pixels[i]]
+        }
+        imageData.data.set(buf8)
+        context.putImageData(imageData, 0, 0)
+      } else {
+        clearInterval(timer)
+        timer = null
+      }
+    }
+
+    
+  },
+  destroyed() {
+    this.ws.close()
+  }
+}
+</script>
+
 <template>
     <v-app>
       <v-content>
         <v-toolbar color="primary">
           <v-toolbar-side-icon />
-          <v-toolbar-title>Change Me</v-toolbar-title>
+          <v-toolbar-title>Super Mario Bros</v-toolbar-title>
         </v-toolbar>
-        <v-container>Hello world</v-container>
+        <v-container>
+          <canvas ref="canvas" width="256" height="240" />
+
+        </v-container>
       </v-content>
     </v-app>
 </template>
 
-<script>
-export default {
-
-}
-</script>
-
 <style>
 
 </style>

+ 1 - 1
package-lock.json

@@ -1,5 +1,5 @@
 {
-  "name": "**CHANGEME**",
+  "name": "nes-ai",
   "version": "1.0.0",
   "lockfileVersion": 1,
   "requires": true,

+ 4 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "**CHANGEME**",
+  "name": "nes-ai",
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
@@ -49,9 +49,12 @@
     "es6-string-html-template": "^1.0.2",
     "express": "^4.16.4",
     "express-ws": "^4.0.0",
+    "fifo": "^2.3.0",
     "file-loader": "^3.0.1",
+    "jsnes": "^1.1.0",
     "material-icons": "^0.3.0",
     "morgan": "^1.9.1",
+    "move-terminal-cursor": "^0.0.4",
     "stylus": "^0.54.5",
     "stylus-loader": "^3.0.2",
     "text-loader": "^0.0.1",

+ 67 - 0
server/game.js

@@ -0,0 +1,67 @@
+const SCREEN_WIDTH = 256;
+const SCREEN_HEIGHT = 240;
+const SAMPLE_REDUCTION = 4;
+
+const EventEmitter = require('events')
+const fs = require('fs')
+const jsnes = require('jsnes')
+const move = require('move-terminal-cursor')
+
+const chunk = (a, l) => Array(Math.ceil(a.length / l)).fill().map((_, i) => (a.slice ? a.slice(i * l, i * l + l) : a.substr(i * l, i * l + 1)))
+const sample = (a, n) => Array(Math.floor(a.length / n)).fill().map((_, i) => a[i * n])
+
+class Game extends EventEmitter {
+  constructor() {
+    super()
+    const rom = fs.readFileSync('./Super Mario Bros. (World).nes', 'binary')
+    this.nes = new jsnes.NES({
+      onFrame: this.onFrame.bind(this)
+    })
+    this.nes.loadROM(rom)
+    this.frameCount = 0
+    this.packetBuffer = new ArrayBuffer(256 * 4 + SCREEN_WIDTH * SCREEN_HEIGHT)
+    this.paletteBuffer = new Uint32Array(this.packetBuffer)
+    this.pixelBuffer = new Uint8ClampedArray(this.packetBuffer, 256 * 4)
+    this.frameBuffer = new ArrayBuffer(SCREEN_WIDTH * SCREEN_HEIGHT * 4)
+    this.screen32 = new Uint32Array(this.frameBuffer)
+    this.nes.ppu.buffer = this.screen32
+    this.frameBytes = new Uint8ClampedArray(this.frameBuffer)
+  }
+
+  onFrame(frameBuffer) {
+//    if (this.frameCount++ % SAMPLE_REDUCTION) {
+      const colorMap = new Map()
+      for (let i = 0; i < 256; i++) {
+        this.paletteBuffer[i] = this.nes.ppu.palTable.curTable[i]
+        colorMap.set(this.nes.ppu.palTable.curTable[i], i)
+      }
+      for (let i = 0; i < this.screen32.length; i++) {
+        this.pixelBuffer[i] = colorMap.get(this.screen32[i])
+      }
+      this.emit('frame', this.packetBuffer)
+//    }
+    
+
+    // if (true) {//(frameCount++ % 32 === 0) {
+    //   //move('toPos', {row: 1, col: 1})
+    //   const lines = sample(chunk(frameBuffer, SCREEN_WIDTH), SAMPLE_REDUCTION)
+    //   .map(line => sample(line, SAMPLE_REDUCTION))
+    //   //console.log(lines.map(l => Buffer.from(l).toString('hex')).join('\n'))
+
+    //   // console.log('\n\n\n' + chunk(Buffer.from(frameBuffer).toString('hex'), SCREEN_WIDTH * 2).join('\n'))
+    // }
+  }
+
+  start() {
+    if (!this.timer) this.timer = setInterval(() => this.nes.frame(), 1000 / 60)
+  }
+
+  stop() {
+    if (this.timer) {
+      clearInterval(this.timer)
+      this.timer = null
+    }
+  }
+}
+
+module.exports = Game

+ 21 - 0
server/server.js

@@ -1,4 +1,25 @@
 const app = require('./app')
+const Game = require('./game')
+
+game = new Game()
+game.start()
+
+app.ws('/game', (ws, req) => {
+  const send = (frame) => {
+    try {
+      ws.send(frame)
+    } catch(err) {
+      console.warn(err)
+      teardown()
+    }
+  }
+  const teardown = () => {
+    console.log('Detaching')
+    game.off('frame', send)
+  }
+  game.on('frame', send)
+  ws.on('close', () => teardown)
+})
 
 app.listen().catch((err) => {
   console.log(err.toString())

+ 2 - 2
webpack.config.js

@@ -48,8 +48,8 @@ module.exports = {
       templateParameters: { }
     }),
     new WebpackPwaManifest({
-      name: 'CHANGEME',
-      short_name: 'changeme',
+      name: 'NES AI',
+      short_name: 'nes-ai',
       description: 'This is an installable app.',
       background_color: '#ffffff',
       icons: [

+ 29 - 0
yarn.lock

@@ -1167,6 +1167,13 @@ continuable-cache@^0.3.1:
   resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f"
   integrity sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=
 
+control-sequence-introducer@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/control-sequence-introducer/-/control-sequence-introducer-0.0.2.tgz#14ecaf9eadca271632fa29dfb5be4a0893735465"
+  integrity sha1-FOyvnq3KJxYy+inftb5KCJNzVGU=
+  dependencies:
+    terminal-escape-char "*"
+
 convert-source-map@^1.1.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
@@ -1977,6 +1984,11 @@ faye-websocket@~0.11.1:
   dependencies:
     websocket-driver ">=0.5.1"
 
+fifo@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/fifo/-/fifo-2.3.0.tgz#182de8dd0632aa47cf6816d9bc4323317d5259dc"
+  integrity sha1-GC3o3QYyqkfPaBbZvEMjMX1SWdw=
+
 figgy-pudding@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -2992,6 +3004,11 @@ jsesc@~0.5.0:
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
+jsnes@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/jsnes/-/jsnes-1.1.0.tgz#25ff850d473a6cc17176e03daca6cd95c877c56f"
+  integrity sha512-E0UJ68aiOoC3beU/I3Zq61SRK6wz1re3ddzURnkn5bv489GYUI0vh++YOQ3Mz/7GtyaYFXgm7P+kXpfIqHOheg==
+
 json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@@ -3515,6 +3532,13 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     run-queue "^1.0.3"
 
+move-terminal-cursor@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/move-terminal-cursor/-/move-terminal-cursor-0.0.4.tgz#b0ec17f7ecb1da7e095181422eaf64bc04becc08"
+  integrity sha1-sOwX9+yx2n4JUYFCLq9kvAS+zAg=
+  dependencies:
+    control-sequence-introducer "0.0.2"
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -5221,6 +5245,11 @@ tar@^4:
     safe-buffer "^5.1.2"
     yallist "^3.0.2"
 
+terminal-escape-char@*:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/terminal-escape-char/-/terminal-escape-char-0.0.4.tgz#593cd964acca6735f701a57da7665c70b8f122b0"
+  integrity sha1-WTzZZKzKZzX3AaV9p2ZccLjxIrA=
+
 terser-webpack-plugin@^1.1.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9"