subrepo:
  subdir:   "jsnes"
  merged:   "d8021d0"
upstream:
  origin:   "https://github.com/bfirsh/jsnes.git"
  branch:   "master"
  commit:   "d8021d0"
git-subrepo:
  version:  "0.4.9"
  origin:   "???"
  commit:   "???"
This commit is contained in:
Jeremy Penner 2024-10-29 20:12:13 -04:00
parent 1c64fa7436
commit 8dc252d70f
31 changed files with 12041 additions and 0 deletions

13
jsnes/.eslintrc.json Normal file
View file

@ -0,0 +1,13 @@
{
"env": {
"browser": true,
"node": true,
"es6": true,
"commonjs": true
},
"extends": ["eslint:recommended", "prettier"],
"rules": {
"eqeqeq": ["error", "always"],
"no-alert": "error"
}
}

12
jsnes/.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
ignore:
- dependency-name: webpack
versions:
- "< 5"
- ">= 4.0.a"

18
jsnes/.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,18 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '12.x'
- run: yarn
- run: yarn build
- run: yarn test

6
jsnes/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
._*
.DS_Store
/dist
/local-roms
/node_modules
/tmp

12
jsnes/.gitrepo Normal file
View file

@ -0,0 +1,12 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme
;
[subrepo]
remote = https://github.com/bfirsh/jsnes.git
branch = master
commit = d8021d0336cb5c1cf924cd660ecf816bec15c11a
parent = 1c64fa74360920926657bd4670473f68a7e5f74d
method = merge
cmdver = 0.4.9

2
jsnes/.npmignore Normal file
View file

@ -0,0 +1,2 @@
/local-roms
/tmp

13
jsnes/AUTHORS.md Normal file
View file

@ -0,0 +1,13 @@
Authors
=======
* Ben Firshman
Thanks to:
* Jamie Sanders for vNES, the Java emulator that JSNES owes so much to.
* Matt Westcott for JSSpeccy, the original inspiration for JSNES.
* Connor Dunn for a patch that dramatically increased performance on Chrome.
* Jens Lindstrom for some optimisations.
* Rafal Chlodnicki for an Opera fix.
* Ecin Krispie for fixing player 2 controls.

191
jsnes/LICENSE Normal file
View file

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2020 Ben Firshman
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

92
jsnes/README.md Normal file
View file

@ -0,0 +1,92 @@
# JSNES
A JavaScript NES emulator.
It's a library that works in both the browser and Node.js. The browser UI is available at [https://github.com/bfirsh/jsnes-web](https://github.com/bfirsh/jsnes-web).
## Installation
For Node.js or Webpack:
$ npm install jsnes
(Or `yarn add jsnes`.)
In the browser, you can use [unpkg](https://unpkg.com):
```html
<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
```
## Usage
```javascript
// Initialize and set up outputs
var nes = new jsnes.NES({
onFrame: function(frameBuffer) {
// ... write frameBuffer to screen
},
onAudioSample: function(left, right) {
// ... play audio sample
}
});
// Read ROM data from disk (using Node.js APIs, for the sake of this example)
const fs = require('fs');
var romData = fs.readFileSync('path/to/rom.nes', {encoding: 'binary'});
// Load ROM data as a string or byte array
nes.loadROM(romData);
// Run frames at 60 fps, or as fast as you can.
// You are responsible for reliable timing as best you can on your platform.
nes.frame();
nes.frame();
// ...
// Hook up whatever input device you have to the controller.
nes.buttonDown(1, jsnes.Controller.BUTTON_A);
nes.frame();
nes.buttonUp(1, jsnes.Controller.BUTTON_A);
nes.frame();
// ...
```
## Build
To build a distribution:
$ yarn run build
This will create `dist/jsnes.min.js`.
## Running tests
$ yarn test
## Embedding JSNES in a web page
You can use JSNES to embed a playable version of a ROM in a web page. This is handy if you are a homebrew ROM developer and want to put a playable version of your ROM on its web page.
The best implementation is [jsnes-web](https://github.com/bfirsh/jsnes-web) but unfortunately it is not trivial to reuse the code. You'll have to copy and paste the code from that repository, the use the [`<Emulator>`](https://github.com/bfirsh/jsnes-web/blob/master/src/Emulator.js) React component. [Here is a usage example.](https://github.com/bfirsh/jsnes-web/blob/d3c35eec11986412626cbd08668dbac700e08751/src/RunPage.js#L119-L125).
A project for potential contributors (hello!): jsnes-web should be reusable and on NPM! It just needs compiling and bundling.
A more basic example is in the `example/` directory of this repository. Unfortunately this is known to be flawed, and doesn't do timing and sound as well as jsnes-web.
## Formatting code
All code must conform to [Prettier](https://prettier.io/) formatting. The test suite won't pass unless it does.
To automatically format all your code, run:
$ yarn run format
## Maintainers
- [Ben Firshman](http://github.com/bfirsh)
- [Ben Jones](https://github.com/BenShelton)
- [Stephen Hicks](https://github.com/shicks)
- [Alison Saia](https://github.com/allie)
JSNES is based on [James Sanders' vNES](https://github.com/bfirsh/vNES), and owes an awful lot to it. It also wouldn't have happened without [Matt Wescott's JSSpeccy](http://jsspeccy.zxdemo.org/), which sparked the original idea. (Ben, circa 2008: "Hmm, I wonder what else could run in a browser?!")

Binary file not shown.

3
jsnes/example/README.md Normal file
View file

@ -0,0 +1,3 @@
An example app to demonstrate a simple way to embed JSNES.
ROM is by @slembcke: https://github.com/slembcke/InterglacticTransmissing

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Embedding Example</title>
<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
<script type="text/javascript" src="nes-embed.js"></script>
<script>window.onload = function(){nes_load_url("nes-canvas", "InterglacticTransmissing.nes");}</script>
</head>
<body>
<div style="margin: auto; width: 75%;">
<canvas id="nes-canvas" width="256" height="240" style="width: 100%"/>
</div>
<p>DPad: Arrow keys<br/>Start: Return, Select: Tab<br/>A Button: A, B Button: S</p>
</body>
</html>

132
jsnes/example/nes-embed.js Normal file
View file

@ -0,0 +1,132 @@
var SCREEN_WIDTH = 256;
var SCREEN_HEIGHT = 240;
var FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT;
var canvas_ctx, image;
var framebuffer_u8, framebuffer_u32;
var AUDIO_BUFFERING = 512;
var SAMPLE_COUNT = 4*1024;
var SAMPLE_MASK = SAMPLE_COUNT - 1;
var audio_samples_L = new Float32Array(SAMPLE_COUNT);
var audio_samples_R = new Float32Array(SAMPLE_COUNT);
var audio_write_cursor = 0, audio_read_cursor = 0;
var nes = new jsnes.NES({
onFrame: function(framebuffer_24){
for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
},
onAudioSample: function(l, r){
audio_samples_L[audio_write_cursor] = l;
audio_samples_R[audio_write_cursor] = r;
audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
},
});
function onAnimationFrame(){
window.requestAnimationFrame(onAnimationFrame);
image.data.set(framebuffer_u8);
canvas_ctx.putImageData(image, 0, 0);
}
function audio_remain(){
return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK;
}
function audio_callback(event){
var dst = event.outputBuffer;
var len = dst.length;
// Attempt to avoid buffer underruns.
if(audio_remain() < AUDIO_BUFFERING) nes.frame();
var dst_l = dst.getChannelData(0);
var dst_r = dst.getChannelData(1);
for(var i = 0; i < len; i++){
var src_idx = (audio_read_cursor + i) & SAMPLE_MASK;
dst_l[i] = audio_samples_L[src_idx];
dst_r[i] = audio_samples_R[src_idx];
}
audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK;
}
function keyboard(callback, event){
var player = 1;
switch(event.keyCode){
case 38: // UP
callback(player, jsnes.Controller.BUTTON_UP); break;
case 40: // Down
callback(player, jsnes.Controller.BUTTON_DOWN); break;
case 37: // Left
callback(player, jsnes.Controller.BUTTON_LEFT); break;
case 39: // Right
callback(player, jsnes.Controller.BUTTON_RIGHT); break;
case 65: // 'a' - qwerty, dvorak
case 81: // 'q' - azerty
callback(player, jsnes.Controller.BUTTON_A); break;
case 83: // 's' - qwerty, azerty
case 79: // 'o' - dvorak
callback(player, jsnes.Controller.BUTTON_B); break;
case 9: // Tab
callback(player, jsnes.Controller.BUTTON_SELECT); break;
case 13: // Return
callback(player, jsnes.Controller.BUTTON_START); break;
default: break;
}
}
function nes_init(canvas_id){
var canvas = document.getElementById(canvas_id);
canvas_ctx = canvas.getContext("2d");
image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
canvas_ctx.fillStyle = "black";
canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Allocate framebuffer array.
var buffer = new ArrayBuffer(image.data.length);
framebuffer_u8 = new Uint8ClampedArray(buffer);
framebuffer_u32 = new Uint32Array(buffer);
// Setup audio.
var audio_ctx = new window.AudioContext();
var script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
script_processor.onaudioprocess = audio_callback;
script_processor.connect(audio_ctx.destination);
}
function nes_boot(rom_data){
nes.loadROM(rom_data);
window.requestAnimationFrame(onAnimationFrame);
}
function nes_load_data(canvas_id, rom_data){
nes_init(canvas_id);
nes_boot(rom_data);
}
function nes_load_url(canvas_id, path){
nes_init(canvas_id);
var req = new XMLHttpRequest();
req.open("GET", path);
req.overrideMimeType("text/plain; charset=x-user-defined");
req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`);
req.onload = function() {
if (this.status === 200) {
nes_boot(this.responseText);
} else if (this.status === 0) {
// Aborted, so ignore error
} else {
req.onerror();
}
};
req.send();
}
document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)});
document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)});

32
jsnes/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "jsnes",
"version": "1.2.1",
"description": "A JavaScript NES emulator",
"homepage": "https://github.com/bfirsh/jsnes",
"author": "Ben Firshman <ben@firshman.co.uk> (https://fir.sh)",
"main": "src/index.js",
"repository": {
"type": "git",
"url": "git://github.com/bfirsh/jsnes.git"
},
"license": "Apache-2.0",
"scripts": {
"build": "webpack",
"test": "prettier-check src/**/*.js && mocha ./test/*.spec.js",
"test:watch": "mocha -w ./test/*.spec.js",
"prepublish": "npm run build",
"format": "prettier --write src/**/*.js"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-loader": "^2.0.0",
"mocha": "^9.1.1",
"prettier": "^2.0.5",
"prettier-check": "^2.0.0",
"sinon": "^9.0.1",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^3.9.1"
}
}

View file

@ -0,0 +1,75 @@
<!DOCTYPE HTML><html><head>
<title>Concentration Room</title>
<link rel="stylesheet" type="text/css" href="docs/croom.css">
</head><body>
<div id="pgheader"><div class="thereisnosuchthingaspaddingauto">
<h1><img alt="Concentration Room" width="320" height="128" src="docs/croomlogo320.png"></h1>
<ul id="headerlist">
<li><strong>About</strong>
<ul>
<li><a href="#overview">Overview</a>
<li><a href="#requirements">Requirements</a>
<li><a href="#modes">Modes</a>
<li><a href="#faq">FAQ</a>
</ul>
<li><a href="http://pineight.com/croom/dl">Download</a>
</ul>
</div></div><div id="pgbody"><div class="thereisnosuchthingaspaddingauto">
<h2><a name="overview">Overview</a></h2>
<img src="docs/croom_screenshot01.png" style="float:right; margin: 0 0 1em 1em">
<p>
An accident at the biochemical lab has released a neurotoxin,
and you've been quarantined after exposure. Maintain your
sanity by playing a card-matching game.
</p><p>
The table is littered with 10, 20, 36, 52, or 72 face-down cards.
Flip two cards, and if they show the same emblem, you keep them.
If they don't, flip them back.
</p>
<h2><a name="requirements">System Requirements</a></h2>
<p>
Concentration Room is designed for your Nintendo Entertainment System. This version is an NROM-128 (16 KiB PRG, 8 KiB CHR), and it has been tested on a <a title="CompactFlash to NES adapter" href="http://www.retrousb.com/index.php?cPath=24">PowerPak</a>. It also works in PC-based emulators such as <a href="http://nestopia.sourceforge.net/">Nestopia</a> and <a href="http://fceux.com/web/home.html">FCE Ultra</a>.
</p>
<h2><a name="modes">Modes</a></h2>
<dl>
<dt>1 Player Story<dd>
Play solitaire to start to work the toxin out of your system. Then defeat other contaminated technicians and children one on one.
<dt>1 Player Solitaire<dd>
Select a difficulty level, then try to clear the table without having to turn back more than 99 non-matching pairs.
<dt>2 Players<dd>
Two players take turns turning over cards. They can pass one controller back and forth or use one controller each. If a pair doesn't match, the other player presses the A and B Buttons and takes a turn. The first player to take half the pairs wins.
<dt>Vs. CPU<dd>
Like 2 Players, except the second player is controlled by the NES.
</dl>
<h2><a name="faq">FAQ (Fully Anticipated Questions)</a></h2>
<dl>
<dt>How long have you been working on this?<dd>
This is actually my third try. The logo and the earliest background sketch date back to 2000. It got held up because I lacked artistic skill on the 16x16 pixel canvas. The second try in 2007 finalized the appearance of the game, and I did some work on the "emblem designer" that will show up in a future release. In late November 2009, I discovered <a title="Review of Dian Shi Mali on waluigious.com" href="http://www.waluigious.com/2008/09/in-which-dian-shi-ma-li.html"><i>Dian Shi Mali</i></a>, a <a title="Dian Shi Mali article on Wikipedia" href="http://en.wikipedia.org/wiki/Dian_Shi_Mali">gambling simulator</a> for the Famicom (Asian version of the NES) that also uses 16x16 pixel emblems. After a few hours of <a title="Video of Dian Shi Mali play" href="http://www.youtube.com/watch?v=4s1mAPISOzw">pushing Start to rich</a>, I was inspired to create a set of 36 emblems. By then, I was ready to code most of the game in spare time during December 2009.
<dt>Why are you still making games that don't scroll? You're better than that, as I saw in the <a title="Video of a homebrew sidescroller engine" href="http://www.youtube.com/watch?v=GY693NxC9xU">President video</a>.<dd>
I saw it as something simple that I could finish fairly quickly in order to push falling block games off <a href="http://www.pineight.com/">the front page of my web site</a>.
<dt>GameTek already made two other Concentration games on the NES. Why did you make this one?<dd>
The controls in <i>I Can Remember</i> nor <i>Classic Concentration</i> are clunky. Neither of them features a full 72-card deck. And of course, they're not <a title="Free Software Definition (free speech, not free beer)" href="http://www.gnu.org/philosophy/free-sw.html">free software</a>.
<dt>In vs. modes, why end the game at half the cards matched instead of one more than half?<dd>
Pairs early in a game require more skill to clear, and the last pair requires absolutely no skill. For example, a 20-card game tied at 4-4 will always end up 6-4. And at 5-3, the player in the lead likely got more early matches. So if we award no points for the last pair, the first player to reach half always wins.
<dt>What's that font?<dd>
The font in the game's logo is called <a href="http://www.windowfonts.com/fonts/wasted-collection.html">Wasted Collection</a>. The font in <a title="Launcher for small programs for Game Boy Advance" href="http://www.pineight.com/gba/#mbmenu">Multiboot Menu</a> was based on it. The monospace font for menu text originally appeared in the "Who's Cuter" demo and is based on <a href="http://en.wikipedia.org/wiki/Chicago_%28typeface%29">Apple Chicago by Susan Kare</a>. (Another fun font is on <a href="http://www.angelfire.com/stars5/tkcpics2/wildworld/#downloads">this page</a>.)
<dt>Are you a Nazi?<dd>
No, and that's why this game is called Concentration <em>Room,</em> not <a title="National Lampoon video" href="http://www.youtube.com/watch?v=cXeHn9k27Iw">Concentration Camp</a>.
</dl>
<h2>Legal</h2>
<p>
Copyright &copy; 2010 Damian Yerrick &lt;croom&#64;pineight.com&gt;
</p><p>
Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty.
</p><p>
The accompanying program is free software: you can redistribute it and/or modify it under the terms of the <a href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License</a>, version 3 or later. As a special exception, you may copy and distribute exact copies of the program, as published by Damian Yerrick, in iNES or UNIF executable form without source code.
</p><p>
This product is not sponsored or endorsed by Nintendo, Ravensburger, Hasbro, Mattel, Quaker Oats, NBC Universal, GameTek, or Apple.
</p>
</div></div>
</body></html>

BIN
jsnes/roms/croom/croom.nes Normal file

Binary file not shown.

314
jsnes/roms/lj65/README.txt Normal file
View file

@ -0,0 +1,314 @@
_ _ __ ___
| | (_) / / / __|
| | _ / /_ | /__
| | | | | _ \ |___ \
| |_ | | | (_) | .___) |
\__|_| | \___/ \___/
|__/
LJ65
an NES game
by Damian Yerrick
See the legal section below.
_____________________________________________________________________
Introduction
LJ65 is an action puzzle game for NES comparable to the popular
game Tetris(R), except distributed as free software and with more
responsive movement controls.
_____________________________________________________________________
Installing
LJ65 is designed to run on Nintendo Entertainment System (called
Family Computer in Japan) and accurate NES emulators. It is
distributed as source code and an iNES format binary, using mapper
0 (NROM). Separate binaries for NTSC and PAL systems are provided.
This program has been tested on NES using a PowerPak. It also works
on the current versions of Nintendulator, Nestopia, and FCE Ultra.
(Do not use the outdated Nesticle emulator anymore.)
To run LJ65 on an NES without buying a PowerPak, you'll need to
solder together an NES cartridge with at least 16 KB of PRG space
and 4 KB of CHR space. A modded NROM-128 or CNROM board should be
fine. Chris Covell has put together instructions on how to replace
NES Game Paks' mask ROM chips with writable EEPROMs.
http://www.zyx.com/chrisc/solarwarscart.html
To build LJ65 from source code, you will need
* CC65 (from http://www.cc65.org/ but you don't need the
non-free C compiler)
* GNU Make and Coreutils (included with most Linux distributions;
Windows users can use MSYS from http://www.devkitpro.org/)
Modify the makefile to point to where you have CC65 installed.
Then run make. (Windows users can run mk.bat instead, which runs
make in the correct folder.) On a desktop PC from late 2000 with
a Pentium III 866 MHz, recompiling the whole thing takes about one
second. To build some data conversion tools, you'll need a GNU C
compiler such as MinGW; I have included Windows binaries of the
conversion tools for those who want to quickly get into hacking
on LJ65.
_____________________________________________________________________
Game controls
Title screen:
Start: Show playfields.
Game over:
A+B: Join game.
Menu:
Control Pad up, down: Move cursor.
Control Pad left, right: Change option at cursor.
A: Start game.
Game:
Control Pad left, right, down: Move piece.
Control Pad up: Move piece to floor.
Control Pad up, down once landed: Lock piece into place.
A: Rotate piece clockwise.
B: Rotate piece anticlockwise.
Start: Pause game.
_____________________________________________________________________
Play
At first, press Start to skip past each of the informational screens.
Then press Start at the title screen to display the playfields.
At this point, either player can press the A and B buttons at the
same time to begin playing.
The pieces in LJ65 are called tetrominoes. (The word comes from
tetra-, a Greek prefix meaning four, and -omino, as in domino or
pentomino.) Each of the seven tetrominoes is made of four square
blocks and named after a letter of the Latin alphabet that it
resembles:
_ _ ___ ___ _ ___
_______ | |___ ___| | | | _| _| _| |_ |_ |_
|_______| |_____| |_____| |___| |___| |_____| |___|
I J L O S T Z
When you start the game, a tetromino will begin to fall slowly into
the bin. You can move it with the Control Pad and rotate it with
the A or B button.
The goal of LJ65 is to make complete horizontal lines by
packing the pieces into the bin with no holes. If you complete
a line, everything above it will move down a row. If you complete
more than one line with a piece, you get more points.
As you play, the pieces will gradually fall faster, making the game
more difficult. At some point, the pieces will fall so fast that
they appear immediately at the bottom row of the playfield. If you
fill the bin to the top, to the point where more pieces cannot enter,
you "top out" and the game ends.
If you have an overhang in the blocks, you can slide another
piece under it by holding Left or Right as the new piece passes
by the overhang:
_
| |
_| |
|___|
_ _ _ _ _
_| | => _| | | | => _| | |
| _| | _|_| | | _| |
|_| |_| |___| |_|___|
Or in some cases, you can rotate pieces into very tight spaces:
_
_| |
|_ |
|_|
_ ___ _ _ ___ _ ___
| | |_ | => | |_| |_ | => | |___|_ |
| |_ _| | | |_ |_| | | |_ _| |
|___| |___| |___|_|___| |___|_|___|
_____________________________________________________________________
Rotation systems
LJ65 supports two rotation systems, which it calls "Center" and
"Bottom". Center implements rules more familiar to Western players,
while Bottom pleases fans of the Japanese arcade tradition.
In Center, pieces start out with their flat side down, and they
rotate around the center of an imaginary 3x3 or 4x4 cell bounding
box. If this is blocked, try one square to the right, one square to
the left, and finally one square up.
Up locks a piece into place immediately, and down waits for another
press of up or down before locking the piece.
After a piece locks, the next one comes out immediately, but after
the pieces have sped up enough, the next piece waits a bit.
Colors match the so-called Guideline: I is turquoise.
. []. . []. . . . . []. . [][] . []. . . . []. .
[][][] . [][] [][][] [][]. [][]. . [][] . [][] [][].
. . . . []. . []. . []. . . . . . [] [][]. . [].
Figure: T and S rotation in Center
In Bottom, the J, L, S, T, and Z pieces start out with their flat
side up, and they rotate to stay in contact with the bottom of an
imaginary 3x3 cell box. S and Z pieces also keep a block in the
bottom center of this box. If this is blocked by a wall or a block
outside the piece's central column, then try one square to the right,
one square to the left, and finally (in the case of T) one square up.
Down locks on contact, and up waits for another press of up or down
to lock. After a piece locks, the next one waits a bit to come out.
Colors match those from a game with a monkey: I is red.
. . . . []. . . . . []. . . . []. . . . . []. .
[][][] [][]. . []. . [][] . [][] [][]. . [][] [][].
. []. . []. [][][] . []. [][]. . []. [][]. . [].
Figure: T and S rotation in Bottom
_____________________________________________________________________
Scoring
Use up or down on the Control Pad to drop pieces, and you'll get
one point per row that the piece moves down.
You also get points for clearing lines. Clearing more lines
with a single piece is worth more points:
SINGLE (1 line with any piece) 1 * 1 * 100 = 100 points
DOUBLE (2 lines with any piece) 2 * 2 * 100 = 400 points
TRIPLE (3 lines with I, J, or L) 3 * 3 * 100 = 900 points
HOME RUN (4 lines with I only) 4 * 4 * 100 = 1600 points
Making lines with consecutive pieces is called a combo and is
worth even more points. In general, the score for a line clear
is the number of lines cleared with this piece, times the number
of lines cleared so far in this combo, times 100. For example,
a double-triple-single combo is worth a total of 2300 points:
2 lines 2 * 2 * 100 = 200 points
3 lines 3 * 5 * 100 = 1500 points
1 line 1 * 6 * 100 = 600 points
When you start clearing lines, the game shows how many lines you
made in this combo. If you leave a 2-block-wide hole at the side
of the bin, you might manage to make a combo of 12 lines or more.
But then you have to weigh this against keeping your stack low
and earning more drop bonus.
There are some grandmasters who can get millions of points in
some puzzle games. There exists a known corner case in this
game's score computation, and scoring is expected to fail beyond
6,553,000 points.
If two players are playing, and you have GARBAGE turned on in the
menu, and you complete more than one line with a piece, the other
player's field rises by one or more rows:
DOUBLE: 1 line
TRIPLE: 2 lines
HOME RUN: 4 lines
This is not affected by combos.
_____________________________________________________________________
Keypress codes
Some of the lesser-used features of the game are hidden so that
players interested in the most common features don't become confused.
At title screen:
* B + Left hides the ghost piece.
_____________________________________________________________________
Questions
Q: Isn't this a copy of Tetris?
Yes, in part, but we don't believe it infringes Tetris Holding's
copyright. It was developed by people who had not read the source
code of Tetris. We disagree with Tetris Holding's claim of broad
patent-like rights over the game. Any similarity between LJ65 and
Tetris is a consequence of common methods of operation, which are
excluded from U.S. copyright (17 USC 102(b)).
Q: Where's (feature that has appeared in another game)?
If it's mentioned in the "future" list at the bottom of CHANGES.txt,
I know about it, and you may see some of those issues resolved in
the next version. Otherwise, I'd be glad to take suggestions,
provided that they aren't "network play with no lag" or "make the
game just like that Japanese game I saw on YouTube".
Q: Why aren't the blocks square on my TV?
In NTSC, a square pixel is 7/24 of a color subcarrier period wide
in 480i mode or 7/12 of a period in the so-called "240p" mode.
But like the video chipsets in most 8-bit and 16-bit computing
platforms, the NES PPU generates pixels that are not square:
8/12 of a period instead of 7/12. Games for PC, Apple II, or any
other platform with frame buffer video could correct for this by
drawing differently sized tiles, but games for NES are limited to
an 8x8 pixel tile grid. PAL video and widescreen televisions make
the problem even more pronounced.
Q: Why do some pieces change color subtly when they land?
The NES's tile size is 8x8 pixels, but the "attribute table"
assigns palettes to 16x16 pixel areas, or clusters of 2x2 tiles.
Only three colors plus the backdrop color can appear in each
color area. So the game approximates the color of each piece as a
combination of blue, orange, and green throughout the screen.
The MMC5 mapper has ExGrafix, which allows 8x8 pixel color areas.
But the only source of MMC5 hardware is used copies of Castlevania
III: Dracula's Curse and Koei's war sims, unlike the discrete mapper
boards that retrousb.com sells.
Q: Who is the fellow on How to Play, and where are his legs?
Who are you, and where is your tail? ;-)
_____________________________________________________________________
Credits
Program and graphics by Damian Yerrick
Original game design by Alexey Pajitnov
NES assembler toolchain by Ullrich von Bassewitz
NES emulators by Xodnizel, Martin Freij, and Quietust
NES documentation by contributors to http://nesdevwiki.org/
Music:
TEMP is "Tetris New Melody (OCRemoved)" by Am.Fm.GM
K.231 is "Leck mich im Arsch" by Wolfgang A. Mozart
_____________________________________________________________________
Legal
Copyright (c) 2009 Damian Yerrick
This manual is under the following license:
This work is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this work.
Permission is granted to anyone to use this work for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:
1. The origin of this work must not be misrepresented; you
must not claim that you wrote the original work. If you use
this work in a product, an acknowledgment in the product
documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such,
and must not be misrepresented as being the original work.
3. This notice may not be removed or altered from any
source distribution.
The term "source" refers to the preferred form of a work for making
changes to it.
The LJ65 software described by this manual is distributed under
the GNU General Public License, version 2 or later, with ABSOLUTELY
NO WARRANTY. See GPL.txt for details.
LJ65 is not a Tetris product and is not endorsed by Tetris Holding.

BIN
jsnes/roms/lj65/lj65.nes Normal file

Binary file not shown.

27
jsnes/src/controller.js Normal file
View file

@ -0,0 +1,27 @@
var Controller = function () {
this.state = new Array(8);
for (var i = 0; i < this.state.length; i++) {
this.state[i] = 0x40;
}
};
Controller.BUTTON_A = 0;
Controller.BUTTON_B = 1;
Controller.BUTTON_SELECT = 2;
Controller.BUTTON_START = 3;
Controller.BUTTON_UP = 4;
Controller.BUTTON_DOWN = 5;
Controller.BUTTON_LEFT = 6;
Controller.BUTTON_RIGHT = 7;
Controller.prototype = {
buttonDown: function (key) {
this.state[key] = 0x41;
},
buttonUp: function (key) {
this.state[key] = 0x40;
},
};
module.exports = Controller;

2024
jsnes/src/cpu.js Normal file

File diff suppressed because it is too large Load diff

4
jsnes/src/index.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
Controller: require("./controller"),
NES: require("./nes"),
};

1518
jsnes/src/mappers.js Normal file

File diff suppressed because it is too large Load diff

222
jsnes/src/nes.js Normal file
View file

@ -0,0 +1,222 @@
var CPU = require("./cpu");
var Controller = require("./controller");
var PPU = require("./ppu");
var PAPU = require("./papu");
var ROM = require("./rom");
var NES = function (opts) {
this.opts = {
onFrame: function () {},
onAudioSample: null,
onStatusUpdate: function () {},
onBatteryRamWrite: function () {},
// FIXME: not actually used except for in PAPU
preferredFrameRate: 60,
emulateSound: true,
sampleRate: 48000, // Sound sample rate in hz
};
if (typeof opts !== "undefined") {
var key;
for (key in this.opts) {
if (typeof opts[key] !== "undefined") {
this.opts[key] = opts[key];
}
}
}
this.frameTime = 1000 / this.opts.preferredFrameRate;
this.ui = {
writeFrame: this.opts.onFrame,
updateStatus: this.opts.onStatusUpdate,
};
this.cpu = new CPU(this);
this.ppu = new PPU(this);
this.papu = new PAPU(this);
this.mmap = null; // set in loadROM()
this.controllers = {
1: new Controller(),
2: new Controller(),
};
this.ui.updateStatus("Ready to load a ROM.");
this.frame = this.frame.bind(this);
this.buttonDown = this.buttonDown.bind(this);
this.buttonUp = this.buttonUp.bind(this);
this.zapperMove = this.zapperMove.bind(this);
this.zapperFireDown = this.zapperFireDown.bind(this);
this.zapperFireUp = this.zapperFireUp.bind(this);
};
NES.prototype = {
fpsFrameCount: 0,
romData: null,
break: false,
// Set break to true to stop frame loop.
stop: function () {
this.break = true;
},
// Resets the system
reset: function () {
if (this.mmap !== null) {
this.mmap.reset();
}
this.cpu.reset();
this.ppu.reset();
this.papu.reset();
this.lastFpsTime = null;
this.fpsFrameCount = 0;
this.break = false;
},
frame: function () {
this.ppu.startFrame();
var cycles = 0;
var emulateSound = this.opts.emulateSound;
var cpu = this.cpu;
var ppu = this.ppu;
var papu = this.papu;
FRAMELOOP: for (;;) {
if (this.break) break;
if (cpu.cyclesToHalt === 0) {
// Execute a CPU instruction
cycles = cpu.emulate();
if (emulateSound) {
papu.clockFrameCounter(cycles);
}
cycles *= 3;
} else {
if (cpu.cyclesToHalt > 8) {
cycles = 24;
if (emulateSound) {
papu.clockFrameCounter(8);
}
cpu.cyclesToHalt -= 8;
} else {
cycles = cpu.cyclesToHalt * 3;
if (emulateSound) {
papu.clockFrameCounter(cpu.cyclesToHalt);
}
cpu.cyclesToHalt = 0;
}
}
for (; cycles > 0; cycles--) {
if (
ppu.curX === ppu.spr0HitX &&
ppu.f_spVisibility === 1 &&
ppu.scanline - 21 === ppu.spr0HitY
) {
// Set sprite 0 hit flag:
ppu.setStatusFlag(ppu.STATUS_SPRITE0HIT, true);
}
if (ppu.requestEndFrame) {
ppu.nmiCounter--;
if (ppu.nmiCounter === 0) {
ppu.requestEndFrame = false;
ppu.startVBlank();
break FRAMELOOP;
}
}
ppu.curX++;
if (ppu.curX === 341) {
ppu.curX = 0;
ppu.endScanline();
}
}
}
this.fpsFrameCount++;
},
buttonDown: function (controller, button) {
this.controllers[controller].buttonDown(button);
},
buttonUp: function (controller, button) {
this.controllers[controller].buttonUp(button);
},
zapperMove: function (x, y) {
if (!this.mmap) return;
this.mmap.zapperX = x;
this.mmap.zapperY = y;
},
zapperFireDown: function () {
if (!this.mmap) return;
this.mmap.zapperFired = true;
},
zapperFireUp: function () {
if (!this.mmap) return;
this.mmap.zapperFired = false;
},
getFPS: function () {
var now = +new Date();
var fps = null;
if (this.lastFpsTime) {
fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000);
}
this.fpsFrameCount = 0;
this.lastFpsTime = now;
return fps;
},
reloadROM: function () {
if (this.romData !== null) {
this.loadROM(this.romData);
}
},
// Loads a ROM file into the CPU and PPU.
// The ROM file is validated first.
loadROM: function (data) {
// Load ROM file:
this.rom = new ROM(this);
this.rom.load(data);
this.reset();
this.mmap = this.rom.createMapper();
this.mmap.loadROM();
this.ppu.setMirroring(this.rom.getMirroringType());
this.romData = data;
},
setFramerate: function (rate) {
this.opts.preferredFrameRate = rate;
this.frameTime = 1000 / rate;
this.papu.setSampleRate(this.opts.sampleRate, false);
},
toJSON: function () {
return {
// romData: this.romData,
cpu: this.cpu.toJSON(),
mmap: this.mmap.toJSON(),
ppu: this.ppu.toJSON(),
papu: this.papu.toJSON(),
};
},
fromJSON: function (s) {
this.reset();
// this.romData = s.romData;
this.cpu.fromJSON(s.cpu);
this.mmap.fromJSON(s.mmap);
this.ppu.fromJSON(s.ppu);
this.papu.fromJSON(s.papu);
},
};
module.exports = NES;

1559
jsnes/src/papu.js Normal file

File diff suppressed because it is too large Load diff

1753
jsnes/src/ppu.js Normal file

File diff suppressed because it is too large Load diff

204
jsnes/src/rom.js Normal file
View file

@ -0,0 +1,204 @@
var Mappers = require("./mappers");
var Tile = require("./tile");
var ROM = function (nes) {
this.nes = nes;
this.mapperName = new Array(92);
for (var i = 0; i < 92; i++) {
this.mapperName[i] = "Unknown Mapper";
}
this.mapperName[0] = "Direct Access";
this.mapperName[1] = "Nintendo MMC1";
this.mapperName[2] = "UNROM";
this.mapperName[3] = "CNROM";
this.mapperName[4] = "Nintendo MMC3";
this.mapperName[5] = "Nintendo MMC5";
this.mapperName[6] = "FFE F4xxx";
this.mapperName[7] = "AOROM";
this.mapperName[8] = "FFE F3xxx";
this.mapperName[9] = "Nintendo MMC2";
this.mapperName[10] = "Nintendo MMC4";
this.mapperName[11] = "Color Dreams Chip";
this.mapperName[12] = "FFE F6xxx";
this.mapperName[15] = "100-in-1 switch";
this.mapperName[16] = "Bandai chip";
this.mapperName[17] = "FFE F8xxx";
this.mapperName[18] = "Jaleco SS8806 chip";
this.mapperName[19] = "Namcot 106 chip";
this.mapperName[20] = "Famicom Disk System";
this.mapperName[21] = "Konami VRC4a";
this.mapperName[22] = "Konami VRC2a";
this.mapperName[23] = "Konami VRC2a";
this.mapperName[24] = "Konami VRC6";
this.mapperName[25] = "Konami VRC4b";
this.mapperName[32] = "Irem G-101 chip";
this.mapperName[33] = "Taito TC0190/TC0350";
this.mapperName[34] = "32kB ROM switch";
this.mapperName[64] = "Tengen RAMBO-1 chip";
this.mapperName[65] = "Irem H-3001 chip";
this.mapperName[66] = "GNROM switch";
this.mapperName[67] = "SunSoft3 chip";
this.mapperName[68] = "SunSoft4 chip";
this.mapperName[69] = "SunSoft5 FME-7 chip";
this.mapperName[71] = "Camerica chip";
this.mapperName[78] = "Irem 74HC161/32-based";
this.mapperName[91] = "Pirate HK-SF3 chip";
};
ROM.prototype = {
// Mirroring types:
VERTICAL_MIRRORING: 0,
HORIZONTAL_MIRRORING: 1,
FOURSCREEN_MIRRORING: 2,
SINGLESCREEN_MIRRORING: 3,
SINGLESCREEN_MIRRORING2: 4,
SINGLESCREEN_MIRRORING3: 5,
SINGLESCREEN_MIRRORING4: 6,
CHRROM_MIRRORING: 7,
header: null,
rom: null,
vrom: null,
vromTile: null,
romCount: null,
vromCount: null,
mirroring: null,
batteryRam: null,
trainer: null,
fourScreen: null,
mapperType: null,
valid: false,
load: function (data) {
var i, j, v;
if (data.indexOf("NES\x1a") === -1) {
throw new Error("Not a valid NES ROM.");
}
this.header = new Array(16);
for (i = 0; i < 16; i++) {
this.header[i] = data.charCodeAt(i) & 0xff;
}
this.romCount = this.header[4];
this.vromCount = this.header[5] * 2; // Get the number of 4kB banks, not 8kB
this.mirroring = (this.header[6] & 1) !== 0 ? 1 : 0;
this.batteryRam = (this.header[6] & 2) !== 0;
this.trainer = (this.header[6] & 4) !== 0;
this.fourScreen = (this.header[6] & 8) !== 0;
this.mapperType = (this.header[6] >> 4) | (this.header[7] & 0xf0);
/* TODO
if (this.batteryRam)
this.loadBatteryRam();*/
// Check whether byte 8-15 are zero's:
var foundError = false;
for (i = 8; i < 16; i++) {
if (this.header[i] !== 0) {
foundError = true;
break;
}
}
if (foundError) {
this.mapperType &= 0xf; // Ignore byte 7
}
// Load PRG-ROM banks:
this.rom = new Array(this.romCount);
var offset = 16;
for (i = 0; i < this.romCount; i++) {
this.rom[i] = new Array(16384);
for (j = 0; j < 16384; j++) {
if (offset + j >= data.length) {
break;
}
this.rom[i][j] = data.charCodeAt(offset + j) & 0xff;
}
offset += 16384;
}
// Load CHR-ROM banks:
this.vrom = new Array(this.vromCount);
for (i = 0; i < this.vromCount; i++) {
this.vrom[i] = new Array(4096);
for (j = 0; j < 4096; j++) {
if (offset + j >= data.length) {
break;
}
this.vrom[i][j] = data.charCodeAt(offset + j) & 0xff;
}
offset += 4096;
}
// Create VROM tiles:
this.vromTile = new Array(this.vromCount);
for (i = 0; i < this.vromCount; i++) {
this.vromTile[i] = new Array(256);
for (j = 0; j < 256; j++) {
this.vromTile[i][j] = new Tile();
}
}
// Convert CHR-ROM banks to tiles:
var tileIndex;
var leftOver;
for (v = 0; v < this.vromCount; v++) {
for (i = 0; i < 4096; i++) {
tileIndex = i >> 4;
leftOver = i % 16;
if (leftOver < 8) {
this.vromTile[v][tileIndex].setScanline(
leftOver,
this.vrom[v][i],
this.vrom[v][i + 8]
);
} else {
this.vromTile[v][tileIndex].setScanline(
leftOver - 8,
this.vrom[v][i - 8],
this.vrom[v][i]
);
}
}
}
this.valid = true;
},
getMirroringType: function () {
if (this.fourScreen) {
return this.FOURSCREEN_MIRRORING;
}
if (this.mirroring === 0) {
return this.HORIZONTAL_MIRRORING;
}
return this.VERTICAL_MIRRORING;
},
getMapperName: function () {
if (this.mapperType >= 0 && this.mapperType < this.mapperName.length) {
return this.mapperName[this.mapperType];
}
return "Unknown Mapper, " + this.mapperType;
},
mapperSupported: function () {
return typeof Mappers[this.mapperType] !== "undefined";
},
createMapper: function () {
if (this.mapperSupported()) {
return new Mappers[this.mapperType](this.nes);
} else {
throw new Error(
"This ROM uses a mapper not supported by JSNES: " +
this.getMapperName() +
"(" +
this.mapperType +
")"
);
}
},
};
module.exports = ROM;

198
jsnes/src/tile.js Normal file
View file

@ -0,0 +1,198 @@
var Tile = function () {
// Tile data:
this.pix = new Array(64);
this.fbIndex = null;
this.tIndex = null;
this.x = null;
this.y = null;
this.w = null;
this.h = null;
this.incX = null;
this.incY = null;
this.palIndex = null;
this.tpri = null;
this.c = null;
this.initialized = false;
this.opaque = new Array(8);
};
Tile.prototype = {
setBuffer: function (scanline) {
for (this.y = 0; this.y < 8; this.y++) {
this.setScanline(this.y, scanline[this.y], scanline[this.y + 8]);
}
},
setScanline: function (sline, b1, b2) {
this.initialized = true;
this.tIndex = sline << 3;
for (this.x = 0; this.x < 8; this.x++) {
this.pix[this.tIndex + this.x] =
((b1 >> (7 - this.x)) & 1) + (((b2 >> (7 - this.x)) & 1) << 1);
if (this.pix[this.tIndex + this.x] === 0) {
this.opaque[sline] = false;
}
}
},
render: function (
buffer,
srcx1,
srcy1,
srcx2,
srcy2,
dx,
dy,
palAdd,
palette,
flipHorizontal,
flipVertical,
pri,
priTable
) {
if (dx < -7 || dx >= 256 || dy < -7 || dy >= 240) {
return;
}
this.w = srcx2 - srcx1;
this.h = srcy2 - srcy1;
if (dx < 0) {
srcx1 -= dx;
}
if (dx + srcx2 >= 256) {
srcx2 = 256 - dx;
}
if (dy < 0) {
srcy1 -= dy;
}
if (dy + srcy2 >= 240) {
srcy2 = 240 - dy;
}
if (!flipHorizontal && !flipVertical) {
this.fbIndex = (dy << 8) + dx;
this.tIndex = 0;
for (this.y = 0; this.y < 8; this.y++) {
for (this.x = 0; this.x < 8; this.x++) {
if (
this.x >= srcx1 &&
this.x < srcx2 &&
this.y >= srcy1 &&
this.y < srcy2
) {
this.palIndex = this.pix[this.tIndex];
this.tpri = priTable[this.fbIndex];
if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
//console.log("Rendering upright tile to buffer");
buffer[this.fbIndex] = palette[this.palIndex + palAdd];
this.tpri = (this.tpri & 0xf00) | pri;
priTable[this.fbIndex] = this.tpri;
}
}
this.fbIndex++;
this.tIndex++;
}
this.fbIndex -= 8;
this.fbIndex += 256;
}
} else if (flipHorizontal && !flipVertical) {
this.fbIndex = (dy << 8) + dx;
this.tIndex = 7;
for (this.y = 0; this.y < 8; this.y++) {
for (this.x = 0; this.x < 8; this.x++) {
if (
this.x >= srcx1 &&
this.x < srcx2 &&
this.y >= srcy1 &&
this.y < srcy2
) {
this.palIndex = this.pix[this.tIndex];
this.tpri = priTable[this.fbIndex];
if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
buffer[this.fbIndex] = palette[this.palIndex + palAdd];
this.tpri = (this.tpri & 0xf00) | pri;
priTable[this.fbIndex] = this.tpri;
}
}
this.fbIndex++;
this.tIndex--;
}
this.fbIndex -= 8;
this.fbIndex += 256;
this.tIndex += 16;
}
} else if (flipVertical && !flipHorizontal) {
this.fbIndex = (dy << 8) + dx;
this.tIndex = 56;
for (this.y = 0; this.y < 8; this.y++) {
for (this.x = 0; this.x < 8; this.x++) {
if (
this.x >= srcx1 &&
this.x < srcx2 &&
this.y >= srcy1 &&
this.y < srcy2
) {
this.palIndex = this.pix[this.tIndex];
this.tpri = priTable[this.fbIndex];
if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
buffer[this.fbIndex] = palette[this.palIndex + palAdd];
this.tpri = (this.tpri & 0xf00) | pri;
priTable[this.fbIndex] = this.tpri;
}
}
this.fbIndex++;
this.tIndex++;
}
this.fbIndex -= 8;
this.fbIndex += 256;
this.tIndex -= 16;
}
} else {
this.fbIndex = (dy << 8) + dx;
this.tIndex = 63;
for (this.y = 0; this.y < 8; this.y++) {
for (this.x = 0; this.x < 8; this.x++) {
if (
this.x >= srcx1 &&
this.x < srcx2 &&
this.y >= srcy1 &&
this.y < srcy2
) {
this.palIndex = this.pix[this.tIndex];
this.tpri = priTable[this.fbIndex];
if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
buffer[this.fbIndex] = palette[this.palIndex + palAdd];
this.tpri = (this.tpri & 0xf00) | pri;
priTable[this.fbIndex] = this.tpri;
}
}
this.fbIndex++;
this.tIndex--;
}
this.fbIndex -= 8;
this.fbIndex += 256;
}
}
},
isTransparent: function (x, y) {
return this.pix[(y << 3) + x] === 0;
},
toJSON: function () {
return {
opaque: this.opaque,
pix: this.pix,
};
},
fromJSON: function (s) {
this.opaque = s.opaque;
this.pix = s.pix;
},
};
module.exports = Tile;

25
jsnes/src/utils.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = {
copyArrayElements: function (src, srcPos, dest, destPos, length) {
for (var i = 0; i < length; ++i) {
dest[destPos + i] = src[srcPos + i];
}
},
copyArray: function (src) {
return src.slice(0);
},
fromJSON: function (obj, state) {
for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) {
obj[obj.JSON_PROPERTIES[i]] = state[obj.JSON_PROPERTIES[i]];
}
},
toJSON: function (obj) {
var state = {};
for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) {
state[obj.JSON_PROPERTIES[i]] = obj[obj.JSON_PROPERTIES[i]];
}
return state;
},
};

72
jsnes/test/nes.spec.js Normal file
View file

@ -0,0 +1,72 @@
var assert = require("chai").assert;
var fs = require("fs");
var NES = require("../src/nes");
var sinon = require("sinon");
describe("NES", function() {
it("can be initialized", function() {
var nes = new NES();
});
it("loads a ROM and runs a frame", function(done) {
var onFrame = sinon.spy();
var nes = new NES({ onFrame: onFrame });
fs.readFile("roms/croom/croom.nes", function(err, data) {
if (err) return done(err);
nes.loadROM(data.toString("binary"));
nes.frame();
assert(onFrame.calledOnce);
assert.isArray(onFrame.args[0][0]);
assert.lengthOf(onFrame.args[0][0], 256 * 240);
done();
});
});
it("generates the correct frame buffer", function(done) {
var onFrame = sinon.spy();
var nes = new NES({ onFrame: onFrame });
fs.readFile("roms/croom/croom.nes", function(err, data) {
if (err) return done(err);
nes.loadROM(data.toString("binary"));
// Check the first index of a white pixel on the first 6 frames of
// output. Croom only uses 2 colors on the initial screen which makes
// it easy to detect. Comparing full snapshots of each frame takes too
// long.
var expectedIndexes = [-1, -1, -1, 2056, 4104, 4104];
for (var i = 0; i < 6; i++) {
nes.frame();
assert.equal(onFrame.lastCall.args[0].indexOf(0xFFFFFF), expectedIndexes[i]);
}
done();
});
});
describe("#loadROM()", function() {
it("throws an error given an invalid ROM", function() {
var nes = new NES();
assert.throws(function() {
nes.loadROM("foo");
}, "Not a valid NES ROM.");
});
});
describe("#getFPS()", function() {
var nes = new NES();
before(function(done) {
fs.readFile("roms/croom/croom.nes", function(err, data) {
if (err) return done(err);
nes.loadROM(data.toString("binary"));
done();
});
});
it("returns an FPS count when frames have been run", function() {
assert.isNull(nes.getFPS());
nes.frame();
nes.frame();
var fps = nes.getFPS();
assert.isNumber(fps);
assert.isAbove(fps, 0);
});
});
});

37
jsnes/webpack.config.js Normal file
View file

@ -0,0 +1,37 @@
var path = require("path");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
entry: {
jsnes: "./src/index.js",
"jsnes.min": "./src/index.js",
},
devtool: "source-map",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
library: "jsnes",
libraryTarget: "umd",
umdNamedDefine: true,
},
module: {
rules: [
{
test: /\.js$/,
enforce: "pre",
exclude: /node_modules/,
use: [
{
loader: "eslint-loader",
},
],
},
],
},
plugins: [
new UglifyJsPlugin({
include: /\.min\.js$/,
sourceMap: true,
}),
],
};

3465
jsnes/yarn.lock Normal file

File diff suppressed because it is too large Load diff