24c42cc3e8
* Fixes for infection game * Updated infection game to use message class * fixed whitespace * Changes to Message class
519 lines
17 KiB
Markdown
519 lines
17 KiB
Markdown
# Infection
|
|
|
|
## ~avatar avatar
|
|
|
|
There is a disease outbreak! Will you find patient zero?!?
|
|
|
|
## ~
|
|
|
|
**Infection** is a distributed game which simulates
|
|
the spread of an illness. **The goal is to stop the outbreak before every player dies!**
|
|
|
|
* **Number of players:** 1 Master and 4, or more, additional players. The Master and all other players need a @boardname@ with battery pack.
|
|
|
|
In this game, a Master @boardname@ infects a single, initial player ("patient zero")
|
|
with the illness. The infected player is contagious immediately but won't show any signs sickness
|
|
during the incubation time. The sickness gets transmitted when two @boardname@s get close enough to each other to "contract the disease".
|
|
After the incubation period, a sad face appears on the screen. After the sickness period, the player will eventually die and a skull displays on the screen. Dead players are out of the game.
|
|
|
|
If at least one player survives the outbreak, the game is won. Otherwise, try again!
|
|
|
|
## ~ hint
|
|
|
|
**Infection** is an engaging game that will get your students running around.
|
|
We recommend playing it outside, or in a large open area, to give more space for the activity.
|
|
|
|
## ~
|
|
|
|
## How to play
|
|
|
|
Press `A+B` to enter Master mode (there's only one Master in a game).
|
|
|
|
Wait for players to be paired. The number of paired players will display on the screen.
|
|
When paired, a letter appears on the player's screen, this letter is the player's identity.
|
|
|
|
Once all of your players are registered, press `A+B` to start the infection game.
|
|
The game randomly picks a player as **patient zero**.
|
|
|
|
A player will transmit the disease to another player if close enough (detecting a sufficient `RSSI`), and if a certain probability (`TRANSMISSIONPROB`) of disease transfer is reached.
|
|
During the incubation phase (`INCUBATION`), the player does not show any sign of illness (happy face).
|
|
After that phase, the player gets sick and shows a sad face on the screen. After the sick (sad) face, the player dies and a skull shows up.
|
|
|
|
Once the game is over, the @boardname@ will show the player's id (`A`, `B`, `C`...), health, and
|
|
the id of the player who infected them. The Master @boardname@ will show the identity of patient zero.
|
|
|
|
Icons used in the game:
|
|
|
|
* Pairing: `IconNames.Ghost`
|
|
* Paired: `IconNames.Happy`
|
|
* Dead: `IconNames.Skull`
|
|
* Sick: `IconNames.Sad`
|
|
* Incubating: `IconNames.Confused`
|
|
* Healthy: `IconNames.Happy`
|
|
|
|
## ~ hint
|
|
|
|
Take a look at this video to see how the @boardname@ uses radio to spread "disease"
|
|
in this game:
|
|
|
|
https://www.youtube.com/watch?v=Re3H2ISfQE8
|
|
|
|
## ~
|
|
|
|
## Project share
|
|
|
|
### ~ hint
|
|
|
|
This code uses language features, such as ``switch`` or ``enum``, that are not supported in blocks.
|
|
As a result, it will not convert back to blocks.
|
|
|
|
### ~
|
|
|
|
## JavaScript code
|
|
|
|
```typescript
|
|
/**
|
|
* Infection game
|
|
*
|
|
* Flash all micro:bit will this script
|
|
*
|
|
* Press A+B to enter master mode (1 per game)
|
|
*
|
|
* Wait for players to be paired. The number of paired player will display on screen.
|
|
* An icon will appear on player's screen.
|
|
*
|
|
* Press A+B to start the infection game. The master will pick a random
|
|
* player as patient zero.
|
|
*
|
|
* A player will transmit the disease if close enough (RSSI)
|
|
* and with a certain probability (TRANSMISSIONPROB).
|
|
* During the incudation phase (INCUBATION), the player does not show any sign
|
|
* of illness. After that phase, the sad face shows up.
|
|
*
|
|
* The game will automatically stop once all players are dead or healthy. The master can
|
|
* also press A+B again to stop the game.
|
|
*
|
|
* Once the game is over, the micro:bit will show the player id (A,B,C...), health and
|
|
* who infected him.
|
|
*
|
|
* Icons used in the game:
|
|
*
|
|
* Pairing: IconNames.Ghost
|
|
* Paired: IconNames.Happy
|
|
* Dead: IconNames.Skull
|
|
* Sick: IconNames.Sad
|
|
* Incubating: IconNames.Confused
|
|
* Healthy: IconNames.Happy
|
|
*
|
|
*/
|
|
const INCUBATION = 20000; // time before showing symptoms
|
|
const DEATH = 40000; // time before dying off the disease
|
|
const RSSI = -45; // db
|
|
const TRANSMISSIONPROB = 40; // % probability to transfer disease
|
|
|
|
enum GameState {
|
|
Stopped,
|
|
Pairing,
|
|
Infecting,
|
|
Running,
|
|
Over
|
|
}
|
|
|
|
enum HealthState {
|
|
Healthy,
|
|
Incubating,
|
|
Sick,
|
|
Dead
|
|
}
|
|
|
|
enum MessageKind {
|
|
PairRequest,
|
|
PairConfirmation,
|
|
HealthSet,
|
|
HealthValue,
|
|
InitialInfect,
|
|
TransmitVirus,
|
|
GameState
|
|
}
|
|
|
|
const GameIcons = {
|
|
Pairing: IconNames.Ghost,
|
|
Paired: IconNames.Happy,
|
|
Dead: IconNames.Skull,
|
|
Sick: IconNames.Sad,
|
|
Incubating: IconNames.Confused,
|
|
Healthy: IconNames.Happy
|
|
}
|
|
|
|
class Message {
|
|
|
|
private _data: Buffer;
|
|
|
|
constructor(input?: Buffer) {
|
|
this._data = input || control.createBuffer(13);
|
|
}
|
|
|
|
get kind(): number {
|
|
return this._data.getNumber(NumberFormat.Int8LE, 0);
|
|
}
|
|
|
|
set kind(x: number) {
|
|
this._data.setNumber(NumberFormat.Int8LE, 0, x);
|
|
}
|
|
|
|
get fromSerialNumber(): number {
|
|
return this._data.getNumber(NumberFormat.Int32LE, 1);
|
|
}
|
|
|
|
set fromSerialNumber(x: number) {
|
|
this._data.setNumber(NumberFormat.Int32LE, 1, x);
|
|
}
|
|
|
|
get value(): number {
|
|
return this._data.getNumber(NumberFormat.Int32LE, 5);
|
|
}
|
|
|
|
set value(x: number) {
|
|
this._data.setNumber(NumberFormat.Int32LE, 5, x);
|
|
}
|
|
|
|
get toSerialNumber(): number {
|
|
return this._data.getNumber(NumberFormat.Int32LE, 9);
|
|
}
|
|
|
|
set toSerialNumber(x: number) {
|
|
this._data.setNumber(NumberFormat.Int32LE, 9, x);
|
|
}
|
|
|
|
send() {
|
|
radio.sendBuffer(this._data);
|
|
basic.pause(250);
|
|
}
|
|
}
|
|
|
|
const playerIcons = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
class Player {
|
|
id: number;
|
|
icon: number;
|
|
health: HealthState;
|
|
show() {
|
|
basic.showString(playerIcons[this.icon]);
|
|
}
|
|
}
|
|
|
|
// common state
|
|
let state = GameState.Stopped;
|
|
|
|
// master state
|
|
let master = false;
|
|
let patientZero: Player;
|
|
const players: Player[] = [];
|
|
|
|
// player state
|
|
let paired = false;
|
|
let infectedBy = -1; // who infected (playerIcon)
|
|
let infectedTime = 0; // local time when infection happened
|
|
let playerIcon = -1; // player icon and identity
|
|
let health = HealthState.Healthy;
|
|
|
|
// get a player instance (creates one as needed)
|
|
function player(id: number): Player {
|
|
for (const p of players)
|
|
if (p.id == id) return p;
|
|
|
|
// add player to game
|
|
let p = new Player();
|
|
p.id = id;
|
|
p.icon = (players.length + 1) % playerIcons.length;
|
|
p.health = HealthState.Healthy;
|
|
players.push(p);
|
|
serial.writeLine(`player ==> ${p.id}`)
|
|
|
|
return p;
|
|
}
|
|
|
|
function allDead(): boolean {
|
|
for (const p of players)
|
|
if (p.health != HealthState.Dead) return false;
|
|
return true;
|
|
}
|
|
|
|
function gameOver() {
|
|
state = GameState.Over;
|
|
if (patientZero)
|
|
patientZero.show();
|
|
}
|
|
|
|
function gameFace() {
|
|
switch (state) {
|
|
case GameState.Stopped:
|
|
basic.showIcon(GameIcons.Pairing);
|
|
break;
|
|
case GameState.Pairing:
|
|
if (playerIcon > -1)
|
|
basic.showString(playerIcons[playerIcon]);
|
|
else
|
|
basic.showIcon(paired ? GameIcons.Paired : GameIcons.Pairing, 1);
|
|
break;
|
|
case GameState.Infecting:
|
|
case GameState.Running:
|
|
switch (health) {
|
|
case HealthState.Dead:
|
|
basic.showIcon(GameIcons.Dead, 1);
|
|
break;
|
|
case HealthState.Sick:
|
|
basic.showIcon(GameIcons.Sick, 1);
|
|
break;
|
|
default:
|
|
basic.showIcon(GameIcons.Healthy, 1);
|
|
break;
|
|
}
|
|
break;
|
|
case GameState.Over:
|
|
// show id
|
|
basic.showString(playerIcons[playerIcon]);
|
|
basic.pause(2000);
|
|
// show health
|
|
switch (health) {
|
|
case HealthState.Dead:
|
|
basic.showIcon(GameIcons.Dead, 2000);
|
|
break;
|
|
case HealthState.Sick:
|
|
basic.showIcon(GameIcons.Sick, 2000);
|
|
break;
|
|
case HealthState.Incubating:
|
|
basic.showIcon(GameIcons.Incubating, 2000);
|
|
break;
|
|
default:
|
|
basic.showIcon(GameIcons.Healthy, 2000);
|
|
break;
|
|
}
|
|
// show how infected
|
|
if (infectedBy > -1) {
|
|
basic.showString(" INFECTED BY");
|
|
basic.showString(playerIcons[infectedBy]);
|
|
basic.pause(2000);
|
|
} else {
|
|
basic.showString(" PATIENT ZERO");
|
|
basic.pause(2000);
|
|
}
|
|
// show score
|
|
game.showScore();
|
|
basic.pause(1000);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// master button controller
|
|
input.onButtonPressed(Button.AB, () => {
|
|
// register as master
|
|
if (state == GameState.Stopped && !master) {
|
|
master = true;
|
|
paired = true;
|
|
state = GameState.Pairing;
|
|
serial.writeLine("registered as master");
|
|
radio.setTransmitPower(7); // beef up master signal
|
|
basic.showString("0");
|
|
return;
|
|
}
|
|
|
|
if (!master) return; // master only beyond this
|
|
|
|
// launch game
|
|
if (state == GameState.Pairing) {
|
|
// pick 1 player and infect him
|
|
patientZero = players[Math.randomRange(0, players.length - 1)];
|
|
// infecting message needs to be confirmed by
|
|
// the player
|
|
state = GameState.Infecting;
|
|
serial.writeLine(`game started ${players.length} players`);
|
|
} // end game
|
|
else if (state == GameState.Running) {
|
|
gameOver();
|
|
}
|
|
})
|
|
|
|
radio.setGroup(42);
|
|
radio.onReceivedBuffer(function (receivedBuffer: Buffer) {
|
|
const incomingMessage = new Message(receivedBuffer);
|
|
const signal = radio.receivedPacket(RadioPacketProperty.SignalStrength);
|
|
if (master) {
|
|
switch (incomingMessage.kind) {
|
|
case MessageKind.PairRequest:
|
|
// register player
|
|
let n = players.length;
|
|
player(incomingMessage.fromSerialNumber);
|
|
// show player number if changed
|
|
if (n != players.length) {
|
|
basic.showNumber(players.length);
|
|
}
|
|
break;
|
|
case MessageKind.HealthValue:
|
|
let p = player(incomingMessage.fromSerialNumber);
|
|
p.health = incomingMessage.value;
|
|
// check if all infected
|
|
if (allDead())
|
|
gameOver();
|
|
break;
|
|
}
|
|
} else {
|
|
switch (incomingMessage.kind) {
|
|
case MessageKind.GameState:
|
|
// update game state
|
|
state = incomingMessage.value as GameState;
|
|
break;
|
|
case MessageKind.InitialInfect:
|
|
if (infectedBy < 0 &&
|
|
incomingMessage.toSerialNumber == control.deviceSerialNumber()) {
|
|
// infected by master
|
|
infectedBy = 0; // infected my master
|
|
infectedTime = input.runningTime();
|
|
health = HealthState.Incubating;
|
|
serial.writeLine(`infected ${control.deviceSerialNumber()}`);
|
|
}
|
|
break;
|
|
case MessageKind.HealthSet:
|
|
if (incomingMessage.toSerialNumber == control.deviceSerialNumber()) {
|
|
const newHealth = incomingMessage.value;
|
|
if (health < newHealth) {
|
|
health = newHealth;
|
|
}
|
|
}
|
|
break;
|
|
case MessageKind.PairConfirmation:
|
|
if (!paired && state == GameState.Pairing &&
|
|
incomingMessage.toSerialNumber == control.deviceSerialNumber()) {
|
|
// paired!
|
|
serial.writeLine(`player paired ==> ${control.deviceSerialNumber()}`)
|
|
playerIcon = incomingMessage.value;
|
|
paired = true;
|
|
}
|
|
break;
|
|
case MessageKind.TransmitVirus:
|
|
if (state == GameState.Running) {
|
|
if (health == HealthState.Healthy) {
|
|
serial.writeLine(`signal: ${signal}`);
|
|
if (signal > RSSI &&
|
|
Math.randomRange(0, 100) > TRANSMISSIONPROB) {
|
|
infectedBy = incomingMessage.value;
|
|
infectedTime = input.runningTime();
|
|
health = HealthState.Incubating;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case MessageKind.HealthValue:
|
|
if (health != HealthState.Dead && signal > RSSI) {
|
|
game.addScore(1);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
|
|
// main game loop
|
|
basic.forever(() => {
|
|
let message: Message;
|
|
if (master) {
|
|
switch (state) {
|
|
case GameState.Pairing:
|
|
// tell each player they are registered
|
|
for (const p of players) {
|
|
message = new Message();
|
|
message.kind = MessageKind.PairConfirmation;
|
|
message.value = p.icon;
|
|
message.toSerialNumber = p.id;
|
|
message.send();
|
|
}
|
|
serial.writeLine(`pairing ${players.length} players`);
|
|
basic.pause(500);
|
|
break;
|
|
case GameState.Infecting:
|
|
if (patientZero.health == HealthState.Healthy) {
|
|
message = new Message();
|
|
message.kind = MessageKind.InitialInfect;
|
|
message.toSerialNumber = patientZero.id;
|
|
message.send();
|
|
basic.pause(100);
|
|
} else {
|
|
serial.writeLine(`patient ${patientZero.id} infected`);
|
|
// show startup
|
|
basic.showIcon(GameIcons.Dead);
|
|
state = GameState.Running;
|
|
}
|
|
break;
|
|
case GameState.Running:
|
|
for (const p of players) {
|
|
message = new Message();
|
|
message.kind = MessageKind.HealthSet;
|
|
message.value = p.health;
|
|
message.toSerialNumber = p.id;
|
|
message.send();
|
|
}
|
|
break;
|
|
case GameState.Over:
|
|
if (patientZero)
|
|
patientZero.show();
|
|
break;
|
|
}
|
|
message = new Message()
|
|
message.kind = MessageKind.GameState;
|
|
message.value = state;
|
|
message.send();
|
|
} else { // player loop
|
|
switch (state) {
|
|
case GameState.Pairing:
|
|
// broadcast player id
|
|
if (playerIcon < 0) {
|
|
message = new Message();
|
|
message.kind = MessageKind.PairRequest;
|
|
message.fromSerialNumber = control.deviceSerialNumber();
|
|
message.send();
|
|
} else if (infectedBy > -1) {
|
|
message = new Message();
|
|
message.kind = MessageKind.HealthValue;
|
|
message.fromSerialNumber = control.deviceSerialNumber();
|
|
message.value = health;
|
|
message.send();
|
|
}
|
|
break;
|
|
case GameState.Infecting:
|
|
message = new Message();
|
|
message.kind = MessageKind.HealthValue;
|
|
message.fromSerialNumber = control.deviceSerialNumber();
|
|
message.value = health;
|
|
message.send();
|
|
break;
|
|
case GameState.Running:
|
|
// update health status
|
|
if (health != HealthState.Healthy && input.runningTime() - infectedTime > DEATH)
|
|
health = HealthState.Dead;
|
|
else if (health != HealthState.Healthy && input.runningTime() - infectedTime > INCUBATION)
|
|
health = HealthState.Sick;
|
|
// transmit disease
|
|
if (health == HealthState.Incubating || health == HealthState.Sick) {
|
|
message = new Message();
|
|
message.kind = MessageKind.TransmitVirus;
|
|
message.fromSerialNumber = control.deviceSerialNumber();
|
|
message.value = playerIcon;
|
|
message.send();
|
|
}
|
|
message = new Message();
|
|
message.kind = MessageKind.HealthValue;
|
|
message.fromSerialNumber = control.deviceSerialNumber();
|
|
message.value = health;
|
|
message.send();
|
|
break;
|
|
}
|
|
// show current animation
|
|
gameFace();
|
|
}
|
|
})
|
|
|
|
basic.showIcon(GameIcons.Pairing)
|
|
```
|
|
|
|
```package
|
|
radio
|
|
```
|