Added kidney_labe and Cyste_kid

This commit is contained in:
verboomp
2026-04-16 08:14:20 +02:00
parent aa66c030f8
commit 9cc8ac8cad
40 changed files with 6762 additions and 0 deletions

67
kidney_lab/CLAUDE.md Normal file
View File

@@ -0,0 +1,67 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Running the game
Open `index.html` directly in a browser — there is no build step, no bundler, and no package manager. All JS files are loaded as plain `<script>` tags in dependency order.
For local development use any static file server, e.g.:
```bash
python3 -m http.server 8080
# then open http://localhost:8080
```
## Architecture
The game is split into seven files. Load order in `index.html` reflects the dependency chain:
```
settings.js → player.js → conveyor.js → lab.js → highscore.js → game.js → intro.js
```
### File responsibilities
| File | Exports | Role |
|---|---|---|
| `settings.js` | `SETTINGS` (frozen object) | All numeric constants and colours. No side-effects. Tweak values here only. |
| `player.js` | `Player` class | Grid position (`col` 0-2, `row` 0-2), inventory, movement cooldown, walk animation. No rendering. |
| `conveyor.js` | `LeftConveyorSystem`, `RightConveyorSystem`, `Good`, `Medication` classes + state enums | Belt physics and item lifecycle. No rendering. |
| `lab.js` | `Lab` class, `LabState` enum | Accepts deposited goods, detects specials, runs analysis timer, fires `onMedicationReady` callback. No rendering. |
| `game.js` | `Game.init(name1, name2)` | Game loop, all canvas rendering, input handling, 2P split-screen logic. Calls `HighScoreScreen.show()` on time-out. |
| `intro.js` | `IntroScreen.show()` | Two-step intro: player count selection → name entry → `Game.init()`. |
| `highscore.js` | `HighScoreScreen.show(names, scores, playerCount)` | Persists top-10 to `localStorage` (`kidneylab_scores`), renders results. Both 1P (scalar) and 2P (array) calls are supported. |
### 2-player split-screen (game.js)
All mutable state is held in per-player arrays (`gPlayers[i]`, `gScores[i]`, etc.). The `RS` proxy object exposes `_pIdx`-indexed getters so `renderOneSide()` can be called twice without duplication.
P2's half is drawn with a mirror transform (`ctx.translate(W, 0); ctx.scale(-1, 1)`). Text inside that transform must go through the `ft(text, x, y)` helper, which applies a local `scale(-1, 1)` to cancel the outer flip.
P2 keyboard input (WASD) has left/right swapped to match the mirrored visual: pressing visual-left (A) calls `move('right')`. The virtual joystick applies the same swap via the `swapLR` flag in `makeJoystick()`.
### Interaction model
Interactions are purely position-based — no action button. `handleInteractionsFor(pIdx)` runs every frame:
- `col === 0` → attempt pickup from left belt at current row
- `col === PLAYER_COLS - 1, row === 2` → deposit all carried goods to lab
- `col === PLAYER_COLS - 1, row === 1` → pick up medication from right belt
- `col === PLAYER_COLS - 1, row === 0` → deliver medication to patient
### Layout constants (all in `settings.js`)
```
x: 0 350 600 900
|←LEFT→ |←MIDDLE → |←RIGHT →|
belts player grid lab/belt/patient
```
Rows are shared vertically: `BELT_ROWS: [155, 300, 445]` (top→bottom, y-centre).
Player pixel position is computed in `Player.pixelX()` — evenly distributed across the middle zone with a 30 px margin from each edge.
### Mobile joystick
`makeJoystick()` in `game.js` builds a virtual joystick per player and appends it to the game screen div. The joystick uses `position: fixed` anchored via `.joy-left` / `.joy-right` CSS classes. It is hidden on non-touch devices via `@media (pointer: coarse)`. Repeated movement while held is driven by `setInterval` at 160 ms; the player's own `moveCooldown` (180 ms) naturally gates actual step rate.

637
kidney_lab/css/styles.css Normal file
View File

@@ -0,0 +1,637 @@
/* ═══════════════════════════════════════════════════════════════
styles.css — Mario Lab (MW-56 Game & Watch clone)
All look-and-feel: layout, typography, colours, animations.
No game logic lives here.
═══════════════════════════════════════════════════════════════ */
/* ── Reset & base ────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #06101e;
--surface: #0d1e38;
--surface2: #10243f;
--border: #1a3a60;
--border2: #2a5a90;
--text: #88ccff;
--text-dim: #4a7aaa;
--text-bright:#cceeff;
--gold: #ffdd44;
--green: #44ffaa;
--red: #ff4455;
--accent: #4499ff;
--font-mono: "Courier New", "Lucida Console", monospace;
}
html, body {
height: 100%;
width: 100%;
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
overflow: hidden;
user-select: none;
}
/* ── Screen system ───────────────────────────────────────────── */
#app {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.screen {
display: none;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
overflow-y: auto;
}
.screen.active {
display: flex;
}
/* ── Game screen: canvas centred ─────────────────────────────── */
#game-screen {
gap: 10px;
background: var(--bg);
}
#game-canvas {
display: block;
border: 2px solid var(--border2);
border-radius: 8px;
box-shadow:
0 0 0 4px var(--surface),
0 0 30px rgba(68,153,255,0.25),
0 0 60px rgba(0,0,0,0.6);
outline: none;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
.controls-legend {
font-size: 11px;
color: var(--text-dim);
text-align: center;
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
}
.controls-legend kbd {
display: inline-block;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 5px;
font-size: 10px;
color: var(--text);
}
/* ── Shared button style ─────────────────────────────────────── */
.btn-primary {
display: inline-block;
background: linear-gradient(135deg, #1a5caa 0%, #0a3070 100%);
color: #cceeff;
border: 2px solid var(--border2);
border-radius: 6px;
padding: 12px 36px;
font-family: var(--font-mono);
font-size: 16px;
font-weight: bold;
letter-spacing: 3px;
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s, transform 0.1s;
text-transform: uppercase;
box-shadow: 0 0 16px rgba(68,153,255,0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #2a7aee 0%, #1a4aa0 100%);
box-shadow: 0 0 24px rgba(68,153,255,0.55);
}
.btn-primary:active {
transform: scale(0.97);
}
/* ══════════════════════════════════════════════════════════════
INTRO SCREEN
══════════════════════════════════════════════════════════════ */
#intro-screen {
background: radial-gradient(ellipse at center, #0d1e38 0%, #06101e 100%);
}
.intro-inner {
max-width: 560px;
width: 90%;
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px 0;
}
/* Logo block */
.intro-logo {
text-align: center;
line-height: 1.1;
}
.logo-gw {
display: block;
font-size: 13px;
letter-spacing: 6px;
color: var(--text-dim);
text-transform: uppercase;
}
.logo-title {
display: block;
font-size: clamp(32px, 8vw, 54px);
font-weight: bold;
letter-spacing: 8px;
color: var(--text-bright);
text-shadow:
0 0 20px rgba(68,153,255,0.7),
0 0 40px rgba(68,153,255,0.4);
animation: title-pulse 3s ease-in-out infinite;
}
.logo-model {
display: block;
font-size: 14px;
letter-spacing: 4px;
color: var(--gold);
margin-top: 4px;
}
@keyframes title-pulse {
0%, 100% { text-shadow: 0 0 20px rgba(68,153,255,0.7), 0 0 40px rgba(68,153,255,0.4); }
50% { text-shadow: 0 0 30px rgba(68,200,255,1), 0 0 60px rgba(68,153,255,0.7); }
}
/* Description */
.intro-description {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 18px;
font-size: 13px;
line-height: 1.7;
color: var(--text);
}
.intro-description strong {
color: var(--gold);
}
/* Rules */
.intro-rules {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 18px;
}
.intro-rules table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.intro-rules td {
padding: 4px 6px;
color: var(--text);
}
.intro-rules .pts {
text-align: right;
font-weight: bold;
font-size: 13px;
}
.intro-rules .pos { color: var(--green); }
.intro-rules .neg { color: var(--red); }
.rule-note {
margin-top: 10px;
font-size: 11px;
color: var(--text-dim);
line-height: 1.6;
}
.rule-note strong {
color: var(--text-bright);
}
/* Controls hint */
.intro-controls {
font-size: 12px;
color: var(--text-dim);
line-height: 1.8;
text-align: center;
}
.intro-controls kbd {
display: inline-block;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 6px;
font-size: 11px;
color: var(--text);
}
.intro-controls em {
color: var(--accent);
font-style: normal;
}
/* Form */
.intro-form {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
}
.intro-form label {
font-size: 12px;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
}
.intro-form input[type="text"] {
background: var(--surface2);
border: 2px solid var(--border2);
border-radius: 6px;
color: #ffffff;
font-family: var(--font-mono);
font-size: 20px;
font-weight: bold;
letter-spacing: 4px;
padding: 10px 20px;
text-align: center;
text-transform: uppercase;
width: 240px;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.intro-form input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 16px rgba(68,153,255,0.4);
}
.intro-form input[type="text"]::placeholder {
color: var(--text-dim);
letter-spacing: 2px;
}
/* Player count selection */
.player-select {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.player-select-label {
font-size: 11px;
letter-spacing: 4px;
color: var(--text-dim);
text-transform: uppercase;
}
.player-select-btns {
display: flex;
gap: 20px;
}
.btn-player-count {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 110px;
height: 100px;
background: var(--surface2);
border: 2px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: var(--font-mono);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.1s;
gap: 4px;
}
.btn-player-count:hover {
background: linear-gradient(135deg, #1a5caa 0%, #0a3070 100%);
border-color: var(--accent);
box-shadow: 0 0 20px rgba(68,153,255,0.45);
}
.btn-player-count:active {
transform: scale(0.96);
}
.btn-player-count .pcount {
font-size: 42px;
font-weight: bold;
color: var(--gold);
line-height: 1;
}
.btn-player-count .pcount-label {
font-size: 11px;
letter-spacing: 3px;
color: var(--text-dim);
}
/* Mode badge & name rows (step 2) */
.mode-badge {
font-size: 11px;
letter-spacing: 4px;
color: var(--gold);
background: rgba(255,221,68,0.1);
border: 1px solid rgba(255,221,68,0.3);
border-radius: 4px;
padding: 4px 14px;
margin-bottom: 4px;
}
.name-row {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.name-row label {
font-size: 11px;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
}
.form-actions {
display: flex;
gap: 14px;
align-items: center;
margin-top: 4px;
}
/* Secondary (back) button */
.btn-secondary {
display: inline-block;
background: transparent;
color: var(--text-dim);
border: 2px solid var(--border);
border-radius: 6px;
padding: 10px 20px;
font-family: var(--font-mono);
font-size: 13px;
letter-spacing: 2px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.btn-secondary:hover {
border-color: var(--border2);
color: var(--text);
}
/* ══════════════════════════════════════════════════════════════
HIGH SCORE SCREEN
══════════════════════════════════════════════════════════════ */
#highscore-screen {
background: radial-gradient(ellipse at center, #0d1e38 0%, #06101e 100%);
}
.hs-inner {
max-width: 580px;
width: 90%;
display: flex;
flex-direction: column;
gap: 20px;
padding: 24px 0;
}
.hs-logo {
text-align: center;
line-height: 1.1;
}
.hs-result {
text-align: center;
background: var(--surface);
border: 2px solid var(--border);
border-radius: 10px;
padding: 20px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.hs-result.hs-gold {
border-color: var(--gold);
box-shadow: 0 0 24px rgba(255,221,68,0.35);
animation: gold-pulse 2s ease-in-out infinite;
}
@keyframes gold-pulse {
0%, 100% { box-shadow: 0 0 24px rgba(255,221,68,0.35); }
50% { box-shadow: 0 0 40px rgba(255,221,68,0.65); }
}
.hs-rank-label {
font-size: 14px;
font-weight: bold;
color: var(--gold);
letter-spacing: 2px;
}
.hs-player {
font-size: 22px;
font-weight: bold;
color: var(--text-bright);
letter-spacing: 5px;
}
.hs-score-display {
font-size: 56px;
font-weight: bold;
color: var(--gold);
line-height: 1;
text-shadow: 0 0 20px rgba(255,221,68,0.6);
}
.hs-score-label {
font-size: 12px;
letter-spacing: 4px;
color: var(--text-dim);
text-transform: uppercase;
}
/* Table */
.hs-table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
overflow-x: auto;
}
.hs-heading {
font-size: 12px;
letter-spacing: 4px;
color: var(--text-dim);
text-align: center;
margin-bottom: 12px;
text-transform: uppercase;
}
.hs-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.hs-table th {
text-align: left;
color: var(--text-dim);
font-size: 10px;
letter-spacing: 2px;
padding: 4px 8px 8px;
border-bottom: 1px solid var(--border);
}
.hs-table td {
padding: 6px 8px;
color: var(--text);
border-bottom: 1px solid rgba(26,58,96,0.4);
}
.hs-table .rank-col { color: var(--text-dim); width: 30px; }
.hs-table .name-col { font-weight: bold; letter-spacing: 2px; }
.hs-table .score-col { color: var(--gold); text-align: right; }
.hs-table .date-col { color: var(--text-dim); font-size: 11px; text-align: right; }
.hs-table tr.my-row td {
background: rgba(68,153,255,0.12);
color: #cceeff;
}
.hs-table tr.my-row .score-col { color: var(--gold); }
.hs-actions {
display: flex;
justify-content: center;
}
/* ══════════════════════════════════════════════════════════════
MOBILE JOYSTICK CONTROLS
══════════════════════════════════════════════════════════════ */
.joystick-wrap {
position: fixed;
bottom: 24px;
/* horizontal position is set entirely by .joy-left / .joy-right */
display: none; /* hidden by default on desktop/mouse */
flex-direction: column;
align-items: center;
gap: 6px;
z-index: 100;
}
/* Only show on touch/coarse-pointer devices (phones, tablets) */
@media (pointer: coarse) {
.joystick-wrap { display: flex; }
}
/* Explicit left AND right on every variant so fixed positioning
never falls back to the element's static (centred) position */
.joystick-wrap.joy-right { right: 24px; left: unset; }
.joystick-wrap.joy-left { left: 24px; right: unset; }
.joystick-label {
font-size: 10px;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
}
/* Outer ring */
.joystick-base {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(13, 30, 56, 0.75);
border: 2px solid var(--border2);
box-shadow: 0 0 24px rgba(0,0,0,0.55), inset 0 0 16px rgba(0,0,0,0.3);
position: relative;
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
.joystick-base.active {
border-color: var(--accent);
box-shadow: 0 0 24px rgba(68,153,255,0.35), inset 0 0 16px rgba(0,0,0,0.3);
}
/* Direction hint rings (subtle) */
.joystick-base::before {
content: '';
position: absolute;
inset: 14px;
border-radius: 50%;
border: 1px dashed rgba(68,153,255,0.18);
pointer-events: none;
}
/* Draggable knob */
.joystick-knob {
width: 48px;
height: 48px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #4499ff, #1a4aa0);
border: 2px solid var(--accent);
box-shadow: 0 0 14px rgba(68,153,255,0.55);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
transition: box-shadow 0.1s;
will-change: transform;
}
.joystick-base.active .joystick-knob {
box-shadow: 0 0 22px rgba(68,153,255,0.85);
}
/* ── Scrollbar styling ───────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border2); }

View File

@@ -0,0 +1,40 @@
# Kidney Lab MW-56
> Dieses Spiel ist lose an das **Game & Watch MW-56 Mario Bros.** von Nintendo aus dem Jahr **1983** angelehnt.
> Wer das Original in Aktion sehen möchte, findet es hier: [Game & Watch MW-56 auf YouTube](https://www.youtube.com/watch?v=ajWf49au8mo)
## Das Spiel
Du schlüpfst in die Rolle eines Arztes, der in einem hektischen Labor arbeitet. Auf drei Förderbändern rollen ununterbrochen Nieren heran und es liegt an dir, sie rechtzeitig aufzusammeln, bevor sie vom Band fallen und verloren gehen.
Trage die Nieren zum Labor am rechten Bildschirmrand und liefere sie dort zur Analyse ab. Je mehr Nieren du erfolgreich abgibst, desto höher dein Punktestand. Aber Vorsicht: Lässt du eine Niere fallen, kostet dich das wertvolle Punkte.
## Die Besonderheit
Gelegentlich befindet sich unter den Nieren eine ganz besondere eine Spenderniere mit außergewöhnlichen Eigenschaften. Gibt der Arzt eine solche Niere ins Labor ab, beginnt dort sofort eine Analyse. Nach kurzer Zeit erscheint auf dem rechten Förderband ein spezielles Medikament. Hole es ab und bringe es schnell zum Patienten oben rechts im Bild er leidet und wartet dringend auf Linderung. Eine erfolgreiche Lieferung wird mit satten fünf Bonuspunkten belohnt.
## Punktewertung
| Aktion | Punkte |
|---|---|
| Niere ins Labor gebracht | +1 |
| Medikament zum Patienten gebracht | +5 |
| Niere fallen lassen | 1 |
| Besondere Niere fallen lassen | 3 |
## Spielregeln
- Der Arzt kann maximal **5 Nieren** gleichzeitig tragen.
- Jede siebte Niere ist im Durchschnitt eine besondere Spenderniere.
- Du hast **3 Minuten** Zeit nutze sie klug!
## Mehrspieler-Modus
Kidney Lab kann auch zu zweit gespielt werden. Beide Spieler steuern ihren eigenen Arzt auf einem gespiegelten Spielfeld und sammeln unabhängig voneinander Punkte. Am Ende gewinnt, wer die meisten Punkte erzielt hat Teamwork ist erlaubt, der Ruhm gehört aber dem Besten!
## Steuerung
| Gerät | Spieler 1 | Spieler 2 |
|---|---|---|
| Tastatur | Pfeiltasten | W / A / S / D |
| Touchscreen | Joystick rechts unten | Joystick links unten |

50
kidney_lab/index.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kidney Lab — MW-56 Game &amp; Watch</title>
<link rel="stylesheet" href="css/styles.css" />
</head>
<body>
<div id="app">
<!-- ── Intro screen ─────────────────────────────────────── -->
<div id="intro-screen" class="screen active"></div>
<!-- ── Game screen (canvas injected by game.js) ─────────── -->
<div id="game-screen" class="screen"></div>
<!-- ── High-score screen ────────────────────────────────── -->
<div id="highscore-screen" class="screen"></div>
</div>
<!--
Script load order matters — each file depends only on what
was loaded before it:
settings → (no deps)
player → settings
conveyor → settings
lab → settings
game → settings + player + conveyor + lab + highscore
intro → game
highscore → intro (back button)
We load highscore before intro so game.js can call HighScoreScreen.show().
intro.js is last so it can call Game.init() immediately on boot.
-->
<script src="js/settings.js"></script>
<script src="js/player.js"></script>
<script src="js/conveyor.js"></script>
<script src="js/lab.js"></script>
<script src="js/highscore.js"></script>
<script src="js/game.js"></script>
<script src="js/intro.js"></script>
<script>
// Boot: show the intro screen.
IntroScreen.show();
</script>
</body>
</html>

200
kidney_lab/js/conveyor.js Normal file
View File

@@ -0,0 +1,200 @@
/**
* conveyor.js
* Left conveyor belts (3×, goods travel → right).
* Right conveyor belt (1×, medication travels ← left).
* Pure logic — no rendering, no DOM.
*/
/* ────────────────────────────── Good ──────────────────────────── */
const GoodState = Object.freeze({
SLIDING: 'sliding', // moving along belt
READY: 'ready', // at belt's right end, waiting for pickup
COLLECTED: 'collected', // grabbed by player
DROPPED: 'dropped', // fell — penalty already applied
});
class Good {
constructor(beltRow, isSpecial) {
this.id = Good._nextId++;
this.beltRow = beltRow;
this.isSpecial = isSpecial;
this.x = SETTINGS.LEFT_ZONE_START + SETTINGS.GOOD_SIZE / 2;
this.y = SETTINGS.BELT_ROWS[beltRow];
this.state = GoodState.SLIDING;
this.waitTimer = 0;
}
}
Good._nextId = 0;
/* ────────────────────────── LeftConveyorSystem ─────────────────── */
class LeftConveyorSystem {
constructor() {
// Belt strip animation offset (px, wraps at 40)
this.animOffset = 0;
// Per-belt state: goods array + countdown to next spawn
this.belts = SETTINGS.BELT_INIT_OFFSETS.map(offset => ({
goods: [],
spawnTimer: offset,
}));
}
_randomSpawnTime() {
return SETTINGS.BELT_MIN_SPAWN
+ Math.random() * (SETTINGS.BELT_MAX_SPAWN - SETTINGS.BELT_MIN_SPAWN);
}
/**
* Update all belts.
* @param {number} dt delta time in ms
* @param {function} onDrop called with (good) when a good drops off
*/
update(dt, onDrop) {
this.animOffset = (this.animOffset + SETTINGS.LEFT_BELT_SPEED * dt / 1000) % 40;
this.belts.forEach((belt, row) => {
// ── Spawn ──
belt.spawnTimer -= dt;
if (belt.spawnTimer <= 0) {
belt.spawnTimer = this._randomSpawnTime();
const isSpecial = Math.random() < SETTINGS.SPECIAL_GOOD_CHANCE;
belt.goods.push(new Good(row, isSpecial));
}
// ── Move & age goods ──
belt.goods.forEach(good => {
if (good.state === GoodState.SLIDING) {
good.x += SETTINGS.LEFT_BELT_SPEED * dt / 1000;
const pickupX = SETTINGS.LEFT_ZONE_END - SETTINGS.GOOD_SIZE * 0.5;
if (good.x >= pickupX) {
good.x = pickupX;
good.state = GoodState.READY;
good.waitTimer = SETTINGS.GOOD_WAIT_TIME;
}
} else if (good.state === GoodState.READY) {
good.waitTimer -= dt;
if (good.waitTimer <= 0) {
good.state = GoodState.DROPPED;
onDrop(good);
}
}
});
// ── Prune finished goods ──
belt.goods = belt.goods.filter(
g => g.state === GoodState.SLIDING || g.state === GoodState.READY
);
});
}
/**
* Try to collect the READY good on the given row.
* Returns the Good object on success, null otherwise.
*/
tryCollect(row) {
const belt = this.belts[row];
const good = belt.goods.find(g => g.state === GoodState.READY);
if (!good) return null;
good.state = GoodState.COLLECTED;
return good;
}
/** True if belt[row] has a READY good waiting at the pickup point. */
hasReadyGood(row) {
return this.belts[row].goods.some(g => g.state === GoodState.READY);
}
/** All goods in SLIDING or READY state across all belts (for rendering). */
allGoods() {
return this.belts.flatMap(b => b.goods);
}
}
/* ─────────────────────── Medication (right belt) ──────────────── */
const MedState = Object.freeze({
ON_BELT: 'on_belt', // sliding left
READY: 'ready', // at left end, waiting for player
PICKED: 'picked', // player has it
EXPIRED: 'expired', // timed out while waiting
});
class Medication {
constructor() {
this.id = Medication._nextId++;
// Start at right edge of right zone
this.x = SETTINGS.RIGHT_ZONE_END - SETTINGS.GOOD_SIZE;
this.y = SETTINGS.BELT_ROWS[1]; // always middle row
this.state = MedState.ON_BELT;
this.waitTimer = 0;
}
}
Medication._nextId = 0;
/* ────────────────────────── RightConveyorSystem ────────────────── */
class RightConveyorSystem {
constructor() {
this.animOffset = 0;
this.medications = [];
}
addMedication() {
this.medications.push(new Medication());
}
/**
* Update right belt medications.
* @param {number} dt delta time ms
*/
update(dt) {
this.animOffset = (this.animOffset + SETTINGS.RIGHT_BELT_SPEED * dt / 1000) % 40;
this.medications.forEach(med => {
if (med.state === MedState.ON_BELT) {
med.x -= SETTINGS.RIGHT_BELT_SPEED * dt / 1000;
const pickupX = SETTINGS.RIGHT_ZONE_START + SETTINGS.GOOD_SIZE * 0.5;
if (med.x <= pickupX) {
med.x = pickupX;
med.state = MedState.READY;
med.waitTimer = SETTINGS.MED_WAIT_TIME;
}
} else if (med.state === MedState.READY) {
med.waitTimer -= dt;
if (med.waitTimer <= 0) {
med.state = MedState.EXPIRED;
}
}
});
// Prune expired / picked medications
this.medications = this.medications.filter(
m => m.state === MedState.ON_BELT || m.state === MedState.READY
);
}
/** True if there is a READY medication at the pickup point (row 1 left end). */
hasReadyMedication() {
return this.medications.some(m => m.state === MedState.READY);
}
/**
* Try to pick up the READY medication.
* Returns true on success.
*/
tryPickup() {
const med = this.medications.find(m => m.state === MedState.READY);
if (!med) return false;
med.state = MedState.PICKED;
return true;
}
/** Visible medications for rendering (ON_BELT and READY). */
visibleMedications() {
return this.medications.filter(
m => m.state === MedState.ON_BELT || m.state === MedState.READY
);
}
}

1080
kidney_lab/js/game.js Normal file

File diff suppressed because it is too large Load Diff

144
kidney_lab/js/highscore.js Normal file
View File

@@ -0,0 +1,144 @@
/**
* highscore.js
* High-score screen: displays final score(s), persists top-10 to localStorage,
* shows leaderboard, offers back-to-start button.
*
* show(names, scores, playerCount)
* names — string (1P) or array of strings (2P)
* scores — number (1P) or array of numbers (2P)
* playerCount — 1 or 2
*/
const HighScoreScreen = (() => {
const STORAGE_KEY = 'kidneylab_scores';
/* ── Persistence ─────────────────────────────────────────────── */
function loadScores() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
} catch {
return [];
}
}
function saveScore(name, score) {
const scores = loadScores();
scores.push({ name: name.toUpperCase().slice(0, 12), score, date: new Date().toLocaleDateString() });
scores.sort((a, b) => b.score - a.score);
const top10 = scores.slice(0, 10);
localStorage.setItem(STORAGE_KEY, JSON.stringify(top10));
return top10;
}
/* ── Helpers ─────────────────────────────────────────────────── */
function rankLabel(rank) {
if (rank === 1) return '🏆 NEW HIGH SCORE!';
if (rank === 2) return '🥈 2nd place!';
if (rank === 3) return '🥉 3rd place!';
return `Rank #${rank}`;
}
function resultCardHTML(playerName, finalScore, scores, myRank) {
const isTop = myRank === 1;
return `
<div class="hs-result ${isTop ? 'hs-gold' : ''}">
<div class="hs-rank-label">${rankLabel(myRank)}</div>
<div class="hs-player">${playerName.toUpperCase()}</div>
<div class="hs-score-display">${finalScore >= 0 ? finalScore : 0}</div>
<div class="hs-score-label">POINTS</div>
</div>`;
}
function tableHTML(scores, highlightNames, highlightScores) {
const rowsHTML = scores.map((s, i) => {
// highlight any entry that matches one of the just-played players
const isMe = highlightNames.some((n, ni) =>
s.name === n.toUpperCase().slice(0, 12) && s.score === highlightScores[ni]
);
return `<tr class="${isMe ? 'my-row' : ''}">
<td class="rank-col">${i + 1}</td>
<td class="name-col">${s.name}</td>
<td class="score-col">${s.score}</td>
<td class="date-col">${s.date}</td>
</tr>`;
}).join('');
return `
<div class="hs-table-wrap">
<h2 class="hs-heading">HALL OF FAME</h2>
<table class="hs-table">
<thead><tr><th>#</th><th>NAME</th><th>SCORE</th><th>DATE</th></tr></thead>
<tbody>${rowsHTML}</tbody>
</table>
</div>`;
}
/* ── Public show ─────────────────────────────────────────────── */
function show(names, scores, playerCount) {
// Normalise to arrays
const nameArr = Array.isArray(names) ? names : [names];
const scoreArr = Array.isArray(scores) ? scores : [scores];
const count = playerCount || nameArr.length;
// Switch screen
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('highscore-screen').classList.add('active');
// Save all players' scores; last save wins for the leaderboard table
let leaderboard;
nameArr.forEach((n, i) => {
leaderboard = saveScore(n, scoreArr[i]);
});
// Build rank info for each player
const rankInfo = nameArr.map((n, i) => {
const rank = leaderboard.findIndex(
s => s.name === n.toUpperCase().slice(0, 12) && s.score === scoreArr[i]
) + 1;
return { name: n, score: scoreArr[i], rank };
});
render(rankInfo, leaderboard, nameArr, scoreArr);
}
function render(rankInfo, leaderboard, nameArr, scoreArr) {
const el = document.getElementById('highscore-screen');
// One result card per player
const cardsHTML = rankInfo.map(r =>
resultCardHTML(r.name, r.score, leaderboard, r.rank)
).join('');
// Cards side-by-side for 2P, centred for 1P
const cardsWrap = rankInfo.length > 1
? `<div style="display:flex;gap:16px;justify-content:center;flex-wrap:wrap;">${cardsHTML}</div>`
: cardsHTML;
el.innerHTML = `
<div class="hs-inner">
<div class="hs-logo">
<span class="logo-gw">GAME &amp; WATCH</span>
<span class="logo-title">KIDNEY LAB</span>
</div>
${cardsWrap}
${tableHTML(leaderboard, nameArr, scoreArr)}
<div class="hs-actions">
<button id="back-btn" class="btn-primary">PLAY AGAIN</button>
</div>
</div>
`;
el.querySelector('#back-btn').addEventListener('click', () => {
IntroScreen.show();
});
}
return { show };
})();

140
kidney_lab/js/intro.js Normal file
View File

@@ -0,0 +1,140 @@
/**
* intro.js
* Introduction screen — two-step flow:
* Step 1: select 1 or 2 players
* Step 2: enter name(s), then start
* Owns the #intro-screen element.
*/
const IntroScreen = (() => {
function show() {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('intro-screen').classList.add('active');
renderStep1();
}
/* ── Shared header (logo + rules) ─────────────────────────── */
function headerHTML() {
return `
<div class="intro-logo">
<span class="logo-gw">GAME &amp; WATCH</span>
<span class="logo-title">KIDNEY LAB</span>
<span class="logo-model">MW-56</span>
</div>
<div class="intro-description">
<p>Help <strong>the Doctor</strong> collect kidneys from the supply belts
and deliver them to the laboratory for analysis.</p>
<p>Bring special medication to the patient to earn bonus points!</p>
</div>
<div class="intro-rules">
<table>
<tr><td>Kidney delivered to lab</td><td class="pts pos">+1 pt</td></tr>
<tr><td>Special med to patient</td><td class="pts pos">+5 pts</td></tr>
<tr><td>Kidney dropped</td><td class="pts neg">1 pt</td></tr>
<tr><td>Special kidney dropped</td><td class="pts neg">3 pts</td></tr>
</table>
<p class="rule-note">Carry up to <strong>5 kidneys</strong> · 1 in 7 chance is special · <strong>3 minutes</strong> on the clock</p>
</div>
<div class="intro-controls">
<p>Move with <kbd>↑↓←→</kbd> or <kbd>W A S D</kbd></p>
<p>Stand at <em>column&nbsp;0</em> (left) to collect · at <em>column&nbsp;3</em> (right) to deposit / pickup / deliver</p>
</div>
`;
}
/* ── Step 1: player count selection ───────────────────────── */
function renderStep1() {
const el = document.getElementById('intro-screen');
el.innerHTML = `
<div class="intro-inner">
${headerHTML()}
<div class="player-select">
<p class="player-select-label">SELECT NUMBER OF PLAYERS</p>
<div class="player-select-btns">
<button class="btn-player-count" id="btn-1p">
<span class="pcount">1</span>
<span class="pcount-label">PLAYER</span>
</button>
<button class="btn-player-count" id="btn-2p">
<span class="pcount">2</span>
<span class="pcount-label">PLAYERS</span>
</button>
</div>
</div>
</div>
`;
el.querySelector('#btn-1p').addEventListener('click', () => renderStep2(1));
el.querySelector('#btn-2p').addEventListener('click', () => renderStep2(2));
}
/* ── Step 2: name entry ────────────────────────────────────── */
function renderStep2(playerCount) {
const el = document.getElementById('intro-screen');
const nameFields = playerCount === 2
? `
<div class="name-row">
<label for="name-p1">Player 1 Name</label>
<input type="text" id="name-p1" maxlength="12"
placeholder="DOCTOR" autocomplete="off" spellcheck="false"/>
</div>
<div class="name-row">
<label for="name-p2">Player 2 Name</label>
<input type="text" id="name-p2" maxlength="12"
placeholder="NURSE" autocomplete="off" spellcheck="false"/>
</div>`
: `
<div class="name-row">
<label for="name-p1">Your Name</label>
<input type="text" id="name-p1" maxlength="12"
placeholder="DOCTOR" autocomplete="off" spellcheck="false"/>
</div>`;
el.innerHTML = `
<div class="intro-inner">
${headerHTML()}
<div class="intro-form">
<div class="mode-badge">${playerCount === 2 ? '2 PLAYER MODE' : '1 PLAYER MODE'}</div>
${nameFields}
<div class="form-actions">
<button class="btn-secondary" id="back-btn">← BACK</button>
<button class="btn-primary" id="start-btn">START GAME</button>
</div>
</div>
</div>
`;
const p1Input = el.querySelector('#name-p1');
p1Input.focus();
el.querySelector('#back-btn').addEventListener('click', renderStep1);
el.querySelector('#start-btn').addEventListener('click', () => startGame(playerCount));
// Enter on last field starts game
const lastInput = el.querySelector(playerCount === 2 ? '#name-p2' : '#name-p1');
lastInput.addEventListener('keydown', e => {
if (e.key === 'Enter') startGame(playerCount);
});
}
/* ── Launch game ───────────────────────────────────────────── */
function startGame(playerCount) {
const el = document.getElementById('intro-screen');
const name1 = (el.querySelector('#name-p1').value.trim() || 'DOCTOR').toUpperCase();
const name2 = playerCount === 2
? ((el.querySelector('#name-p2').value.trim() || 'NURSE').toUpperCase())
: null;
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('game-screen').classList.add('active');
Game.init(name1, name2);
}
return { show };
})();

84
kidney_lab/js/lab.js Normal file
View File

@@ -0,0 +1,84 @@
/**
* lab.js
* Laboratory logic: accept goods, analyse for special goods,
* trigger medication production. No rendering, no DOM.
*/
const LabState = Object.freeze({
IDLE: 'idle',
ANALYZING: 'analyzing',
});
class Lab {
constructor() {
this.reset();
}
reset() {
this.state = LabState.IDLE;
this.analyzeTimer = 0;
this.totalProcessed = 0;
this.flashTimer = 0; // brief visual flash on deposit
this._onMedReady = null;
}
/**
* Deposit an array of goods into the lab.
* @param {Good[]} goods goods from player inventory
* @param {function} onMedicationReady called (no args) when special analysis completes
* @returns {number} count of goods deposited
*/
depositGoods(goods, onMedicationReady) {
if (!goods.length) return 0;
let hasSpecial = false;
goods.forEach(good => {
this.totalProcessed++;
if (good.isSpecial) hasSpecial = true;
});
// Flash feedback
this.flashTimer = 500;
// Start analysis only if we got a special good and aren't already analysing
if (hasSpecial && this.state === LabState.IDLE) {
this.state = LabState.ANALYZING;
this.analyzeTimer = SETTINGS.LAB_ANALYZE_TIME;
this._onMedReady = onMedicationReady;
}
return goods.length;
}
/** True briefly after goods are deposited (visual feedback). */
isFlashing() {
return this.flashTimer > 0;
}
/** True while analysing a special good. */
isAnalyzing() {
return this.state === LabState.ANALYZING;
}
/** Progress 01 of the current analysis (0 if idle). */
analyzeProgress() {
if (this.state !== LabState.ANALYZING) return 0;
return 1 - this.analyzeTimer / SETTINGS.LAB_ANALYZE_TIME;
}
update(dt) {
if (this.flashTimer > 0) this.flashTimer -= dt;
if (this.state === LabState.ANALYZING) {
this.analyzeTimer -= dt;
if (this.analyzeTimer <= 0) {
this.analyzeTimer = 0;
this.state = LabState.IDLE;
if (this._onMedReady) {
this._onMedReady();
this._onMedReady = null;
}
}
}
}
}

140
kidney_lab/js/player.js Normal file
View File

@@ -0,0 +1,140 @@
/**
* player.js
* Player state, grid movement, and inventory management.
* No rendering, no DOM — pure logic.
*/
class Player {
constructor() {
this.reset();
}
reset() {
// Grid position: col 0-3 (left → right), row 0-2 (top → bottom)
this.col = 0;
this.row = 1;
// Carried goods (max SETTINGS.MAX_CARRY)
this.goods = [];
// Whether the player is holding the medication vial
this.hasMedication = false;
// Movement cooldown (ms)
this.moveCooldown = 0;
// Walk-cycle: phase advances (radians) while moveCooldown > 0
this.walkPhase = 0;
this.animFrame = 0;
this.animTimer = 0;
// Flash timer when interacting
this.interactFlash = 0;
}
/* ── Helpers ──────────────────────────────────────────────── */
/** Pixel x centre of current column inside the middle zone. */
pixelX() {
// Evenly distribute columns across the middle zone with a 30px margin
// from each edge, so col 0 sits just right of the belt end and
// col (PLAYER_COLS-1) sits just left of the right-zone border.
const margin = 30;
const avail = SETTINGS.MIDDLE_ZONE_END - SETTINGS.MIDDLE_ZONE_START - margin * 2;
const step = avail / (SETTINGS.PLAYER_COLS - 1);
return SETTINGS.MIDDLE_ZONE_START + margin + this.col * step;
}
/** Pixel y centre of current row. */
pixelY() {
return SETTINGS.BELT_ROWS[this.row];
}
canMove() {
return this.moveCooldown <= 0;
}
canCarryMore() {
return this.goods.length < SETTINGS.MAX_CARRY;
}
/* ── Movement ─────────────────────────────────────────────── */
move(direction) {
if (!this.canMove()) return false;
let nc = this.col;
let nr = this.row;
switch (direction) {
case 'up': nr = Math.max(0, nr - 1); break;
case 'down': nr = Math.min(SETTINGS.PLAYER_ROWS - 1, nr + 1); break;
case 'left': nc = Math.max(0, nc - 1); break;
case 'right': nc = Math.min(SETTINGS.PLAYER_COLS - 1, nc + 1); break;
default: return false;
}
if (nc === this.col && nr === this.row) return false;
this.col = nc;
this.row = nr;
this.moveCooldown = SETTINGS.PLAYER_MOVE_COOLDOWN;
return true;
}
/* ── Inventory ────────────────────────────────────────────── */
/**
* Attempt to add a good to the inventory.
* Returns true on success, false if full.
*/
addGood(good) {
if (!this.canCarryMore()) return false;
this.goods.push(good);
this.interactFlash = 300;
return true;
}
/**
* Remove and return all carried goods (for lab deposit).
*/
depositGoods() {
const deposited = [...this.goods];
this.goods = [];
this.interactFlash = 400;
return deposited;
}
pickupMedication() {
this.hasMedication = true;
this.interactFlash = 400;
}
/**
* Deliver medication to patient.
* Returns true if medication was delivered.
*/
deliverMedication() {
if (!this.hasMedication) return false;
this.hasMedication = false;
this.interactFlash = 600;
return true;
}
/* ── Update ───────────────────────────────────────────────── */
update(dt) {
if (this.moveCooldown > 0) {
this.moveCooldown -= dt;
// Advance walk cycle: ~π radians per 180 ms move cooldown → one leg swing per step
this.walkPhase += dt * 0.0175;
}
if (this.interactFlash > 0) this.interactFlash -= dt;
this.animTimer += dt;
if (this.animTimer >= 200) {
this.animTimer -= 200;
this.animFrame = (this.animFrame + 1) % 4;
}
}
}

101
kidney_lab/js/settings.js Normal file
View File

@@ -0,0 +1,101 @@
/**
* settings.js
* Static game constants — all tunable values live here.
* Do not import game state; this file has zero side-effects.
*/
const SETTINGS = Object.freeze({
/* ── Canvas ───────────────────────────────────────────────── */
CANVAS_WIDTH: 900,
CANVAS_HEIGHT: 520,
HEADER_HEIGHT: 60,
/* ── Timing ───────────────────────────────────────────────── */
GAME_DURATION: 180, // seconds
PLAYER_MOVE_COOLDOWN: 180, // ms between tile moves
LAB_ANALYZE_TIME: 3000, // ms to analyze a special good
GOOD_WAIT_TIME: 2500, // ms a good waits at belt end before dropping
MED_WAIT_TIME: 3500, // ms medication waits at pickup point
/* ── Scoring ──────────────────────────────────────────────── */
POINTS_GOOD_LAB: 1,
POINTS_SPECIAL_PATIENT: 5,
POINTS_DROP_GOOD: -1,
POINTS_DROP_SPECIAL: -3,
/* ── Carry limit ──────────────────────────────────────────── */
MAX_CARRY: 5,
/* ── Special good probability (1 in 7) ───────────────────── */
SPECIAL_GOOD_CHANCE: 1 / 7,
/* ── Layout — zone x boundaries ──────────────────────────── */
LEFT_ZONE_START: 0,
LEFT_ZONE_END: 350,
MIDDLE_ZONE_START: 350,
MIDDLE_ZONE_END: 600,
RIGHT_ZONE_START: 600,
RIGHT_ZONE_END: 900,
/* ── Vertical rows (y centres, shared across all zones) ───── */
// Row 0 = top, Row 1 = middle, Row 2 = bottom
BELT_ROWS: [155, 300, 445],
/* ── Player grid (4 cols × 3 rows inside middle zone) ──────── */
PLAYER_COLS: 3,
PLAYER_ROWS: 3,
PLAYER_COL_WIDTH: 100, // px between column centres
PLAYER_SIZE: 36, // bounding box size for drawing
BELT_HEIGHT: 13, // visual belt strip height (px)
/* ── Belt speeds ──────────────────────────────────────────── */
LEFT_BELT_SPEED: 45, // px / sec (goods move → right)
RIGHT_BELT_SPEED: 33, // px / sec (medication moves ← left)
/* ── Left belt spawn intervals (ms) ──────────────────────── */
BELT_MIN_SPAWN: 3200,
BELT_MAX_SPAWN: 6500,
/* ── Staggered initial spawn offsets per belt (ms) ─────────── */
BELT_INIT_OFFSETS: [600, 2400, 4200],
/* ── Item sizes ───────────────────────────────────────────── */
GOOD_SIZE: 26,
/* ── Patient happy flash duration ────────────────────────── */
PATIENT_HAPPY_DURATION: 1800, // ms
/* ── Colours ──────────────────────────────────────────────── */
COLOR_BG: '#0a1628',
COLOR_HEADER: '#0d1e38',
COLOR_HEADER_LINE: '#1a4070',
COLOR_BELT: '#152840',
COLOR_BELT_STRIPE: '#1e3d5c',
COLOR_BELT_EDGE: '#2a5a80',
COLOR_BELT_R: '#14283c',
COLOR_BELT_STRIPE_R:'#1c3650',
COLOR_GOOD: '#f0a030',
COLOR_GOOD_OUTLINE: '#c07010',
COLOR_SPECIAL: '#ff44aa',
COLOR_SPECIAL_OUT: '#cc1177',
COLOR_MED: '#44ffaa',
COLOR_MED_OUT: '#11cc77',
COLOR_PLAYER: '#4499ff',
COLOR_PLAYER_DARK: '#2266cc',
COLOR_PLAYER_CARRY: '#88ccff',
COLOR_LAB: '#1a4830',
COLOR_LAB_LIGHT: '#22aa55',
COLOR_LAB_OUTLINE: '#33dd77',
COLOR_PATIENT: '#ff8844',
COLOR_PATIENT_DARK: '#cc5511',
COLOR_PATIENT_HAPPY:'#ffdd44',
COLOR_TEXT: '#88ccff',
COLOR_SCORE_VAL: '#ffdd44',
COLOR_TIMER_OK: '#44dd88',
COLOR_TIMER_WARN: '#ffaa22',
COLOR_TIMER_DANGER: '#ff4444',
COLOR_ZONE_DIV: '#1a3a5c',
COLOR_ZONE_DIV2: '#0f2a40',
COLOR_SHADOW: 'rgba(0,0,0,0.45)',
});

24
kidney_lab/readme.txt Normal file
View File

@@ -0,0 +1,24 @@
Introduction:
We want to create a html/javascript game that can be played in the browser. The game is a customized clone from the Game and Watch MW-56 Mario game.
The game is build up from the folowing. It start with an introduction screen with some text, a name input field and a start button.
On Click on the start button the game should start.
When finished a high score page is shown with a button to go back to the start screen.
The GAME:
On top of the screen there is the amount of points. The name of the player. The amount of time the user has left.
The game screen is build up from three different area's from left to right.
The left part of the game contains three Automated conveyor belts under each other comming from the left of the screen and transport goods to the right.
The Automated conveyor belt should contain about one third of the screen. The different belt's randomly transport goods. We need to make sure that no two goods start exactly at the same time.
The second thrird of the screen contains a playable toon that needs to collect the goods that come from the Automated conveyor belts. For this we can go up and down to the location from the three Automated conveyor belts. And he can go to the right three steps.
The last 1/3 of the screen contains at the bottom a laboratory space. Above the laboratory a Automated conveyor belt that runs from right to left. At the top a patient who is in pain.
Now to play the game the user needs to control the playbale toon. He needs to collect the goods and bring them to the laboratory for analyzing the goods. He can carry a maximum of 5 goods. If he tries to carry more they drop on the floor. At random one of the goods is a special kind of goods. This should happen randonly so that one of the seven goods is a special kind of good. If the special kind of good is added to the laboratory than after a short analyzing a special medication is comming from the Automated conveyor belt on thr right that needs to be taken from the playable toon and bring to the patient.
For every good the player gets into the laboratory the user get 1 point. For giving the special good to the patient he gets 5 points. For every good that drops on the floor the user gets -1 point. If the good dropped was a special good he gets -3 points. The user has 3 minutes playing time and needs to collect as many points as he can.