Browse Source

Using hitboxes

Alan Colon 6 years ago
parent
commit
d6a7aba10d
11 changed files with 798 additions and 104 deletions
  1. 58 0
      app/blocks.txt
  2. 223 30
      app/main.vue
  3. 79 0
      common/field.js
  4. 98 0
      common/nes.js
  5. 255 0
      common/smb.js
  6. 47 0
      common/util.js
  7. 3 1
      package.json
  8. 0 67
      server/game.js
  9. 17 6
      server/server.js
  10. 4 0
      webpack.config.js
  11. 14 0
      yarn.lock

+ 58 - 0
app/blocks.txt

@@ -0,0 +1,58 @@
+0000 1000   0x08 solid vine
+
+0x69 water Ground
+0x22 coral
+0xc3 coin
+# Solid    Solid
+#          | Breakable
+#          | | Magic
+           | | |
+0001 0000  x . x  0x10 Secret Pipe Rim Left
+0001 0001  x . x  0x11 Secret Pipe Rim Right
+0001 0010  x . .  0x12 Pipe Rim Left
+0001 0011  x . .  0x13 Pipe Rim Right
+0001 0100  x . .  0x14 Pipe Shaft Left
+0001 0101  x . .  0x15 Pipe Shaft Right
+0001 0110  x . .  0x16 Plant Platform Left
+0001 0111  x . .  0x17 Plant Platform middle
+0001 1000  x . .  0x18 Plant platform right
+0001 1001  x . .  0x19 Mushroom Left
+0001 1010  x . .  0x1a Mushroom middle
+0001 1011  x . .  0x1b Mushroom right
+0001 1100  x . x  0x1c Pipe Rim Top
+0001 1101  x . x  0x1d Pipe Shaft Top
+0001 1110  x . .  0x1e Pipe Joint Top
+0001 1111  x . .  0x1f Pipe Rim Bottom
+0010 0000  x . .  0x20 Pipe Shaft Bottom
+0010 0001  x . .  0x21 Pipe Joint Bottom
+0010 0011  x . x  0x23 Block being bonked
+0010 0100  . . x  0x24 Flag pole top
+0010 0101  . . x  0x25 Flag pole
+0010 0110  . . x  0x26 Vine
+0101 0001  x x x  0x51 Red? Bricks at rest
+0101 0010  x x x  0x52 Bricks at rest
+0101 0010  x . .  0x52 End of level
+0101 0100  x . .  0x54 Ground Block
+0101 0110  x x x  0x56 Bricks hiding vine
+0101 0111  x x x  0x57 Bricks hiding star
+0101 1000  x x x  0x58 Red? Bricks hiding coins
+0101 1010  x x x  0x5a Bricks hiding mushroom or flower
+0101 1011  x x x  0x5b Bricks hiding vine
+0101 1101  x x x  0x5d Bricks hiding coins
+0101 1111  . x x  0x5f hidden coin (KAIZO!!)
+0110 0000  . x x  0x60 Hidden 1-up
+0110 0001  x . .  0x61 Diamond Block
+0110 0010  x . .  0x62 Castle block
+0110 0011  x . .  0x63 Bridge
+0110 0100  x . .  0x64 Bullet Bill barrell
+0110 0101  x . .  0x65 Bullet Bill Base
+0110 0110  x . .  0x66 Bullet Bill Shaft
+0110 0111  x . x  0x67 Spring top
+0110 1000  x . x  0x68 Spring bottom
+1000 1000  x . .  0x88 Cloud
+1000 1001  x . .  0x89 bowser bridge
+1100 0000  x . x  0xc0 Question block with coin
+1100 0001  x . x  0xc1 Question block with mushroom
+1100 0010  . . x  0xc2 Coin
+1100 0100  x . .  0xc4 Spent block
+1100 0101  . . x  0xc5 Axe

+ 223 - 30
app/main.vue

@@ -4,51 +4,214 @@ const SCREEN_WIDTH = 256;
 const SCREEN_HEIGHT = 240;
 const BUFFER_FRAMES = 120;
 const SAMPLE_REDUCTION = 4;
-
+const MAX_BUFFER = 0; 1200;
+const WORKING_MEMORY = 0x800;
+const { cartesianToPolar:c2p, polarToCartesian:p2c, findMemory } = require('../common/util')
+window.findMemory = findMemory
 export default {
+  data: () => ({
+    gameState: {},
+    gameStateString: '',
+    memory: new Uint8Array(WORKING_MEMORY),
+    watch: [
+      0x0086
+    ]
+  }),
+  beforeDestroy() {
+    console.log('beforeDestroy()')
+    this.stop = true
+    this.ws.close()
+  },
   mounted() {
+    console.log('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);
+    const connect = () => {
+      this.ws = new WebSocket(url)
+      this.ws.binaryType = 'arraybuffer'
+      this.ws.addEventListener('message', msg => {
+        if (typeof msg.data === 'string') {
+          this.gameState = JSON.parse(msg.data)
+          this.gameStateString = JSON.stringify(this.gameState.enemyVision, null, 2)
+            .replace(/\{[^\{\}\[\]]*\}/g, x => x.replace(/\s+/g, ' '))
+            .replace(/\[[^\{\}\[\]]*\]/g, x => x.replace(/\s+/g, ' '))
+
+        } else {
+          //buffer.push(msg.data)
+          //if (buffer.length === MAX_BUFFER) buffer.shift()
+          // if (!timer) {
+          //   timer = true
+            this.renderFrame(msg.data)
+            this.renderDebug(this.gameState)
+          //}
+        }
+      })
+      this.ws.addEventListener('close', () => {
+        if (!this.stop) setTimeout(connect, 1000)
+      })
+    }
+    connect()
+    this.canvas = this.$refs.canvas
+    this.context = this.canvas.getContext('2d')
+    this.imageData = this.context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
+    this.memoryCanvas = this.$refs.memoryCanvas
+    this.memoryContext = this.memoryCanvas.getContext('2d')
+    this.memoryImageData = this.memoryContext.getImageData(0, 0, this.memoryCanvas.getAttribute('width'), this.memoryCanvas.getAttribute('height'))
+    this.buf = new ArrayBuffer(this.imageData.data.length);
     // Get the canvas buffer in 8bit and 32bit
-    const buf8 = new Uint8ClampedArray(buf);
-    const buf32 = new Uint32Array(buf);
+    this.buf8 = new Uint8ClampedArray(this.buf);
+    this.buf32 = new Uint32Array(this.buf);
+
+    this.timer = null
+
+    this.keyMap = {
+      a: /*BUTTON_B*/ 1,
+      s: /*BUTTON_A*/ 0,
+      '\\': /*BUTTON_SELECT*/ 2,
+      Enter: /*BUTTON_START*/ 3,
+      ArrowUp: /*BUTTON_UP*/ 4,
+      ArrowDown: /*BUTTON_DOWN*/ 5,
+      ArrowLeft: /*BUTTON_LEFT*/ 6,
+      ArrowRight: /*BUTTON_RIGHT*/ 7
+    }
+
+    this.key = (ev, pressed) => {
+      if (this.keyMap.hasOwnProperty(ev.key)) {
+        const button = this.keyMap[ev.key]
+        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+          this.ws.send(JSON.stringify({
+            type: 'button',
+            button,
+            pressed
+          }))
+        } else {
+          console.warn('Not connected')
+        }
+      }
+    }
+
+    document.addEventListener('keydown', (ev) => this.key(ev, true))
+    document.addEventListener('keyup', (ev) => this.key(ev, false))
+  },
+  methods: {
+    renderDebug(data) {
+      window.data = data
+      this.context.save()
+      // this.context.translate(8, 8)
+      this.context.translate(data.x, data.y + 32)
+      // this.context.beginPath()
+      // this.context.fillStyle = '#080'
+      // //this.context.fillRect(data.hitbox.x1, data.hitbox.y1, data.hitbox.x2 - data.hitbox.x1, data.hitbox.y2 - data.hitbox.y1)
+      // this.context.arc(0, 0, 8, 0, 2 * Math.PI)
+      // this.context.fill()
+
+      // data.enemies.forEach(enemy => {
+      //   if (enemy && enemy.hitbox.y2 > enemy.hitbox.y1) {
+      //     this.context.beginPath()
+      //     this.context.fillStyle = '#800'
+      //     this.context.arc(enemy.x, enemy.y, 8, 0, 2 * Math.PI)
+      //     //this.context.fillRect(enemy.hitbox.x1, enemy.hitbox.y1, enemy.hitbox.x2 - enemy.hitbox.x1, enemy.hitbox.y2 - enemy.hitbox.y1)
+      //     this.context.fill()
+      //   }
+      // })
 
-    let timer
+      const renderVision = (collection, color) => {
+        const arc = (Math.PI * 2 / data.slices)
+        this.context.save()
+        this.context.translate((data.hitbox.x2 - data.hitbox.x1) / 2, (data.hitbox.y2 - data.hitbox.y1) / -2)
+        for (let i = 0; i < data.slices; i++) {
+          const r = collection[i]
+          const {x, y} = p2c({
+            r: (1 - r) * 30,
+            t: arc * i + arc / 2 - Math.PI
+          })
+          this.context.beginPath()
+          this.context.fillStyle = color
+          this.context.arc(x, y, 2, 0, 2 * Math.PI)
+          //this.context.fillRect(enemy.hitbox.x1, enemy.hitbox.y1, enemy.hitbox.x2 - enemy.hitbox.x1, enemy.hitbox.y2 - enemy.hitbox.y1)
+          this.context.fill()
 
-    this.ws.addEventListener('message', msg => {
-      buffer.push(msg.data)
-      if (buffer.length > BUFFER_FRAMES && !timer) {
-        timer = setInterval(renderFrame, (1000 / 60))
+        }
+        this.context.restore()
       }
-    })
+      renderVision(data.enemyVision, 'red')
+      renderVision(data.solidVision, 'blue')
+      renderVision(data.breakableVision, 'yellow')
+      renderVision(data.magicVision, 'green')
 
-    const renderFrame = () => {
-      const data = buffer.shift()
+      const renderHitboxes = (collection, color) => {
+        this.context.strokeStyle = color
+        this.context.beginPath()
+        collection.forEach(entity => {
+          if (entity) this.context.rect(entity.hitbox.x1, entity.hitbox.y1, entity.hitbox.x2 - entity.hitbox.x1, entity.hitbox.y2 - entity.hitbox.y1)
+        })
+        this.context.stroke()
+      }
+
+      renderHitboxes(this.gameState.blocks, 'blue')      
+      renderHitboxes(data.enemies, 'red')
+      renderHitboxes([data], 'yellow')
+      // this.gameState.blocks.forEach(({x, y, v}) => {
+
+      //   // v = v.toString(16).padStart(2)
+      //   this.context.rect(x, y, 16, 16)
+//        for (let xo = -512; xo <= 512; xo += 512) {
+        // const xo = 0
+        //   this.context.rect(x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y, 16, 16)
+        //   this.context.fillStyle = 'black'
+        //   this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo + 1, y * 16 + 0.5 - data.y + 10)
+        //   this.context.fillStyle = 'black'
+        //   this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo - 1, y * 16 + 0.5 - data.y + 7)
+        //   this.context.fillStyle = 'black'
+        //   this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo + 1, y * 16 + 0.5 - data.y + 7)
+        //   this.context.fillStyle = 'black'
+        //   this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo - 1, y * 16 + 0.5 - data.y + 10)
+        //   this.context.fillStyle = '#aaaa00'
+        //   this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y + 9)
+//        }
+      // })
+      // this.context.stroke();
+      // this.context.strokeStyle = null
+      this.context.restore()
+    },
+    renderFrame(data) {
+      //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)
+        const memory = new Uint8Array(data, 256 * 4 + SCREEN_WIDTH * SCREEN_HEIGHT, WORKING_MEMORY)
+        Object.assign(this.memory, memory)
+        window.memory = this.memory
         for (let i = 0; i < pixels.length; i++) {
-          buf32[i] = 0xff000000 | palette[pixels[i]]
+          this.buf32[i] = 0xff000000 | palette[pixels[i]]
+        }
+        this.imageData.data.set(this.buf8)
+        for (let i = 32 * 40; i < memory.length; i++) {
+          const m = memory[i]
+          for (let x = 0; x < 4; x++) {
+            this.memoryImageData.data[i * 4 + x - 32 * 40] = m
+          }
         }
-        imageData.data.set(buf8)
-        context.putImageData(imageData, 0, 0)
-      } else {
-        clearInterval(timer)
-        timer = null
+        this.context.putImageData(this.imageData, 0, 0)
+        this.memoryContext.putImageData(this.memoryImageData, 0, 0)
       }
-    }
 
-    
+
+      //const ms = Math.max(-0.8 * buffer.length + 160, 0)
+      //timer = setTimeout(renderFrame, ms)
+      //requestAnimationFrame(renderFrame)
+      /* https://www.calculator.net/slope-calculator.html?type=1&x11=0&y11=500&x12=100&y12=16&x=12&y=7
+        l = buffer.length
+        l = 0, sleep 500
+        l = 100, sleep 1000/60
+        l = 200, sleep more
+      */
+    }
   },
   destroyed() {
-    this.ws.close()
+    console.log('destroyed()')
+    if (this.ws) this.ws.close()
   }
 }
 </script>
@@ -61,13 +224,43 @@ export default {
           <v-toolbar-title>Super Mario Bros</v-toolbar-title>
         </v-toolbar>
         <v-container>
-          <canvas ref="canvas" width="256" height="240" />
-
+          <canvas style="zoom: 2;" ref="canvas" width="256" height="240" />
+          <canvas style="zoom: 4;" ref="memoryCanvas" width="16" height="128" />
+          <pre>{{gameStateString}}</pre>
+          <ul>
+            <!-- "0x0069", "0x0075", "0x0093", "0x0109", "0x0113", "0x0114", "0x0115", "0x0118", "0x0119", "0x0130", "0x0163", "0x0164", "0x0168", "0x0184", "0x0185", "0x0186", "0x0187", "0x0190", "0x0191", "0x0202", "0x0215", "0x0216", "0x0239", "0x0246", "0x0269", "0x0281", "0x0282", "0x0283", "0x0289", "0x0637", "0x0641", "0x0642", "0x0645", "0x0646", "0x0791", "0x0987", "0x0988", "0x0989", "0x0996", "0x0997", "0x1002", "0x1003", "0x1008", "0x1050", "0x1052", "0x1084", "0x1178", "0x1180", "0x1181", "0x1182", "0x1183", "0x1212", "0x1213", "0x1214", "0x1215", "0x1220", "0x1221", "0x1222", "0x1223", "0x1286", "0x1302", "0x1318", "0x1334", "0x1350", "0x1366", "0x1382", "0x1398", "0x1563", "0x1564", "0x1565", "0x1578", "0x1579", "0x1580", "0x1581", "0x1593", "0x1594", "0x1595", "0x1596", "0x1597", "0x1611", "0x1612", "0x1613", "0x1623", "0x1624", "0x1627", "0x1628", "0x1629", "0x1636", "0x1638", "0x1639", "0x1640", "0x1643", "0x1653", "0x1654", "0x1655", "0x1656", "0x1659", "0x1675", "0x1676", "0x1677" -->
+            <li> x, y: {{gameState.x}}, {{gameState.y}}</li>
+            <li v-for="(key, index) in watch" :key="index">
+              {{key.toString(16)}}: {{memory[key]}}
+            </li>
+            <li>0x006d (Level x): {{gameState.levelX}}</li>
+            <li>0x0491 (Enemy collision): {{memory[0x0491]}}</li>
+            <li>0x04AC: {{memory[0x04AC]}}</li>
+            <li>0x04AC: {{memory[0x04AC]}}</li>
+            <li>0x0750: {{memory[0x0750]}}</li>
+          </ul>
+          <!-- <div style="position: relative;">
+            <div v-for="block in gameState.blocks"
+              :key="gameState.blocks.indexOf(block)"
+              :style="{
+                display: 'block',
+                width: '4px',
+                height: '4px',
+                left: (block.x * 4) + 'px',
+                top: (block.y * 4) + 'px',
+                position: 'absolute',
+                backgroundColor: 'brown'
+              }"
+            >
+            </div>
+          </div> -->
         </v-container>
       </v-content>
     </v-app>
 </template>
 
-<style>
-
+<style scoped>
+canvas {
+  image-rendering: pixelated;
+}
 </style>

+ 79 - 0
common/field.js

@@ -0,0 +1,79 @@
+class Field {
+  constructor(...sizes) {
+    if (Array.isArray(sizes[0])) {
+      this.data = sizes.shift()
+    }
+    this.n = sizes.length
+    this.sizes = sizes
+    this.data = []
+    const g = (p, n) => {
+      if (n < sizes.length) {
+        const end = n === sizes.length - 1
+        for (let i = 0; i < sizes[n]; i++) {
+
+          p[i] = end ? 0 : []
+          g(p[i], n + 1)
+        }
+      }
+    }
+    g(this.data, 0)
+  }
+  * indices() {
+    const self = this
+    const g = function*(p, n) {
+      if (n === self.sizes.length) yield p
+      const size = self.sizes[n]
+      for (let i = 0; i < size; i++) {
+        yield* g([...p, i], n + 1)
+      }
+    }
+    yield* g([], 0)
+  }
+
+  get(...indices) {
+    const g = (o, n) => {
+      if (n === indices.length || n === this.sizes.length) return o
+      const i = indices[n]
+      const f = Math.floor(i)
+      const r = i - f
+      if (r) {
+        // Return weighted value
+        const a = g(o[f], n + 1)
+        const b = g(o[f + 1], n + 1)
+        return a * (1 - r) + b * r
+      } else {
+        return g(o[f], n + 1)
+      }
+    }
+    return g(this.data, 0)
+  }
+
+  set(value, ...indices) {
+    const g = (o, w, n) => {
+      const end = (n === indices.length - 1 || n === this.sizes.length - 1)
+      const i = indices[n]
+      const f = Math.floor(i)
+      const r = i - f
+      if (r) {
+        if (end) {
+          // o[f] = 1, v = .3, w = 1 ... expected result o[f] = .3
+          // value - o[f] = -.7 ... * w = 1.. yep
+          o[f] = o[f] + (1 - r) * w * (value - o[f])
+          o[f + 1] = o[f + 1] + r * w * (value - o[f + 1])
+        } else {
+          g(o[f], (1 - r) * w, n + 1)
+          g(o[f + 1], r * w, n + 1)
+        }
+      } else {
+        if (end) {
+          o[f] = o[f] + w * (value - o[f])
+        } else {
+          g(o[f], w, n + 1)
+        }
+      }
+    }
+    return g(this.data, 1, 0)
+  }
+}
+
+module.exports = Field

+ 98 - 0
common/nes.js

@@ -0,0 +1,98 @@
+const SCREEN_WIDTH = 256;
+const SCREEN_HEIGHT = 240;
+const WORKING_MEMORY = 0x800;
+const SAMPLE_REDUCTION = 4;
+
+const jsnes = require('jsnes')
+const { Subject } = require('rxjs')
+
+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 NES {
+  constructor(rom) {
+    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 + WORKING_MEMORY)
+    this.paletteBuffer = new Uint32Array(this.packetBuffer)
+    this.pixelBuffer = new Uint8ClampedArray(this.packetBuffer, 256 * 4)
+    this.copyMemoryBuffer = new Uint8Array(this.packetBuffer, 256 * 4 + SCREEN_WIDTH * SCREEN_HEIGHT, WORKING_MEMORY)
+    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)
+    this.frames = new Subject()
+  }
+
+  command(data) {
+    if (data.type === 'button') {
+      const player = data.player || 1
+      this.nes.controllers[player].state[data.button] = data.pressed ? 0x41 : 0x40
+    }
+  }
+
+
+  onFrame(frameBuffer) {
+    //console.log('frames.next' + Date.now())
+    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])
+    }
+    for (let i = 0; i < WORKING_MEMORY; i++) {
+      this.copyMemoryBuffer[i] = this.nes.cpu.mem[i]
+    }
+    this.frames.next(this.packetBuffer)
+  }
+
+  genie(code) {
+    const map = { A: 0x0, P: 0x1, Z: 0x2, L: 0x3, G: 0x4, I: 0x5, T: 0x6, Y: 0x7, E: 0x8, O: 0x9, X: 0xA, U: 0xB, K: 0xC, S: 0xD, V: 0xE, N: 0xF }
+    const [n0, n1, n2, n3, n4, n5, n6, n7] = code.split('').map(c => map[c])
+    let address, data, compare
+    
+    address = 0x8000 + 
+    ((n3 & 7) << 12)
+    | ((n5 & 7) << 8) | ((n4 & 8) << 8)
+    | ((n2 & 7) << 4) | ((n1 & 8) << 4)
+    |  (n4 & 7)       |  (n3 & 8)
+    
+    if (code.length === 6) {
+      data = ((n1 & 7) << 4) | ((n0 & 8) << 4) | (n0 & 7)       |  (n5 & 8)
+      compare = null
+    } else {
+      data = ((n1 & 7) << 4) | ((n0 & 8) << 4) | (n0 & 7)       |  (n7 & 8)
+      compare = ((n7 & 7) << 4) | ((n6 & 8) << 4) | (n6 & 7)       |  (n5 & 8)
+    }
+    this.hack(address, data, compare)
+  }
+
+  hack(address, data, compare = null) {
+    let val = this.nes.cpu.mem[address]
+    Object.defineProperty(this.nes.cpu.mem, address, {
+      get: () => compare === null || val === compare
+        ? data
+        : val,
+      set: v => (val = v)
+    })
+  }
+
+  start() {
+    if (!this.timer) this.timer = setInterval(() => this.nes.frame(), 1000 / 60)
+  }
+
+  stop() {
+    if (this.timer) {
+      clearInterval(this.timer)
+      this.timer = null
+    }
+  }
+
+}
+
+module.exports = NES

+ 255 - 0
common/smb.js

@@ -0,0 +1,255 @@
+const { Controller } = require('jsnes')
+const NES = require('./nes')
+const { cartesianToPolar:c2p, cartesianToPolar:p2c } = require('./util')
+const smbRom = require('../Super Mario Bros. (World).nes')
+
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
+const sign = (b, n) => n >> (b - 1) ? n - (1 << b) : n
+
+class SMB extends NES {
+  constructor() {
+    super(smbRom)
+    // Starting level hack:
+    // this.hack(0x9087, 0x07, 0xFF)
+    // this.hack(0x9089, 0x5F, 0xA0)
+    // this.hack(0x908A, 0x07, 0x03)
+
+    // Infinite clock hack:
+    this.hack(0x0787, 0x01) // Stop clock
+
+    // Invincible hack:
+    this.hack(0x079F, 0x10) // Invincible
+
+
+    this.genie('ANTOSA')
+    this.genie('SXIOPO')
+    // this.genie('PAKOPPAA')
+    // setInterval(async () => {
+    //   this.nes.buttonDown(1, Controller.BUTTON_SELECT)
+    //   await sleep(200)
+    //   this.nes.buttonUp(1, Controller.BUTTON_SELECT)
+    // }, 1000)
+  }
+  gameState() {
+    const mem = this.nes.cpu.mem
+
+    const stageAMemory = mem.slice(0x500,0x500 + 16*13)
+    const stageBMemory = mem.slice(0x500 + 16*13, 0x500 + 16*13*2)
+    const blocks = []
+
+    const levelX = (mem[0x006d] * 256 + mem[0x0086])
+    const levelY = mem[0x03b8]
+    const screenX = mem[0x03ad]
+
+    const setBlock = (x, y, v) => {
+      if (v) {
+        /*
+          this.context.rect(x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y, 16, 16)
+        */
+        const block = {x: (x * 16 - levelX % 512), y: y * 16 - levelY, v}
+        if (block.x > 256) block.x -= 512
+        if (block.x < -256) block.x += 512
+        block.hitbox = {
+          x1: block.x,
+          y1: block.y,
+          x2: block.x + 16,
+          y2: block.y + 16
+        }
+        blocks.push(block)
+      }
+    }
+    for (let i = 0; i < 16 * 13; i++) {
+      const x = i % 16
+      const y = Math.floor(i / 16)
+      const a = stageAMemory[i]
+      const b = stageBMemory[i]
+      setBlock(x, y, a)
+      setBlock(x + 16, y, b)
+    }
+
+    const hitbox = a => {
+      const hb = {
+        x1: mem[a] - screenX,
+        y1: mem[a + 1] - levelY - 32,
+        x2: mem[a + 2] - screenX,
+        y2: mem[a + 3] - levelY - 32,
+        // x: (mem[a] + mem[a + 2]) / 2,
+        // y: (mem[a + 1] + mem[a + 3]) / 2,
+        // w: mem[a + 2] - mem[a],
+        // h: mem[a + 3] - mem[a + 1]
+      }
+      if (hb.y2 < hb.y1) hb.y1 -= 256
+      return hb
+    }
+    const int16 = a => mem[a + 25] * 256 + mem[a]
+    const state = {
+      test: mem[0x2000],
+      screenX: (mem[0x006d] * 256 + mem[0x0086]) - mem[0x03ad],
+      player: {
+        hitbox: hitbox(0x04ac),
+        x: mem[0x006d] * 256 + mem[0x0086],
+        y: mem[0x00b5] * 256 + mem[0x00CE],
+        x2: mem[0x0086 - 1],
+        y2: mem[0x00CE + (0x0086 - 0x006d)],
+        facing: sign(2, mem[0x0033]),
+        speedX: sign(8, mem[0x0057]),
+        speedY: sign(8, mem[0x009f]),
+        screenY: mem[0x03b8],
+        screenX: mem[0x03ad],
+      },
+      enemies: Array(5).fill().map((_, i) => ({
+        idx: i,
+        hitbox: hitbox(0x04b0 + 4 * i),
+        type: mem[0x0016 + i],
+        facing: sign(2, mem[0x0046 + i]),
+        speed: sign(8, mem[0x0058 + i]),
+        x: mem[0x0058 + i] * 256 + mem[0x006e + i],
+        y: mem[0x00b6 + i] * 256 + mem[0x00cf + i],
+        screenY: mem[0x03b9 + i],
+        screenX: mem[0x03ae + i]
+      }))
+    }
+
+    //TODO: Convert enemy x,y to polar coordinates r,d, then slice the pie,
+    // filter enemies by slice.start > r > slice.end, then the value for that
+    // slice is the filtered enemies.map(d).reduce(min)
+    const summary = {
+      levelX: (mem[0x006d] * 256 + mem[0x0086]),
+      x: state.player.screenX,
+      y: state.player.screenY,
+      hitbox: state.player.hitbox,
+      // TODO: Assign solids, breakables, magics
+      solids: blocks.filter(({v}) => blockTypes[v][0]),
+      breakables: blocks.filter(({v}) => blockTypes[v][1]),
+      magics: blocks.filter(({v}) => blockTypes[v][2]),
+      enemies: state.enemies.map(enemy => enemy.hitbox.x2 === 0 ? null : ({
+        x: enemy.hitbox.x1 - state.player.hitbox.x1,
+        y: enemy.hitbox.y1 - state.player.hitbox.y1,
+        hitbox: enemy.hitbox
+      })),
+      blocks
+    }
+
+    const vision = (list) => {
+      const slices = 16
+      const visibility = 128
+      const arc = (Math.PI * 2) / slices
+      const views = new Array(slices).fill(0)
+      const check = (coords) => {
+        const p = c2p(coords)
+        const i = Math.floor((p.t + Math.PI) / arc)
+        views[i] = Math.min(Math.max(views[i], 1 - p.r / visibility), 1)
+      }
+      list.forEach(entity => {
+        if (entity) {
+          check(entity)
+          if (entity.hitbox) {
+            check({x: entity.hitbox.x1, y: entity.hitbox.y1})
+            check({x: entity.hitbox.x2, y: entity.hitbox.y1})
+            check({x: entity.hitbox.x1, y: entity.hitbox.y2})
+            check({x: entity.hitbox.x2, y: entity.hitbox.y2})
+          }
+        }
+      })
+      return views
+    }
+
+    summary.enemyVision = vision(summary.enemies)
+    summary.solidVision = vision(summary.solids)
+    summary.breakableVision = vision(summary.breakables)
+    summary.magicVision = vision(summary.magics)
+    // TODO: populate solidVision, breakableVision, magicVision
+    
+
+    Object.assign(summary, {
+      slices: 16,
+      visibility: 256
+    })
+    return summary
+
+    return {
+      player: {
+        hitbox: hitbox(0x04ac),
+        facing: mem[0x0033],
+        speedX: mem[0x0057],
+        speedY: mem[0x009f],
+        levelX: mem[0x006d],
+        levelY: mem[0x00b5] * mem[0x00ce],
+        screenY: mem[0x03b8],
+        screenX: mem[0x03ad]
+
+      },
+      enemies: Array(5).fill().map((_, i) => ({
+        idx: i,
+        hitbox: hitbox(0x04b0 + (4 * i)),
+        type: mem[0x0016 + i],
+        facing: mem[0x0046 + i],
+        speed: mem[0x0058 + i],
+        levelX: mem[0x006e + i],
+        levelY: mem[0x00b6 + i] * mem[0x00cf + i],
+        screenY: mem[0x03b9 + i],
+        screenX: mem[0x03ae + i]
+      })),
+      levelLayoutAddress: mem[0x00e7],
+      level: mem[0x0760]
+    }
+  }
+}
+
+const blockTypes = Object.assign(Array(256).fill([0,0,0]), {
+  //     Solid
+  //     |  Breakable
+  //     |  |  Magic
+  //     |  |  |
+  0x10: [1, 0, 1], // Secret Pipe Rim Left
+  0x11: [1, 0, 1], // Secret Pipe Rim Right
+  0x12: [1, 0, 0], // Pipe Rim Left
+  0x13: [1, 0, 0], // Pipe Rim Right
+  0x14: [1, 0, 0], // Pipe Shaft Left
+  0x15: [1, 0, 0], // Pipe Shaft Right
+  0x16: [1, 0, 0], // Plant Platform Left
+  0x17: [1, 0, 0], // Plant Platform middle
+  0x18: [1, 0, 0], // Plant platform right
+  0x19: [1, 0, 0], // Mushroom Left
+  0x1a: [1, 0, 0], // Mushroom middle
+  0x1b: [1, 0, 0], // Mushroom right
+  0x1c: [1, 0, 1], // Pipe Rim Top
+  0x1d: [1, 0, 1], // Pipe Shaft Top
+  0x1e: [1, 0, 0], // Pipe Joint Top
+  0x1f: [1, 0, 0], // Pipe Rim Bottom
+  0x20: [1, 0, 0], // Pipe Shaft Bottom
+  0x21: [1, 0, 0], // Pipe Joint Bottom
+  0x23: [1, 0, 1], // Block being bonked
+  0x24: [0, 0, 1], // Flag pole top
+  0x25: [0, 0, 1], // Flag pole
+  0x26: [0, 0, 1], // Vine
+  0x51: [1, 1, 1], // Red? Bricks at rest
+  0x52: [1, 1, 1], // Bricks at rest
+  0x52: [1, 0, 0], // End of level
+  0x54: [1, 0, 0], // Ground Block
+  0x56: [1, 1, 1], // Bricks hiding vine
+  0x57: [1, 1, 1], // Bricks hiding star
+  0x58: [1, 1, 1], // Red? Bricks hiding coins
+  0x5a: [1, 1, 1], // Bricks hiding mushroom or flower
+  0x5b: [1, 1, 1], // Bricks hiding vine
+  0x5d: [1, 1, 1], // Bricks hiding coins
+  0x5f: [0, 1, 1], // hidden coin (KAIZO!!)
+  0x60: [0, 1, 1], // Hidden 1-up
+  0x61: [1, 0, 0], // Diamond Block
+  0x62: [1, 0, 0], // Castle block
+  0x63: [1, 0, 0], // Bridge
+  0x64: [1, 0, 0], // Bullet Bill barrell
+  0x65: [1, 0, 0], // Bullet Bill Base
+  0x66: [1, 0, 0], // Bullet Bill Shaft
+  0x67: [1, 0, 1], // Spring top
+  0x68: [1, 0, 1], // Spring bottom
+  0x88: [1, 0, 0], // Cloud
+  0x89: [1, 0, 0], // bowser bridge
+  0xc0: [1, 0, 1], // Question block with coin
+  0xc1: [1, 0, 1], // Question block with mushroom
+  0xc2: [0, 0, 1], // Coin
+  0xc4: [1, 0, 0], // Spent block
+  0xc5: [0, 0, 1], // Axe
+})
+
+module.exports = SMB

+ 47 - 0
common/util.js

@@ -0,0 +1,47 @@
+const cartesianToPolar = ({x, y}) => ({r: Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), t: Math.atan2(y, x)})
+const polarToCartesian = ({r, t}) => ({x: r * Math.cos(t), y: r * Math.sin(t)})
+
+const findMemory = (mem) => {
+  let originalSnap = Object.assign({}, Array.from(mem))
+  let snap = Object.assign({}, Array.from(mem))
+  let criteria = () => true
+  let finalCriteria = () => true
+
+  const check = () => {
+    for (let key of Object.keys(snap)) {
+      if (!criteria(mem[key], snap[key])) delete snap[key]
+      else snap[key] = mem[key]
+    }
+    return o
+  }
+
+  const result = () => {
+    console.log(
+      Object.keys(snap)
+      .filter(x => finalCriteria(snap[x], originalSnap[x]))
+      .map(x => `0x${x.toString(16).padStart(4, '0')}`)
+    )
+  }
+
+  const setCriteria = (c, f) => {
+    criteria = c
+    finalCriteria = f
+    return o
+  }
+  const o = {
+    with: c => setCriteria(c),
+    increasing: () => setCriteria((a, b) => a >= b, (a, b) => a > b),
+    decreasing: () => setCriteria((a, b) => a <= b, (a, b) => a > b),
+    static: () => setCriteria((a, b) => a === b, () => true),
+    check,
+    result
+  }
+
+  return o
+}
+
+module.exports = {
+  cartesianToPolar,
+  polarToCartesian,
+  findMemory
+}

+ 3 - 1
package.json

@@ -7,7 +7,7 @@
     "build": "webpack --mode=production",
     "build:dev": "webpack --watch",
     "server": "node server/server.js --mode=production",
-    "server:dev": "nodemon --watch server server/server.js --mode=development",
+    "server:dev": "nodemon --watch server --watch common server/server.js --mode=development",
     "test": "mocha",
     "test:dev": "mocha --watch",
     "pwa:build": "nodemon --verbose --ignore dist --exec webpack",
@@ -43,6 +43,7 @@
     "angular-aria": "^1.7.7",
     "angular-material": "^1.1.12",
     "angular-ui-router": "^1.0.20",
+    "arraybuffer-loader": "^1.0.7",
     "babel-loader": "^8.0.5",
     "body-parser": "^1.18.3",
     "chalk": "^2.4.2",
@@ -55,6 +56,7 @@
     "material-icons": "^0.3.0",
     "morgan": "^1.9.1",
     "move-terminal-cursor": "^0.0.4",
+    "rxjs": "^6.5.2",
     "stylus": "^0.54.5",
     "stylus-loader": "^3.0.2",
     "text-loader": "^0.0.1",

+ 0 - 67
server/game.js

@@ -1,67 +0,0 @@
-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

+ 17 - 6
server/server.js

@@ -1,24 +1,35 @@
 const app = require('./app')
-const Game = require('./game')
+const fs = require('fs')
+require.extensions['.nes'] = function(module, filename) {
+  module.exports = fs.readFileSync(filename, 'binary')
+}
 
-game = new Game()
+const SMB = require('../common/smb')
+
+game = new SMB()
+console.log(game.start.toString())
 game.start()
 
 app.ws('/game', (ws, req) => {
   const send = (frame) => {
     try {
+      ws.send(JSON.stringify(game.gameState()))
       ws.send(frame)
     } catch(err) {
-      console.warn(err)
+      console.warn(err.message)
       teardown()
     }
   }
+  const frameSub = game.frames.subscribe(send)
   const teardown = () => {
     console.log('Detaching')
-    game.off('frame', send)
+    frameSub.unsubscribe()
   }
-  game.on('frame', send)
-  ws.on('close', () => teardown)
+  ws.on('message', (msg) => {
+    const data = JSON.parse(msg)
+    game.command(data)
+  })
+  ws.on('close', teardown)
 })
 
 app.listen().catch((err) => {

+ 4 - 0
webpack.config.js

@@ -37,6 +37,10 @@ module.exports = {
       {
         test: /\.(ttf|woff|woff2|svg|eot)$/,
         loader: 'file-loader'
+      },
+      {
+        test: /\.nes$/,
+        loader: 'arraybuffer-loader'
       }
     ]
   },

+ 14 - 0
yarn.lock

@@ -479,6 +479,13 @@ array-unique@^0.3.2:
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
+arraybuffer-loader@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/arraybuffer-loader/-/arraybuffer-loader-1.0.7.tgz#1045f87e187c7d5da59a4391572d30504ca135ad"
+  integrity sha512-8FPjFP26OF723Oiaj5E208WW2tAgE0noCsrtQZ20TwdHt6qs5iuAaKqX5WY/C3wYdF/2cRUiuIXj98qy/2NB7g==
+  dependencies:
+    loader-utils "^1.1.0"
+
 asap@~2.0.3:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@@ -4660,6 +4667,13 @@ rx@^4.1.0:
   resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
   integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=
 
+rxjs@^6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7"
+  integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==
+  dependencies:
+    tslib "^1.9.0"
+
 safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"