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

383 lines
10 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-03-17 10:42:11 +00:00
import { PaillierPubKey, ReadOnlyCyphertext } from "../crypto/paillier.js";
2023-03-05 17:19:37 +00:00
import { Region } from "./map.js";
import { showDefenseDom } from "./dom.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,
new ReadOnlyCyphertext(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-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(
new ReadOnlyCyphertext(
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-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);
}
regions[ourRegion.name] = cipherText.toString();
ourRegion.reinforce(cipherText);
}
socket.emit("message", Packet.createReinforce(regions));
this.totalStrength += 1;
this.endTurn();
}
/**
* 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-02-17 13:48:07 +00:00
if (data.region !== undefined) {
if (this.reinforce(data)) {
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") {
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-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)
) {
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) {
defender.strength -= 1;
} else {
offender.strength -= 1;
}
if (defender.strength === 0) {
defender.strength = offenderRolls.length + 1;
offender.strength -= offenderRolls.length + 1;
defender.owner = this;
break;
}
}
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-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;
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
2023-02-17 12:46:21 +00:00
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
2023-02-17 13:48:07 +00:00
sender.strength > strength &&
strength > 0
) {
2023-02-17 12:46:21 +00:00
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
/**
* 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
}