├── .gitignore ├── LICENSE ├── README.md ├── emu ├── audio.go ├── cpu.go ├── display.go ├── doc.go ├── emulator.go ├── emulator_test.go ├── input.go └── memory.go ├── games ├── BRIX.ch8 ├── PONG2.ch8 ├── TETRIS.ch8 └── UFO.ch8 ├── go.mod ├── go.sum ├── images ├── brix.gif ├── chip8.jpg ├── chip8go.png ├── pong2.gif ├── tetris.gif └── ufo.gif └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | chip8go 2 | roms/** 3 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 szTheory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![chip8go](images/chip8go.png) 2 | 3 | > a CHIP-8 emulator written in Go 4 | 5 | ![Brix](images/brix.gif) 6 | 7 | - [About](#about) 8 | - [Instructions](#instructions) 9 | - [Controls](#controls) 10 | - [Games](#games) 11 | 12 | ## About 13 | 14 | CHIP-8 is an interpreted programming language originally designed for hobby computers in the mid-70s. 15 | 16 | ![Telmac 1800 running CHIP-8 game Space Intercept (Joseph Weisbecker, 1978)](images/chip8.jpg) 17 | 18 | > Telmac 1800 running CHIP-8 game Space Intercept (Joseph Weisbecker, 1978) 19 | 20 | ## Instructions 21 | 22 | Download chip8go and run the program. A file dialog will appear for you to choose a `.ch8` game. Several quality public domain games are included in the `games` folder. 23 | 24 | ## Controls 25 | 26 | `Enter` resets the game 27 | 28 | Game buttons are on the left side of your keyboard: 29 | 30 | ```ascii 31 | 1 2 3 4 32 | Q W E R 33 | A S D F 34 | Z X C V 35 | ``` 36 | 37 | Each corresponding to keys on the original CHIP-8 layout: 38 | 39 | ```ascii 40 | C D E F 41 | 8 9 A B 42 | 4 5 6 7 43 | 0 1 2 3 44 | ``` 45 | 46 | ## Games 47 | 48 | ### Brix 49 | 50 | ![Brix](images/brix.gif) 51 | 52 | - `Q` move left 53 | - `E` move right 54 | 55 | ### Pong 2 56 | 57 | ![Pong 2](images/pong2.gif) 58 | 59 | #### Player 1 60 | 61 | - `1` Move up 62 | - `Q` Move down 63 | 64 | #### Player 2 65 | 66 | - `4` Move up 67 | - `R` Move down 68 | 69 | ### Tetris 70 | 71 | ![Tetris](images/tetris.gif) 72 | 73 | - `W` Move left 74 | - `E` Move right 75 | - `Q` Rotate 76 | - `A` Fast drop 77 | 78 | ### UFO 79 | 80 | ![Tetris](images/ufo.gif) 81 | 82 | - `Q` Shoot left 83 | - `W` Shoot up 84 | - `E` Shoot right 85 | -------------------------------------------------------------------------------- /emu/audio.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/hajimehoshi/ebiten/audio" 7 | ) 8 | 9 | const ( 10 | sampleRate = 44100 11 | frequency = 440 12 | ) 13 | 14 | func NewAudioPlayer() *audio.Player { 15 | var audioContext *audio.Context 16 | if currentContext := audio.CurrentContext(); currentContext != nil { 17 | audioContext = currentContext 18 | } else { 19 | var err error 20 | audioContext, err = audio.NewContext(sampleRate) 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | // Pass the (infinite) stream to audio.NewPlayer. 27 | audioPlayer, err := audio.NewPlayer(audioContext, &stream{}) 28 | if err != nil { 29 | panic(err) 30 | } 31 | audioPlayer.SetVolume(0) 32 | 33 | // After calling Play, the stream never ends as long as the player object lives. 34 | if err := audioPlayer.Play(); err != nil { 35 | panic(err) 36 | } 37 | 38 | return audioPlayer 39 | } 40 | 41 | // stream is an infinite stream of 440 Hz sine wave. 42 | type stream struct { 43 | position int64 44 | remaining []byte 45 | } 46 | 47 | // Read from io.Reader 48 | // Read fills the data with sine wave samples. 49 | func (s *stream) Read(buf []byte) (int, error) { 50 | if len(s.remaining) > 0 { 51 | n := copy(buf, s.remaining) 52 | s.remaining = s.remaining[n:] 53 | 54 | return n, nil 55 | } 56 | 57 | var origBuf []byte 58 | if len(buf)%4 > 0 { 59 | origBuf = buf 60 | buf = make([]byte, len(origBuf)+4-len(origBuf)%4) 61 | } 62 | 63 | const length = int64(sampleRate / frequency) 64 | const max = 32767 65 | 66 | p := s.position / 4 67 | for i := 0; i < len(buf)/4; i++ { 68 | b := int16(math.Sin(2*math.Pi*float64(p)/float64(length)) * max) 69 | buf[4*i] = byte(b) 70 | buf[4*i+1] = byte(b >> 8) 71 | buf[4*i+2] = byte(b) 72 | buf[4*i+3] = byte(b >> 8) 73 | 74 | p++ 75 | } 76 | 77 | s.position += int64(len(buf)) 78 | s.position %= length * 4 79 | 80 | if origBuf != nil { 81 | n := copy(origBuf, buf) 82 | s.remaining = buf[n:] 83 | 84 | return n, nil 85 | } 86 | 87 | return len(buf), nil 88 | } 89 | 90 | // Close from io.Closer 91 | func (s *stream) Close() error { 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /emu/cpu.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | type CPU struct { 4 | V [16]byte //data registers V0-VF 5 | I uint16 //index (memory address) register 6 | PC uint16 //program counter 7 | SP byte //stack pointer 8 | Stack [16]uint16 //stack 9 | 10 | DelayTimer byte //used for timing game events, can be set/read 11 | SoundTimer byte //beeps when value is nonzero 12 | } 13 | 14 | func (c *CPU) Setup() { 15 | c.PC = RamProgramStart 16 | } 17 | -------------------------------------------------------------------------------- /emu/display.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | type Display struct { 4 | Pixels [ScreenWidthPx][ScreenHeightPx]byte 5 | Draw bool 6 | } 7 | 8 | const ( 9 | ScreenWidthPx = 64 10 | ScreenHeightPx = 32 11 | 12 | SpriteWidthPx = 8 13 | PixelFontByteLength = 5 14 | ) 15 | 16 | func (d *Display) Clear() { 17 | for x := 0; x < ScreenWidthPx; x++ { 18 | for y := 0; y < ScreenHeightPx; y++ { 19 | d.Pixels[x][y] = 0 20 | } 21 | } 22 | } 23 | 24 | // Sprites are XORed onto the existing screen. 25 | // Returns true if any pixels were erased, false otherwise 26 | func (d *Display) DrawSprite(x byte, y byte, row byte) bool { 27 | erased := false 28 | yIndex := y % ScreenHeightPx 29 | 30 | for i := x; i < x+8; i++ { 31 | xIndex := i % ScreenWidthPx 32 | 33 | wasSet := d.Pixels[xIndex][yIndex] == 1 34 | value := row >> (x + 8 - i - 1) & 1 35 | 36 | d.Pixels[xIndex][yIndex] ^= value 37 | 38 | if wasSet && d.Pixels[xIndex][yIndex] == 0 { 39 | erased = true 40 | } 41 | } 42 | 43 | return erased 44 | } 45 | -------------------------------------------------------------------------------- /emu/doc.go: -------------------------------------------------------------------------------- 1 | // Chip-8 specifications available at 2 | // http://devernay.free.fr/hacks/chip8/C8TECH10.HTM 3 | package emu 4 | -------------------------------------------------------------------------------- /emu/emulator.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/hajimehoshi/ebiten/audio" 8 | ) 9 | 10 | type Emulator struct { 11 | cpu *CPU 12 | memory *Memory 13 | Display *Display 14 | Input *Input 15 | AudioPlayer *audio.Player 16 | 17 | waitingForInputRegisterOffset byte 18 | } 19 | 20 | func (e *Emulator) SoundEnabled() bool { 21 | return e.cpu.SoundTimer > 0 22 | } 23 | 24 | func (e *Emulator) Setup(romFilename string) { 25 | // cpu 26 | e.cpu = new(CPU) 27 | e.cpu.Setup() 28 | 29 | // memory 30 | e.memory = new(Memory) 31 | e.memory.Setup() 32 | e.memory.LoadGame(romFilename) 33 | 34 | // display 35 | e.Display = new(Display) 36 | 37 | // input 38 | e.Input = new(Input) 39 | 40 | // audio 41 | e.AudioPlayer = NewAudioPlayer() 42 | } 43 | 44 | func (e *Emulator) CatchInput(keyIndex byte) { 45 | e.cpu.V[e.waitingForInputRegisterOffset] = keyIndex 46 | e.Input.WaitingForInput = false 47 | } 48 | 49 | func (e *Emulator) EmulateCycle() { 50 | // LD Vx, K 51 | // Blocks execution until input is received 52 | if e.Input.WaitingForInput { 53 | return 54 | } 55 | 56 | // Fetch instruction at program counter 57 | var instruction uint16 = (uint16(e.memory.RAM[e.cpu.PC]) << 8) | uint16(e.memory.RAM[e.cpu.PC+1]) 58 | 59 | // Advance program counter 60 | e.cpu.PC += 2 61 | 62 | // Break instruction into component args 63 | nnn := instruction & 0xFFF 64 | n := byte(instruction & 0xF) 65 | x := byte(instruction & 0xF00 >> 8) 66 | y := byte(instruction & 0xF0 >> 4) 67 | kk := byte(instruction & 0xFF) 68 | 69 | // Execute instruction 70 | switch instruction { 71 | case 0x00E0: 72 | e.op00E0() 73 | case 0x00EE: 74 | e.op00EE() 75 | default: 76 | switch byte(instruction & 0xF000 >> 12) { 77 | case 0x0: 78 | e.op0nnn(nnn) 79 | case 0x1: 80 | e.op1nnn(nnn) 81 | case 0x2: 82 | e.op2nnn(nnn) 83 | case 0x3: 84 | e.op3xkk(x, kk) 85 | case 0x4: 86 | e.op4xkk(x, kk) 87 | case 0x5: 88 | e.op5xy0(x, y) 89 | case 0x6: 90 | e.op6xkk(x, kk) 91 | case 0x7: 92 | e.op7xkk(x, kk) 93 | case 0x8: 94 | switch instruction & 0xF { 95 | case 0x0: 96 | e.op8xy0(x, y) 97 | case 0x1: 98 | e.op8xy1(x, y) 99 | case 0x2: 100 | e.op8xy2(x, y) 101 | case 0x3: 102 | e.op8xy3(x, y) 103 | case 0x4: 104 | e.op8xy4(x, y) 105 | case 0x5: 106 | e.op8xy5(x, y) 107 | case 0x6: 108 | e.op8xy6(x, y) 109 | case 0x7: 110 | e.op8xy7(x, y) 111 | case 0xE: 112 | e.op8xyE(x, y) 113 | default: 114 | panicInstructionNotImplemented(instruction) 115 | } 116 | case 0x9: 117 | e.op9xy0(x, y) 118 | case 0xA: 119 | e.opAnnn(nnn) 120 | case 0xB: 121 | e.opBnnn(nnn) 122 | case 0xC: 123 | e.opCxkk(x, kk) 124 | case 0xD: 125 | e.opDxyn(x, y, n) 126 | case 0xE: 127 | switch instruction & 0xFF { 128 | case 0x9E: 129 | e.opEx9E(x) 130 | case 0xA1: 131 | e.opExA1(x) 132 | default: 133 | panicInstructionNotImplemented(instruction) 134 | } 135 | case 0xF: 136 | switch instruction & 0xFF { 137 | case 0x07: 138 | e.opFx07(x) 139 | case 0x0A: 140 | e.opFx0A(x) 141 | case 0x15: 142 | e.opFx15(x) 143 | case 0x18: 144 | e.opFx18(x) 145 | case 0x1E: 146 | e.opFx1E(x) 147 | case 0x29: 148 | e.opFx29(x) 149 | case 0x33: 150 | e.opFx33(x) 151 | case 0x55: 152 | e.opFx55(x) 153 | case 0x65: 154 | e.opFx65(x) 155 | default: 156 | panicInstructionNotImplemented(instruction) 157 | } 158 | default: 159 | panicInstructionNotImplemented(instruction) 160 | } 161 | } 162 | } 163 | 164 | func (e *Emulator) UpdateDelayTimer() { 165 | if e.cpu.DelayTimer > 0 { 166 | e.cpu.DelayTimer-- 167 | } 168 | } 169 | 170 | func (e *Emulator) UpdateSoundTimer() { 171 | if e.cpu.SoundTimer > 0 { 172 | e.cpu.SoundTimer-- 173 | } 174 | } 175 | 176 | // 00E0 - CLS 177 | // Clear the display. 178 | func (e *Emulator) op00E0() { 179 | e.Display.Clear() 180 | } 181 | 182 | // 00EE - RET 183 | // Return from a subroutine. 184 | // The interpreter sets the program counter to the address at the top of 185 | // the stack, then subtracts 1 from the stack pointer. 186 | func (e *Emulator) op00EE() { 187 | e.cpu.PC = uint16(e.cpu.Stack[e.cpu.SP]) 188 | e.cpu.SP-- 189 | } 190 | 191 | // 0nnn - SYS addr 192 | // Jump to a machine code routine at nnn. 193 | // This instruction is only used on the old computers on which Chip-8 was 194 | // originally implemented. It is ignored by modern interpreters. 195 | func (e *Emulator) op0nnn(addr uint16) { 196 | // Do nothing. 197 | // This instruction is ignored by modern interpreters 198 | } 199 | 200 | // 1nnn - JP addr 201 | // Jump to location nnn. 202 | // The interpreter sets the program counter to nnn. 203 | func (e *Emulator) op1nnn(addr uint16) { 204 | e.cpu.PC = addr 205 | } 206 | 207 | // 2nnn - CALL addr 208 | // Call subroutine at nnn. 209 | // The interpreter increments the stack pointer, then puts the current PC 210 | // on the top of the stack. The PC is then set to nnn. 211 | func (e *Emulator) op2nnn(addr uint16) { 212 | e.cpu.SP++ 213 | e.cpu.Stack[e.cpu.SP] = e.cpu.PC 214 | e.cpu.PC = addr 215 | } 216 | 217 | // 3xkk - SE Vx, byte 218 | // Skip next instruction if Vx = kk. 219 | // The interpreter compares register Vx to kk, and if they are equal, 220 | // increments the program counter by 2. 221 | func (e *Emulator) op3xkk(x, kk byte) { 222 | if e.cpu.V[x] == kk { 223 | e.cpu.PC += 2 224 | } 225 | } 226 | 227 | // 4xkk - SNE Vx, byte 228 | // Skip next instruction if Vx != kk. 229 | // The interpreter compares register Vx to kk, and if they are not equal, 230 | // increments the program counter by 2. 231 | func (e *Emulator) op4xkk(x, kk byte) { 232 | if e.cpu.V[x] != kk { 233 | e.cpu.PC += 2 234 | } 235 | } 236 | 237 | // 5xy0 - SE Vx, Vy 238 | // Skip next instruction if Vx = Vy. 239 | // The interpreter compares register Vx to register Vy, and if they are equal, 240 | // increments the program counter by 2. 241 | func (e *Emulator) op5xy0(x, y byte) { 242 | if e.cpu.V[x] == e.cpu.V[y] { 243 | e.cpu.PC += 2 244 | } 245 | } 246 | 247 | // 6xkk - LD Vx, byte 248 | // Set Vx = kk. 249 | // The interpreter puts the value kk into register Vx. 250 | func (e *Emulator) op6xkk(x, kk byte) { 251 | e.cpu.V[x] = kk 252 | } 253 | 254 | // 7xkk - ADD Vx, byte 255 | // Set Vx = Vx + kk. 256 | // Adds the value kk to the value of register Vx, then stores the result in Vx. 257 | func (e *Emulator) op7xkk(x, kk byte) { 258 | e.cpu.V[x] += kk 259 | } 260 | 261 | // 8xy0 - LD Vx, Vy 262 | // Set Vx = Vy. 263 | // Stores the value of register Vy in register Vx. 264 | func (e *Emulator) op8xy0(x, y byte) { 265 | e.cpu.V[x] = e.cpu.V[y] 266 | } 267 | 268 | // 8xy1 - OR Vx, Vy 269 | // Set Vx = Vx OR Vy. 270 | // Performs a bitwise OR on the values of Vx and Vy, then stores the 271 | // result in Vx. A bitwise OR compares the corrseponding bits from two 272 | // values, and if either bit is 1, then the same bit in the result is 273 | // also 1. Otherwise, it is 0. 274 | func (e *Emulator) op8xy1(x, y byte) { 275 | e.cpu.V[x] |= e.cpu.V[y] 276 | } 277 | 278 | // 8xy2 - AND Vx, Vy 279 | // Set Vx = Vx AND Vy. 280 | // Performs a bitwise AND on the values of Vx and Vy, then stores the result 281 | // in Vx. A bitwise AND compares the corrseponding bits from two values, 282 | // and if both bits are 1, then the same bit in the result is also 1. 283 | // Otherwise, it is 0. 284 | func (e *Emulator) op8xy2(x, y byte) { 285 | e.cpu.V[x] &= e.cpu.V[y] 286 | } 287 | 288 | // 8xy3 - XOR Vx, Vy 289 | // Set Vx = Vx XOR Vy. 290 | // Performs a bitwise exclusive OR on the values of Vx and Vy, then 291 | // stores the result in Vx. An exclusive OR compares the corrseponding 292 | // bits from two values, and if the bits are not both the same, then the 293 | // corresponding bit in the result is set to 1. Otherwise, it is 0. 294 | func (e *Emulator) op8xy3(x, y byte) { 295 | e.cpu.V[x] ^= e.cpu.V[y] 296 | } 297 | 298 | // 8xy4 - ADD Vx, Vy 299 | // Set Vx = Vx + Vy, set VF = carry. 300 | // The values of Vx and Vy are added together. If the result is greater 301 | // than 8 bits (i.e., > 255,) VF is set to 1, otherwise 0. Only the lowest 302 | // 8 bits of the result are kept, and stored in Vx. 303 | func (e *Emulator) op8xy4(x, y byte) { 304 | sum := uint16(e.cpu.V[x]) + uint16(e.cpu.V[y]) 305 | 306 | var overflowStatus byte 307 | if sum > 0xFFFF { 308 | overflowStatus = 1 309 | } 310 | e.cpu.V[0xF] = overflowStatus 311 | 312 | e.cpu.V[x] = byte(sum) 313 | } 314 | 315 | // 8xy5 - SUB Vx, Vy 316 | // Set Vx = Vx - Vy, set VF = NOT borrow. 317 | // If Vx > Vy, then VF is set to 1, otherwise 0. Then Vy is subtracted 318 | // from Vx, and the results stored in Vx. 319 | func (e *Emulator) op8xy5(x, y byte) { 320 | var noBorrow byte 321 | if e.cpu.V[x] > e.cpu.V[y] { 322 | noBorrow = 1 323 | } 324 | e.cpu.V[0xF] = noBorrow 325 | 326 | e.cpu.V[x] -= e.cpu.V[y] 327 | } 328 | 329 | // 8xy6 - SHR Vx {, Vy} 330 | // Set Vx = Vx SHR 1. 331 | // If the least-significant bit of Vx is 1, then VF is set to 1, otherwise 0. 332 | // Then Vx is divided by 2. 333 | func (e *Emulator) op8xy6(x, y byte) { 334 | 335 | var lsbIsOne byte 336 | if (e.cpu.V[x] & 0xF) == 1 { 337 | lsbIsOne = 1 338 | } 339 | e.cpu.V[0xF] = lsbIsOne 340 | 341 | e.cpu.V[x] >>= 1 342 | } 343 | 344 | // 8xy7 - SUBN Vx, Vy 345 | // Set Vx = Vy - Vx, set VF = NOT borrow. 346 | // If Vy > Vx, then VF is set to 1, otherwise 0. Then Vx is subtracted 347 | // from Vy, and the results stored in Vx. 348 | func (e *Emulator) op8xy7(x, y byte) { 349 | var noBorrow byte 350 | if e.cpu.V[y] > e.cpu.V[x] { 351 | noBorrow = 1 352 | } 353 | e.cpu.V[0xF] = noBorrow 354 | 355 | e.cpu.V[x] = e.cpu.V[y] - e.cpu.V[x] 356 | } 357 | 358 | // 8xyE - SHL Vx {, Vy} 359 | // Set Vx = Vx SHL 1. 360 | // If the most-significant bit of Vx is 1, then VF is set to 1, 361 | // otherwise to 0. Then Vx is multiplied by 2. 362 | func (e *Emulator) op8xyE(x, y byte) { 363 | var msbIsOne byte 364 | if (e.cpu.V[x] >> 7) == 1 { 365 | msbIsOne = 1 366 | } 367 | e.cpu.V[0xF] = msbIsOne 368 | 369 | e.cpu.V[x] <<= 1 370 | } 371 | 372 | // 9xy0 - SNE Vx, Vy 373 | // Skip next instruction if Vx != Vy. 374 | // The values of Vx and Vy are compared, and if they are not equal, 375 | // the program counter is increased by 2. 376 | func (e *Emulator) op9xy0(x, y byte) { 377 | if e.cpu.V[x] != e.cpu.V[y] { 378 | e.cpu.PC += 2 379 | } 380 | } 381 | 382 | // Annn - LD I, addr 383 | // Set I = nnn. 384 | // The value of register I is set to nnn. 385 | func (e *Emulator) opAnnn(addr uint16) { 386 | e.cpu.I = addr 387 | } 388 | 389 | // Bnnn - JP V0, addr 390 | // Jump to location nnn + V0. 391 | // The program counter is set to nnn plus the value of V0. 392 | func (e *Emulator) opBnnn(addr uint16) { 393 | e.cpu.PC = addr + uint16(e.cpu.V[0]) 394 | } 395 | 396 | // Cxkk - RND Vx, byte 397 | // Set Vx = random byte AND kk. 398 | // The interpreter generates a random number from 0 to 255, which is then 399 | // ANDed with the value kk. The results are stored in Vx. See instruction 400 | // 8xy2 for more information on AND. 401 | func (e *Emulator) opCxkk(x, kk byte) { 402 | randomValue := byte(rand.Uint32() % 255) 403 | 404 | e.cpu.V[x] = randomValue & kk 405 | } 406 | 407 | // Dxyn - DRW Vx, Vy, nibble 408 | // Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision. 409 | // The interpreter reads n bytes from memory, starting at the address stored in I. 410 | // These bytes are then displayed as sprites on screen at coordinates (Vx, Vy). 411 | // Sprites are XORed onto the existing screen. If this causes any pixels to be erased, 412 | // VF is set to 1, otherwise it is set to 0. If the sprite is positioned so part of it 413 | // is outside the coordinates of the display, it wraps around to the opposite 414 | // side of the screen. 415 | func (e *Emulator) opDxyn(x, y, n byte) { 416 | xVal := e.cpu.V[x] 417 | yVal := e.cpu.V[y] 418 | 419 | e.cpu.V[0xF] = 0 420 | 421 | var i byte = 0 422 | for ; i < n; i++ { 423 | row := e.memory.RAM[e.cpu.I+uint16(i)] 424 | 425 | if erased := e.Display.DrawSprite(xVal, yVal+i, row); erased { 426 | e.cpu.V[0xF] = 1 427 | } 428 | } 429 | } 430 | 431 | // Ex9E - SKP Vx 432 | // Skip next instruction if key with the value of Vx is pressed. 433 | // Checks the keyboard, and if the key corresponding to the value 434 | // of Vx is currently in the down position, PC is increased by 2. 435 | func (e *Emulator) opEx9E(x byte) { 436 | if e.Input.IsPressed(e.cpu.V[x]) { 437 | e.cpu.PC += 2 438 | } 439 | } 440 | 441 | // ExA1 - SKNP Vx 442 | // Skip next instruction if key with the value of Vx is not pressed. 443 | // Checks the keyboard, and if the key corresponding to the value of 444 | // Vx is currently in the up position, PC is increased by 2. 445 | func (e *Emulator) opExA1(x byte) { 446 | if !e.Input.IsPressed(e.cpu.V[x]) { 447 | e.cpu.PC += 2 448 | } 449 | } 450 | 451 | // Fx07 - LD Vx, DT 452 | // Set Vx = delay timer value. 453 | // The value of DT is placed into Vx. 454 | func (e *Emulator) opFx07(x byte) { 455 | e.cpu.V[x] = e.cpu.DelayTimer 456 | } 457 | 458 | // Fx0A - LD Vx, K 459 | // Wait for a key press, store the value of the key in Vx. 460 | // All execution stops until a key is pressed, then the value of that key is stored in Vx. 461 | func (e *Emulator) opFx0A(x byte) { 462 | e.Input.WaitingForInput = true 463 | e.waitingForInputRegisterOffset = x 464 | } 465 | 466 | // Fx15 - LD DT, Vx 467 | // Set delay timer = Vx. 468 | // DT is set equal to the value of Vx. 469 | func (e *Emulator) opFx15(x byte) { 470 | e.cpu.DelayTimer = e.cpu.V[x] 471 | } 472 | 473 | // Fx18 - LD ST, Vx 474 | // Set sound timer = Vx. 475 | // ST is set equal to the value of Vx. 476 | func (e *Emulator) opFx18(x byte) { 477 | e.cpu.SoundTimer = e.cpu.V[x] 478 | } 479 | 480 | // Fx1E - ADD I, Vx 481 | // Set I = I + Vx. 482 | // The values of I and Vx are added, and the results are stored in I. 483 | func (e *Emulator) opFx1E(x byte) { 484 | e.cpu.I += uint16(e.cpu.V[x]) 485 | } 486 | 487 | // Fx29 - LD F, Vx 488 | // Set I = location of sprite for digit Vx. 489 | // The value of I is set to the location for the hexadecimal sprite corresponding 490 | // to the value of Vx. See section 2.4, Display, for more information on the 491 | // Chip-8 hexadecimal font. 492 | func (e *Emulator) opFx29(x byte) { 493 | e.cpu.I = uint16(RamFontStart) + uint16(e.cpu.V[x]*PixelFontByteLength) 494 | } 495 | 496 | // Fx33 - LD B, Vx 497 | // Store BCD representation of Vx in memory locations I, I+1, and I+2. 498 | // The interpreter takes the decimal value of Vx, and places the hundreds 499 | // digit in memory at location in I, the tens digit at location I+1, 500 | // and the ones digit at location I+2. 501 | func (e *Emulator) opFx33(x byte) { 502 | decimalValue := e.cpu.V[x] 503 | 504 | e.memory.RAM[e.cpu.I] = decimalValue / 100 //hundreds 505 | decimalValue -= e.memory.RAM[e.cpu.I] * 100 506 | 507 | e.memory.RAM[e.cpu.I+1] = decimalValue / 10 //tens 508 | decimalValue -= e.memory.RAM[e.cpu.I+1] * 10 509 | 510 | e.memory.RAM[e.cpu.I+2] = decimalValue / 1 //ones 511 | } 512 | 513 | // Fx55 - LD [I], Vx 514 | // Store registers V0 through Vx in memory starting at location I. 515 | // The interpreter copies the values of registers V0 through Vx into 516 | // memory, starting at the address in I. 517 | func (e *Emulator) opFx55(x byte) { 518 | var i byte = 0 519 | for ; i <= x; i++ { 520 | e.memory.RAM[e.cpu.I+uint16(i)] = e.cpu.V[i] 521 | } 522 | } 523 | 524 | // Fx65 - LD Vx, [I] 525 | // Read registers V0 through Vx from memory starting at location I. 526 | // The interpreter reads values from memory starting at location I 527 | // into registers V0 through Vx. 528 | func (e *Emulator) opFx65(x byte) { 529 | var i byte = 0 530 | for ; i <= x; i++ { 531 | e.cpu.V[i] = e.memory.RAM[e.cpu.I+uint16(i)] 532 | } 533 | } 534 | 535 | func panicInstructionNotImplemented(instruction uint16) { 536 | panic("Instruction not implemented 0x" + fmt.Sprintf("%X", instruction)) 537 | } 538 | -------------------------------------------------------------------------------- /emu/emulator_test.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Fx33 - LD B, Vx 8 | func TestOpFx33(t *testing.T) { 9 | e := new(Emulator) 10 | e.Setup("../roms/IBM.ch8") 11 | 12 | tests := [16][5]byte{ 13 | {0, 255, 2, 5, 5}, 14 | } 15 | 16 | e.cpu.I = RamProgramStart 17 | 18 | for i := 0; i < len(tests); i++ { 19 | test := tests[i] 20 | register := test[0] 21 | decimal := test[1] 22 | hundreds := test[2] 23 | tens := test[3] 24 | ones := test[4] 25 | 26 | e.cpu.V[register] = decimal 27 | e.opFx33(register) 28 | 29 | if e.memory.RAM[e.cpu.I] != hundreds { 30 | t.Errorf("Expected hundreds of %d but was %d", hundreds, e.memory.RAM[e.cpu.I]) 31 | } 32 | if e.memory.RAM[e.cpu.I+1] != tens { 33 | t.Errorf("Expected tens of %d but was %d", tens, e.memory.RAM[e.cpu.I+1]) 34 | } 35 | if e.memory.RAM[e.cpu.I+2] != ones { 36 | t.Errorf("Expected ones of %d but was %d", ones, e.memory.RAM[e.cpu.I+2]) 37 | } 38 | } 39 | } 40 | 41 | // Fx65 - LD Vx, [I] 42 | func TestOpFx65(t *testing.T) { 43 | e := new(Emulator) 44 | e.Setup("../roms/IBM.ch8") 45 | 46 | expected := byte(0xAA) 47 | e.cpu.I = RamProgramStart 48 | e.memory.RAM[e.cpu.I] = expected 49 | i := uint16(0xFF65) 50 | e.opFx65(byte(i & 0xF00 >> 8)) 51 | 52 | for i := 0; i <= 0xF; i++ { 53 | actual := e.cpu.V[0] 54 | if expected != actual { 55 | t.Errorf("Expected %X at V%d but was %X", expected, i, actual) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /emu/input.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | type Input struct { 4 | // input is done with a hex keyboard that has 5 | // 16 keys ranging 0 to F 6 | keys [16]bool 7 | WaitingForInput bool 8 | } 9 | 10 | func (i *Input) IsPressed(keyIndex byte) bool { 11 | return i.keys[keyIndex] 12 | } 13 | 14 | func (i *Input) Update(keyIndex byte, pressed bool) { 15 | i.keys[keyIndex] = pressed 16 | } 17 | -------------------------------------------------------------------------------- /emu/memory.go: -------------------------------------------------------------------------------- 1 | package emu 2 | 3 | import ( 4 | "io/ioutil" 5 | ) 6 | 7 | type Memory struct { 8 | // 4KB (4,096 bytes) from 0x000 (0) to 0xFFF (4095) 9 | // the uppermost 256 bytes (0xF00-0xFFF) are reserved for display refresh, 10 | // and the 96 bytes below that (0xEA0-0xEFF) are reserved for the call stack, 11 | // internal use, and other variables 12 | RAM [RamSize]byte 13 | } 14 | 15 | const ( 16 | RamProgramStart = 0x200 17 | RamSize = 0x1000 18 | RamFontStart byte = 0x0 19 | ) 20 | 21 | func (m *Memory) Setup() { 22 | m.installFont() 23 | } 24 | 25 | func (m *Memory) LoadGame(romFilename string) { 26 | contents, err := ioutil.ReadFile(romFilename) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | for i, n := range contents { 32 | location := RamProgramStart + i 33 | if location > RamSize { 34 | panic("ROM was too large to fit into Chip-8 memory") 35 | } 36 | 37 | m.RAM[location] = n 38 | } 39 | } 40 | 41 | func (m *Memory) installFont() { 42 | // Chip-8's 4x5 pixel font set (0-F) 43 | // See http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.4 44 | fontBytes := [80]byte{ 45 | 0xF0, 0x90, 0x90, 0x90, 0xF0, //0 46 | 0x20, 0x60, 0x20, 0x20, 0x70, //1 47 | 0xF0, 0x10, 0xF0, 0x80, 0xF0, //2 48 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, //3 49 | 0x90, 0x90, 0xF0, 0x10, 0x10, //4 50 | 0xF0, 0x80, 0xF0, 0x10, 0xF0, //5 51 | 0xF0, 0x80, 0xF0, 0x90, 0xF0, //6 52 | 0xF0, 0x10, 0x20, 0x40, 0x40, //7 53 | 0xF0, 0x90, 0xF0, 0x90, 0xF0, //8 54 | 0xF0, 0x90, 0xF0, 0x10, 0xF0, //9 55 | 0xF0, 0x90, 0xF0, 0x90, 0x90, //A 56 | 0xE0, 0x90, 0xE0, 0x90, 0xE0, //B 57 | 0xF0, 0x80, 0x80, 0x80, 0xF0, //C 58 | 0xE0, 0x90, 0x90, 0x90, 0xE0, //D 59 | 0xF0, 0x80, 0xF0, 0x80, 0xF0, //E 60 | 0xF0, 0x80, 0xF0, 0x80, 0x80, //F 61 | } 62 | 63 | for i, b := range fontBytes { 64 | m.RAM[RamFontStart+byte(i)] = b 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /games/BRIX.ch8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/games/BRIX.ch8 -------------------------------------------------------------------------------- /games/PONG2.ch8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/games/PONG2.ch8 -------------------------------------------------------------------------------- /games/TETRIS.ch8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/games/TETRIS.ch8 -------------------------------------------------------------------------------- /games/UFO.ch8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/games/UFO.ch8 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/szTheory/chip8go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gotk3/gotk3 v0.4.0 // indirect 7 | github.com/hajimehoshi/ebiten v1.11.4 8 | github.com/sqweek/dialog v0.0.0-20200601143742-43ea34326190 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 5 | github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= 6 | github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= 7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= 8 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 9 | github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= 10 | github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 11 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 12 | github.com/gotk3/gotk3 v0.4.0 h1:TIuhyQitGeRTxOQIV3AJlYtEWWJpC74JHwAIsxlH8MU= 13 | github.com/gotk3/gotk3 v0.4.0/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo= 14 | github.com/hajimehoshi/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU= 15 | github.com/hajimehoshi/ebiten v1.11.4 h1:ngYF0NxKjFBsY/Bol6V0X/b0hoCCTi9nJRg7Dv8+ePc= 16 | github.com/hajimehoshi/ebiten v1.11.4/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg= 17 | github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE= 18 | github.com/hajimehoshi/oto v0.3.4/go.mod h1:PgjqsBJff0efqL2nlMJidJgVJywLn6M4y8PI4TfeWfA= 19 | github.com/hajimehoshi/oto v0.5.4 h1:Dn+WcYeF310xqStKm0tnvoruYUV5Sce8+sfUaIvWGkE= 20 | github.com/hajimehoshi/oto v0.5.4/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 21 | github.com/jakecoffman/cp v0.1.0/go.mod h1:a3xPx9N8RyFAACD644t2dj/nK4SuLg1v+jL61m2yVo4= 22 | github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= 23 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/mattn/go-gtk v0.0.0-20180216084204-5a311a1830ab/go.mod h1:PwzwfeB5syFHXORC3MtPylVcjIoTDT/9cvkKpEndGVI= 30 | github.com/mattn/go-pointer v0.0.0-20171114154726-1d30dc4b6f28/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 31 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 32 | github.com/skelterjohn/go.wde v0.0.0-20180104102407-a0324cbf3ffe/go.mod h1:zXxNsJHeUYIqpg890APBNEn9GoCbA4Cdnvuv3mx4fBk= 33 | github.com/sqweek/dialog v0.0.0-20200601143742-43ea34326190 h1:unOhcX2BMB09ByGiIvJds5p68ztCJlWGH8pLRxdz+Mg= 34 | github.com/sqweek/dialog v0.0.0-20200601143742-43ea34326190/go.mod h1:QSrNdZLZB8VoFPGlZ2vDuA2oNaVdhld3g0PZLc7soX8= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 38 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 39 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 40 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 41 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 42 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 43 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 44 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= 45 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 46 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 47 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 48 | golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0 h1:nZASbxDuz7CO3227BWCCf0MC6ynyvKh6eMDoLcNXAk0= 49 | golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 50 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 51 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 52 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 53 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 54 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 55 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 58 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 62 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 64 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 65 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 66 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 67 | golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 68 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 71 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | -------------------------------------------------------------------------------- /images/brix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/brix.gif -------------------------------------------------------------------------------- /images/chip8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/chip8.jpg -------------------------------------------------------------------------------- /images/chip8go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/chip8go.png -------------------------------------------------------------------------------- /images/pong2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/pong2.gif -------------------------------------------------------------------------------- /images/tetris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/tetris.gif -------------------------------------------------------------------------------- /images/ufo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szTheory/chip8go/5a4c3ea9a28b5c01acf8b34382f0194f2463643b/images/ufo.gif -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "image/color" 6 | "path" 7 | 8 | "github.com/hajimehoshi/ebiten" 9 | "github.com/hajimehoshi/ebiten/inpututil" 10 | "github.com/sqweek/dialog" 11 | "github.com/szTheory/chip8go/emu" 12 | ) 13 | 14 | func main() { 15 | game := new(Game) 16 | if err := game.pickGame(); err != nil { 17 | return 18 | } 19 | 20 | ebiten.SetWindowSize(ScreenWidth, ScreenHeight) 21 | if err := ebiten.RunGame(game); err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | const ( 27 | scaleFactor = 10 28 | cyclesPerFrame = 10 29 | ScreenWidth = emu.ScreenWidthPx * scaleFactor 30 | ScreenHeight = emu.ScreenHeightPx * scaleFactor 31 | ) 32 | 33 | type Game struct { 34 | emulator *emu.Emulator 35 | romFilename string 36 | } 37 | 38 | // Update the logical state 39 | func (g *Game) Update(screen *ebiten.Image) error { 40 | inputs := keyPairs() 41 | 42 | // Enter key resets game 43 | if ebiten.IsKeyPressed(ebiten.KeyEnter) { 44 | g.reset() 45 | } 46 | 47 | for i := 0; i < cyclesPerFrame; i++ { 48 | // update inputs 49 | for i := 0; i < len(inputs); i++ { 50 | keyIndex := inputs[i].index 51 | key := inputs[i].key 52 | isPressed := ebiten.IsKeyPressed(key) 53 | 54 | g.emulator.Input.Update(keyIndex, isPressed) 55 | if isPressed && inpututil.IsKeyJustPressed(key) && g.emulator.Input.WaitingForInput { 56 | g.emulator.CatchInput(keyIndex) 57 | } 58 | } 59 | 60 | // emulate a cycle 61 | g.emulator.EmulateCycle() 62 | } 63 | 64 | // update audio 65 | var volume float64 66 | if g.emulator.SoundEnabled() { 67 | volume = 1 68 | } 69 | g.emulator.AudioPlayer.SetVolume(volume) 70 | 71 | // update timers 72 | g.emulator.UpdateDelayTimer() 73 | g.emulator.UpdateSoundTimer() 74 | 75 | return nil 76 | } 77 | 78 | // Render the screen 79 | func (g *Game) Draw(screen *ebiten.Image) { 80 | var err error 81 | var canvas *ebiten.Image 82 | if canvas, err = ebiten.NewImage(emu.ScreenWidthPx, emu.ScreenHeightPx, ebiten.FilterDefault); err != nil { 83 | panic(err) 84 | } 85 | if err := canvas.Fill(color.Black); err != nil { 86 | panic(err) 87 | } 88 | 89 | for x := 0; x < emu.ScreenWidthPx; x++ { 90 | for y := 0; y < emu.ScreenHeightPx; y++ { 91 | setColor := color.Black 92 | if g.emulator.Display.Pixels[x][y] == 1 { 93 | setColor = color.White 94 | } 95 | if setColor != canvas.At(x, y) { 96 | canvas.Set(x, y, setColor) 97 | } 98 | } 99 | } 100 | 101 | geometry := ebiten.GeoM{} 102 | if err := screen.DrawImage(canvas, &ebiten.DrawImageOptions{GeoM: geometry}); err != nil { 103 | panic(err) 104 | } 105 | } 106 | 107 | func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { 108 | return emu.ScreenWidthPx, emu.ScreenHeightPx 109 | } 110 | 111 | func (g *Game) reset() { 112 | g.emulator = new(emu.Emulator) 113 | g.emulator.Setup(g.romFilename) 114 | } 115 | 116 | func (g *Game) pickGame() error { 117 | romFilename, err := dialog.File().Filter("CHIP-8 game file", "ch8").Load() 118 | if err != nil { 119 | return err 120 | } 121 | if romFilename == "" { 122 | return errors.New("No game selected") 123 | } 124 | 125 | g.loadGame(romFilename) 126 | return nil 127 | } 128 | 129 | func (g *Game) loadGame(romFilename string) { 130 | ebiten.SetWindowTitle("Chip-8 - " + path.Base(romFilename)) 131 | g.romFilename = romFilename 132 | g.reset() 133 | } 134 | 135 | type keyPair struct { 136 | index byte 137 | key ebiten.Key 138 | } 139 | 140 | func keyPairs() [16]keyPair { 141 | list := [16]keyPair{ 142 | {0, ebiten.KeyX}, 143 | {1, ebiten.Key1}, 144 | {2, ebiten.Key2}, 145 | {3, ebiten.Key3}, 146 | {4, ebiten.KeyQ}, 147 | {5, ebiten.KeyW}, 148 | {6, ebiten.KeyE}, 149 | {7, ebiten.KeyA}, 150 | {8, ebiten.KeyS}, 151 | {9, ebiten.KeyD}, 152 | {0xA, ebiten.KeyZ}, 153 | {0xB, ebiten.KeyC}, 154 | {0xC, ebiten.Key4}, 155 | {0xD, ebiten.KeyR}, 156 | {0xE, ebiten.KeyF}, 157 | {0xF, ebiten.KeyV}, 158 | } 159 | 160 | return list 161 | } 162 | --------------------------------------------------------------------------------