Riskless/static/js/modules/interface/player.js

546 lines
16 KiB
JavaScript
Raw Normal View History

import { Packet } from "./packet.js";
2023-03-05 17:19:37 +00:00
import { socket, game, random } from "./main.js";
2023-03-04 14:19:26 +00:00
import { RsaPubKey } from "../crypto/rsa.js";
2023-04-10 10:19:11 +00:00
import { PaillierPubKey, ReadOnlyCiphertext } from "../crypto/paillier.js";
2023-05-01 13:42:17 +00:00
import { Region, Strength } from "./map.js";
2023-03-05 17:19:37 +00:00
import { showDefenseDom } from "./dom.js";
2023-04-27 11:52:02 +00:00
import {
2023-04-30 17:42:52 +00:00
proveBitLength,
2023-04-27 11:52:02 +00:00
proveFortify,
proveRegions,
2023-04-28 09:32:05 +00:00
verifyBitLength,
2023-04-27 11:52:02 +00:00
verifyFortify,
verifyRegions,
} from "./proofs.js";
// Timeout to consider a player disconnected
const TIMEOUT = 30_000;
const PHASE_REINFORCE = 1;
const PHASE_ATTACK = 2;
const PHASE_FORTIFY = 3;
2023-02-17 13:48:07 +00:00
let totalDice = 0;
export class Player {
constructor(id, local, rsaKey, paillierKey) {
2023-03-17 10:42:11 +00:00
// Game state
this.totalStrength = 0;
this.ready = false;
// Protocol state
2023-01-29 16:47:37 +00:00
this.timeout = null;
this.id = id;
this.rsaPubKey = RsaPubKey.fromJSON(rsaKey);
this.paillierPubKey = PaillierPubKey.fromJSON(paillierKey);
2023-03-07 15:43:47 +00:00
this.lastPacket = 0;
// Data which is reset every turn
this.isPlaying = false;
this.reinforcementsPlaced = 0;
this.reinforcementsAvailable = 0;
2023-02-17 13:48:07 +00:00
this.offenderRegion = null;
this.turnPhase = PHASE_REINFORCE;
2023-02-08 17:55:45 +00:00
2023-02-18 15:12:06 +00:00
// Data for defending
this.defenderPromise = null;
this.defenderAmount = null;
this.resetColor();
if (local) {
// Emit keepalive messages to inform other players we are still here
window.setInterval(() => {
socket.emit("message", Packet.createKeepAlive());
2023-03-04 00:25:54 +00:00
}, TIMEOUT / 2);
}
2023-01-29 16:47:37 +00:00
}
resetTimeout(game) {
2023-01-29 16:47:37 +00:00
if (this.timeout !== null) {
2023-01-31 12:34:13 +00:00
window.clearTimeout(this.timeout);
2023-01-29 16:47:37 +00:00
}
this.timeout = window.setTimeout(() => {
game.removePlayer(this);
2023-01-29 16:47:37 +00:00
}, TIMEOUT);
}
2023-02-06 11:04:37 +00:00
2023-02-08 17:55:45 +00:00
/**
* Get our color as used on the board.
*/
getColor() {
return this.color;
}
resetColor() {
let randomColor = Math.random() * 360;
this.color = `hsl(${randomColor} 57% 50%)`;
}
2023-03-13 14:52:14 +00:00
/**
* Get all regions controlled by this player.
*/
getRegions() {
return Region.getAllRegions().filter((r) => r.owner === this);
}
2023-02-06 11:04:37 +00:00
/**
* Claim a region of the map.
*
* @param data Data received via socket.
*/
claim(data) {
let region = Region.getRegion(data.region);
if (region.owner === null) {
2023-03-17 10:42:11 +00:00
region.claim(
this,
2023-04-10 10:19:11 +00:00
new ReadOnlyCiphertext(this.paillierPubKey, BigInt(data.cipherText))
2023-03-17 10:42:11 +00:00
);
this.totalStrength += 1;
2023-02-08 17:55:45 +00:00
return true;
} else {
return false;
2023-02-06 11:04:37 +00:00
}
}
2023-02-10 15:47:21 +00:00
/**
* Reinforce a region of the map.
*
2023-03-13 14:52:14 +00:00
* Todo. Check object before committing changes.
*
2023-02-10 15:47:21 +00:00
* @param data Data received via socket.
*/
reinforce(data) {
2023-04-18 19:29:39 +00:00
if (!verifyRegions(data.verification, this.paillierPubKey)) {
2023-04-21 08:50:20 +00:00
console.log("Failed to verify reinforcements!");
return false;
2023-04-18 19:29:39 +00:00
}
2023-03-13 14:52:14 +00:00
for (let regionName of Object.keys(data.regions)) {
let region = Region.getRegion(regionName);
2023-02-10 15:47:21 +00:00
2023-03-13 14:52:14 +00:00
if (region.owner === this) {
region.reinforce(
2023-04-10 10:19:11 +00:00
new ReadOnlyCiphertext(
this.paillierPubKey,
BigInt(data.regions[regionName])
)
);
2023-03-13 14:52:14 +00:00
}
2023-02-10 15:47:21 +00:00
}
2023-03-13 14:52:14 +00:00
2023-03-17 10:42:11 +00:00
this.totalStrength += 1;
2023-04-10 10:19:11 +00:00
// request proofs
for (let region of this.getRegions()) {
if ([...region.neighbours.values()].find((r) => r.owner === game.us)) {
region.requestProof();
}
}
2023-03-13 14:52:14 +00:00
return true;
2023-02-10 15:47:21 +00:00
}
2023-03-17 10:42:11 +00:00
sendClaim(region) {
let cipherText = this.paillierPubKey.encrypt(1n);
Region.getRegion(region).claim(this, cipherText);
socket.emit("message", Packet.createRegionClaim(region, cipherText.toString()));
this.totalStrength += 1;
this.endTurn();
}
sendReinforce(region) {
let regions = {};
for (let ourRegion of this.getRegions()) {
let cipherText;
if (ourRegion.name !== region) {
cipherText = this.paillierPubKey.encrypt(0n);
} else {
cipherText = this.paillierPubKey.encrypt(1n);
}
2023-04-18 19:29:39 +00:00
regions[ourRegion.name] = cipherText;
2023-03-17 10:42:11 +00:00
ourRegion.reinforce(cipherText);
}
2023-04-18 19:29:39 +00:00
let verification = proveRegions(regions);
socket.emit("message", Packet.createReinforce(regions, verification));
2023-03-17 10:42:11 +00:00
this.totalStrength += 1;
2023-04-21 08:50:20 +00:00
if (game.isPregame()) {
game.us.endTurn();
2023-04-10 10:19:11 +00:00
2023-04-21 08:50:20 +00:00
if (game.allReinforcementsPlaced()) {
game.incrementState();
}
2023-03-24 16:53:02 +00:00
}
2023-03-17 10:42:11 +00:00
}
2023-04-24 13:20:44 +00:00
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
2023-04-27 11:52:02 +00:00
if (!verifyFortify(data.fortify, this.paillierPubKey)) {
console.log("Failed to verify fortify!");
return false;
}
2023-04-24 13:20:44 +00:00
if (
2023-04-27 11:52:02 +00:00
!Object.keys(data.fortify.fortify).reduce(
(c, r) => c && Region.getRegion(r).owner === this,
true
)
2023-04-24 13:20:44 +00:00
) {
2023-04-27 11:52:02 +00:00
console.log("Invalid fortify");
2023-04-24 13:20:44 +00:00
return false;
}
2023-04-27 11:52:02 +00:00
for (let regionName of Object.keys(data.fortify.fortify)) {
let region = Region.getRegion(regionName);
2023-04-28 09:32:05 +00:00
let c1 = region.strength.cipherText.clone();
let c2 = new ReadOnlyCiphertext(
this.paillierPubKey,
BigInt(data.fortify.fortify[regionName])
2023-04-27 11:52:02 +00:00
);
2023-04-28 09:32:05 +00:00
c1.update(c2);
let v = verifyBitLength({
...data.fortify.rangeProofs[regionName],
cipherText: c1,
});
if (v !== null && v <= 8) {
region.reinforce(c2);
} else {
return false;
}
2023-04-27 11:52:02 +00:00
}
// request proofs
for (let region of this.getRegions()) {
if ([...region.neighbours.values()].find((r) => r.owner === game.us)) {
region.requestProof();
}
}
return true;
2023-04-24 13:20:44 +00:00
}
sendFortify(startRegion, endRegion, amount) {
let fortify = {
[startRegion]: new Ciphertext(this.paillierPubKey, BigInt(-amount)),
[endRegion]: new Ciphertext(this.paillierPubKey, BigInt(amount)),
};
for (let r of this.getRegions()) {
2023-04-27 11:52:02 +00:00
if (!fortify.hasOwnProperty(r.name)) {
fortify[r.name] = new Ciphertext(this.paillierPubKey, 0n);
2023-04-24 13:20:44 +00:00
}
}
2023-04-27 11:52:02 +00:00
for (let r of Object.keys(fortify)) {
Region.getRegion(r).reinforce(fortify[r]);
}
socket.emit("message", Packet.createFortify(proveFortify(fortify)));
2023-04-24 13:20:44 +00:00
}
/**
* Process a generic action packet representing a player's move.
*
* @param data Data received via socket
2023-03-17 10:42:11 +00:00
* @returns {boolean} Whether this player's turn should now end or not.
*/
2023-02-17 13:48:07 +00:00
async act(data) {
if (this.turnPhase === PHASE_REINFORCE) {
2023-04-10 10:19:11 +00:00
if (data.regions !== undefined) {
2023-04-21 08:50:20 +00:00
if (this === game.us) {
this.reinforcementsPlaced += 1;
} else if (this.reinforce(data)) {
2023-02-17 13:48:07 +00:00
this.reinforcementsPlaced += 1;
}
2023-02-17 13:48:07 +00:00
if (this.reinforcementsPlaced === this.reinforcementsAvailable) {
this.turnPhase = PHASE_ATTACK;
}
}
return false;
} else {
// End turn prematurely.
if (data.action === "END") {
return true;
}
if (this.turnPhase === PHASE_ATTACK && data.action === "ATTACK") {
2023-02-17 13:48:07 +00:00
await this.attack(data);
return false;
}
this.turnPhase = PHASE_FORTIFY;
if (data.action === "FORTIFY") {
2023-04-24 13:20:44 +00:00
if (this === game.us) {
2023-04-27 11:52:02 +00:00
return true;
2023-04-24 13:20:44 +00:00
} else {
return this.fortify(data);
}
}
}
return false;
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
2023-02-17 13:48:07 +00:00
async attack(data) {
let offender = Region.getRegion(data.startRegion);
let defender = Region.getRegion(data.endRegion);
let offenderStrength = parseInt(data.strength);
// Basic validation on game state
if (
offender.owner !== this ||
defender.owner === this ||
offenderStrength > 3 ||
offenderStrength <= 0 ||
offenderStrength >= offender.strength ||
(this.offenderRegion !== null && this.offenderRegion !== offender)
) {
return false;
}
2023-04-30 17:42:52 +00:00
game.contendedRegion = defender;
2023-02-18 15:12:06 +00:00
// If we're the defender, we need to send a packet to state our defense.
2023-03-05 17:19:37 +00:00
if (defender.owner === game.us) {
2023-02-18 16:48:31 +00:00
showDefenseDom(defender.name);
2023-02-18 15:12:06 +00:00
}
// Grab the defense amount from
let defenderStrength = 0;
while (
defenderStrength <= 0 ||
defenderStrength > Math.min(2, defender.strength)
) {
2023-04-21 08:50:20 +00:00
console.log("waiting");
2023-02-18 15:12:06 +00:00
defenderStrength = await defender.owner.getDefense();
}
2023-02-17 13:48:07 +00:00
/* How do Risk attacks work?
- Offender signs 1-3 armies, defender signs 1-2 armies
- Both roll respective dice
- Compare pairs of highest die in the rolls to remove armies.
*/
if (this.offenderRegion === null) {
this.offenderRegion = offender;
}
let offenderRolls = [];
let defenderRolls = [];
// Get random values
for (let i = 0; i < offenderStrength; i++) {
offenderRolls.push(await random.get(6, `dice-${totalDice}`));
totalDice += 1;
}
for (let i = 0; i < defenderStrength; i++) {
defenderRolls.push(await random.get(6, `dice-${totalDice}`));
totalDice += 1;
}
console.log(`attacker rolls: ${offenderRolls}`);
console.log(`defender rolls: ${defenderRolls}`);
offenderRolls.sort();
defenderRolls.sort();
// Compare and settle.
while (offenderRolls.length * defenderRolls.length > 0) {
let offenderResult = offenderRolls.pop();
let defenderResult = defenderRolls.pop();
if (offenderResult > defenderResult) {
2023-04-21 08:50:20 +00:00
defender.strength.transparentUpdate(-1n);
2023-02-17 13:48:07 +00:00
} else {
2023-04-21 08:50:20 +00:00
offender.strength.transparentUpdate(-1n);
2023-02-17 13:48:07 +00:00
}
2023-04-21 20:47:02 +00:00
}
// Handle aftermath.
if (defender.owner === game.us) {
if (defender.strength.cipherText.plainText === 0n) {
2023-04-30 17:42:52 +00:00
// State we don't control the region. This makes programming easier.
socket.emit("message", Packet.createRegionYield());
2023-04-21 20:47:02 +00:00
} else {
2023-04-30 17:42:52 +00:00
let ct = defender.strength.cipherText.clone();
2023-05-01 13:42:17 +00:00
ct.update(new Ciphertext(ct.pubKey, -1n, 0n));
2023-04-21 20:47:02 +00:00
// Prove we still control the region
2023-04-30 17:42:52 +00:00
let proof = proveBitLength(ct);
// Send proof we maintain it
socket.emit("message", Packet.createRegionProof(proof));
2023-04-21 20:47:02 +00:00
}
2023-04-23 16:23:42 +00:00
} else if (this === game.us) {
2023-04-21 20:47:02 +00:00
if (defender.strength.assumedStrength === 0n) {
// Handle region gain
2023-05-01 13:42:17 +00:00
let newStrength = new Ciphertext(
this.paillierPubKey,
BigInt(offenderRolls.length) + 1n
2023-04-21 20:47:02 +00:00
);
2023-04-30 17:42:52 +00:00
2023-05-01 13:42:17 +00:00
defender.owner = this;
defender.strength = new Strength(newStrength, defender.name);
2023-04-30 17:42:52 +00:00
// Send the new ciphertext
2023-05-01 13:42:17 +00:00
socket.emit("message", Packet.createRegionCapture(newStrength));
2023-04-30 17:42:52 +00:00
} else {
// State we didn't capture. Again, makes programming easier.
socket.emit("message", Packet.createRegionYield());
2023-02-17 13:48:07 +00:00
}
2023-05-01 13:42:17 +00:00
}
let resolutions = await defender.resolveAttack();
if (
resolutions.attackerRes.action === "CAPTURE" &&
resolutions.defenderRes.action === "YIELD"
) {
// Capture occurred.
if (this === game.us) {
// Already done above.
} else {
defender.owner = this;
defender.strength = new Strength(
new ReadOnlyCiphertext(
this.paillierPubKey,
BigInt(resolutions.attackerRes.cipherText)
),
defender.name
);
}
} else if (resolutions.attackerRes.action === "YIELD") {
// Do nothing, no capture occurred.
2023-04-23 16:23:42 +00:00
} else {
2023-05-01 13:42:17 +00:00
// Conflict res. Need to check proof.
let v = verifyBitLength(
resolutions.defenderRes.proof,
defender.owner.paillierPubKey
);
2023-04-30 17:42:52 +00:00
2023-05-01 13:42:17 +00:00
if (v !== null && v <= 8) {
// Accept defender (do nothing)
} else {
// Accept attacker
if (this === game.us) {
// Already done above.
} else {
defender.owner = this;
defender.strength = new Strength(
new ReadOnlyCiphertext(
this.paillierPubKey,
BigInt(resolutions.attackerRes.cipherText)
),
defender.name
);
}
}
2023-02-17 13:48:07 +00:00
}
2023-02-18 15:12:06 +00:00
// Reset the promises in case they attack again.
defender.owner.defenderPromise = null;
defender.owner.defenderAmount = null;
2023-04-30 17:42:52 +00:00
defender.defenderRes = null;
defender.attackerRes = null;
defender.attackResolver = null;
game.contendedRegion = null;
2023-02-17 13:48:07 +00:00
}
2023-02-18 15:12:06 +00:00
async setDefense(amount) {
await navigator.locks.request("defender", () => {
this.defenderAmount = amount;
if (this.defenderPromise !== null) {
this.defenderPromise(amount);
}
});
}
async getDefense() {
let promise, resolver;
await navigator.locks.request("defender", () => {
if (this.defenderAmount === null) {
promise = new Promise((resolve) => {
resolver = resolve;
});
this.defenderPromise = resolver;
} else {
promise = new Promise((resolve) => {
resolve(this.defenderAmount);
});
}
});
return promise;
}
/**
* Calculate the number of additional reinforcements to gain on this turn.
*/
additionalReinforcements() {
return Math.min(
3,
Math.floor(
2023-03-05 17:19:37 +00:00
Object.values(Region.getAllRegions()).filter(
(region) => region.owner === this
).length / 3
)
);
}
2023-02-06 11:04:37 +00:00
/**
* Start a player's turn.
*/
startTurn() {
this.isPlaying = true;
this.reinforcementsPlaced = 0;
this.reinforcementsAvailable = this.additionalReinforcements();
2023-02-17 13:48:07 +00:00
this.offenderRegion = null;
this.turnPhase = PHASE_REINFORCE;
2023-02-06 11:04:37 +00:00
}
/**
* End player's turn
*/
endTurn() {
this.isPlaying = false;
2023-02-06 12:30:24 +00:00
this.nextPlayer().startTurn();
2023-03-05 17:19:37 +00:00
const event = new CustomEvent("playerChange");
document.dispatchEvent(event);
2023-02-06 11:04:37 +00:00
}
nextPlayer() {
2023-03-05 17:19:37 +00:00
// todo move this to :class:Game
let sorted = Object.values(game.players).sort((a, b) => (a.id < b.id ? -1 : 1));
2023-02-08 17:55:45 +00:00
let ourIndex = sorted.findIndex((player) => player.id === this.id);
2023-02-06 11:04:37 +00:00
return sorted[(ourIndex + 1) % sorted.length];
}
2023-01-29 16:47:37 +00:00
}