diff options
Diffstat (limited to 'diplomacy/web/src/gui/utils')
-rw-r--r-- | diplomacy/web/src/gui/utils/dipStorage.jsx | 140 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/inline_game_view.jsx | 158 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/load_game_from_disk.js | 83 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/map_data.js | 98 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/order.js | 24 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/order_building.js | 211 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/power_view.jsx | 65 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/province.js | 117 | ||||
-rw-r--r-- | diplomacy/web/src/gui/utils/saveGameToDisk.js | 18 |
9 files changed, 914 insertions, 0 deletions
diff --git a/diplomacy/web/src/gui/utils/dipStorage.jsx b/diplomacy/web/src/gui/utils/dipStorage.jsx new file mode 100644 index 0000000..db5baad --- /dev/null +++ b/diplomacy/web/src/gui/utils/dipStorage.jsx @@ -0,0 +1,140 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +/* DipStorage scheme: +global +- connection + - username + - hostname + - port + - showServerFields +users +- (username) + - games + - (game_id) + - phase: string + - local_orders: {power_name => [orders]} +*/ + +let STORAGE = null; + +export class DipStorage { + static load() { + if (!STORAGE) { + const global = window.localStorage.global; + const users = window.localStorage.users; + STORAGE = { + global: (global && JSON.parse(global)) || { + connection: { + username: null, + hostname: null, + port: null, + showServerFields: null + } + }, + users: (users && JSON.parse(users)) || {} + }; + } + } + + static save() { + if (STORAGE) { + window.localStorage.global = JSON.stringify(STORAGE.global); + window.localStorage.users = JSON.stringify(STORAGE.users); + } + } + + static getConnectionForm() { + DipStorage.load(); + return Object.assign({}, STORAGE.global.connection); + } + + static getUserGames(username) { + DipStorage.load(); + if (STORAGE.users[username]) + return Object.keys(STORAGE.users[username].games); + return null; + } + + static getUserGameOrders(username, gameID, gamePhase) { + DipStorage.load(); + if (STORAGE.users[username] && STORAGE.users[username].games[gameID] + && STORAGE.users[username].games[gameID].phase === gamePhase) + return Object.assign({}, STORAGE.users[username].games[gameID].local_orders); + return null; + } + + static setConnectionUsername(username) { + DipStorage.load(); + STORAGE.global.connection.username = username; + DipStorage.save(); + } + + static setConnectionHostname(hostname) { + DipStorage.load(); + STORAGE.global.connection.hostname = hostname; + DipStorage.save(); + } + + static setConnectionPort(port) { + DipStorage.load(); + STORAGE.global.connection.port = port; + DipStorage.save(); + } + + static setConnectionshowServerFields(showServerFields) { + DipStorage.load(); + STORAGE.global.connection.showServerFields = showServerFields; + DipStorage.save(); + } + + static addUserGame(username, gameID) { + DipStorage.load(); + if (!STORAGE.users[username]) + STORAGE.users[username] = {games: {}}; + if (!STORAGE.users[username].games[gameID]) + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + DipStorage.save(); + } + + static addUserGameOrders(username, gameID, gamePhase, powerName, orders) { + DipStorage.addUserGame(username, gameID); + if (STORAGE.users[username].games[gameID].phase !== gamePhase) + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + STORAGE.users[username].games[gameID].phase = gamePhase; + STORAGE.users[username].games[gameID].local_orders[powerName] = orders; + DipStorage.save(); + } + + static removeUserGame(username, gameID) { + DipStorage.load(); + if (STORAGE.users[username] && STORAGE.users[username].games[gameID]) { + delete STORAGE.users[username].games[gameID]; + DipStorage.save(); + } + } + + static clearUserGameOrders(username, gameID, powerName) { + DipStorage.addUserGame(username, gameID); + if (powerName) { + if (STORAGE.users[username].games[gameID].local_orders[powerName]) + delete STORAGE.users[username].games[gameID].local_orders[powerName]; + } else { + STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}}; + } + DipStorage.save(); + } +} diff --git a/diplomacy/web/src/gui/utils/inline_game_view.jsx b/diplomacy/web/src/gui/utils/inline_game_view.jsx new file mode 100644 index 0000000..ec2ca46 --- /dev/null +++ b/diplomacy/web/src/gui/utils/inline_game_view.jsx @@ -0,0 +1,158 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +import React from "react"; +import {JoinForm} from "../forms/join_form"; +import {STRINGS} from "../../diplomacy/utils/strings"; +import {ContentGame} from "../pages/content_game"; +import {Button} from "../components/button"; +import {DeleteButton} from "../components/delete_button"; + +export class InlineGameView { + constructor(page, gameData) { + this.page = page; + this.game = gameData; + this.get = this.get.bind(this); + this.joinGame = this.joinGame.bind(this); + this.showGame = this.showGame.bind(this); + } + + joinGame(formData) { + const form = { + power_name: formData[`power_name_${this.game.game_id}`], + registration_password: formData[`registration_password_${this.game.game_id}`] + }; + if (!form.power_name) + form.power_name = null; + if (!form.registration_password) + form.registration_password = null; + form.game_id = this.game.game_id; + this.page.channel.joinGame(form) + .then((networkGame) => { + this.game = networkGame.local; + this.page.addToMyGames(this.game); + return networkGame.getAllPossibleOrders(); + }) + .then(allPossibleOrders => { + this.game.setPossibleOrders(allPossibleOrders); + this.page.load( + `game: ${this.game.game_id}`, + <ContentGame data={this.game}/>, + {success: 'Game joined.'} + ); + }) + .catch((error) => { + this.page.error('Error when joining game ' + this.game.game_id + ': ' + error); + }); + } + + showGame() { + this.page.load(`game: ${this.game.game_id}`, <ContentGame data={this.game}/>); + } + + getJoinUI() { + if (this.game.role) { + // Game already joined. + return ( + <div className={'games-form'}> + <Button key={'button-show-' + this.game.game_id} title={'show'} onClick={this.showGame}/> + <Button key={'button-leave-' + this.game.game_id} title={'leave'} + onClick={() => this.page.leaveGame(this.game.game_id)}/> + </div> + ); + } else { + // Game not yet joined. + return <JoinForm key={this.game.game_id} game_id={this.game.game_id} powers={this.game.controlled_powers} + password_required={this.game.registration_password} + onSubmit={this.joinGame}/>; + } + } + + getActionButtons() { + const buttons = []; + // Button to add/remove game from "My games" list. + if (this.page.hasMyGame(this.game.game_id)) { + if (!this.game.client) { + // Game in My Games and not joined. We can remove it. + buttons.push(<Button key={`my-game-remove`} title={'Remove from My Games'} + small={true} large={true} + onClick={() => this.page.removeFromMyGames(this.game.game_id)}/>); + } + } else { + // Game not in My Games, we can add it. + buttons.push(<Button key={`my-game-add`} title={'Add to My Games'} + small={true} large={true} + onClick={() => this.page.addToMyGames(this.game)}/>); + } + // Button to delete game. + if ([STRINGS.MASTER_TYPE, STRINGS.OMNISCIENT_TYPE].includes(this.game.observer_level)) { + buttons.push( + <DeleteButton key={`game-delete-${this.game.game_id}`} + title={'Delete this game'} + confirmTitle={'Click again to confirm deletion'} + waitingTitle={'Deleting ...'} + onClick={() => this.page.removeGame(this.game.game_id)}/> + ); + } + return buttons; + } + + get(name) { + if (name === 'players') { + return `${this.game.n_players} / ${this.game.n_controls}`; + } + if (name === 'rights') { + const elements = []; + if (this.game.observer_level) { + let levelName = ''; + if (this.game.observer_level === STRINGS.MASTER_TYPE) + levelName = 'master'; + else if (this.game.observer_level === STRINGS.OMNISCIENT_TYPE) + levelName = 'omniscient'; + else + levelName = 'observer'; + elements.push((<p key={0}><strong>Observer right:</strong><br/>{levelName}</p>)); + } + if (this.game.controlled_powers && this.game.controlled_powers.length) { + const powers = this.game.controlled_powers.slice(); + powers.sort(); + elements.push(( + <div key={1}><strong>Currently handled power{powers.length === 1 ? '' : 's'}</strong></div>)); + for (let power of powers) + elements.push((<div key={power}>{power}</div>)); + } + return elements.length ? (<div>{elements}</div>) : ''; + } + if (name === 'rules') { + if (this.game.rules) + return <div>{this.game.rules.map(rule => <div key={rule}>{rule}</div>)}</div>; + return ''; + } + if (name === 'join') + return this.getJoinUI(); + if (name === 'actions') + return this.getActionButtons(); + if (name === 'game_id') { + const date = new Date(this.game.timestamp_created / 1000); + const dateString = `${date.toLocaleDateString()} - ${date.toLocaleTimeString()}`; + return <div> + <div><strong>{this.game.game_id}</strong></div> + <div>({dateString})</div> + </div>; + } + return this.game[name]; + } +} diff --git a/diplomacy/web/src/gui/utils/load_game_from_disk.js b/diplomacy/web/src/gui/utils/load_game_from_disk.js new file mode 100644 index 0000000..ca49aa0 --- /dev/null +++ b/diplomacy/web/src/gui/utils/load_game_from_disk.js @@ -0,0 +1,83 @@ +import $ from "jquery"; +import {STRINGS} from "../../diplomacy/utils/strings"; +import {Game} from "../../diplomacy/engine/game"; + +export function loadGameFromDisk(onLoad, onError) { + const input = $(document.createElement('input')); + input.attr("type", "file"); + input.trigger('click'); + input.change(event => { + const file = event.target.files[0]; + if (!file.name.match(/\.json$/i)) { + onError(`Invalid JSON filename ${file.name}`); + return; + } + const reader = new FileReader(); + reader.onload = () => { + const savedData = JSON.parse(reader.result); + const gameObject = {}; + gameObject.game_id = `(local) ${savedData.id}`; + gameObject.map_name = savedData.map; + gameObject.rules = savedData.rules; + gameObject.state_history = {}; + gameObject.message_history = {}; + gameObject.order_history = {}; + gameObject.result_history = {}; + + // Load all saved phases (expect the latest one) to history fields. + for (let i = 0; i < savedData.phases.length - 1; ++i) { + const savedPhase = savedData.phases[i]; + const gameState = savedPhase.state; + const phaseOrders = savedPhase.orders || {}; + const phaseResults = savedPhase.results || {}; + const phaseMessages = {}; + if (savedPhase.messages) { + for (let message of savedPhase.messages) { + phaseMessages[message.time_sent] = message; + } + } + if (!gameState.name) + gameState.name = savedPhase.name; + gameObject.state_history[gameState.name] = gameState; + gameObject.message_history[gameState.name] = phaseMessages; + gameObject.order_history[gameState.name] = phaseOrders; + gameObject.result_history[gameState.name] = phaseResults; + } + + // Load latest phase separately and use it later to define the current game phase. + const latestPhase = savedData.phases[savedData.phases.length - 1]; + const latestGameState = latestPhase.state; + const latestPhaseOrders = latestPhase.orders || {}; + const latestPhaseResults = latestPhase.results || {}; + const latestPhaseMessages = {}; + if (latestPhase.messages) { + for (let message of latestPhase.messages) { + latestPhaseMessages[message.time_sent] = message; + } + } + if (!latestGameState.name) + latestGameState.name = latestPhase.name; + // TODO: NB: What is latest phase in loaded JSON contains order results? Not sure if it is well handled. + gameObject.result_history[latestGameState.name] = latestPhaseResults; + + gameObject.messages = []; + gameObject.role = STRINGS.OBSERVER_TYPE; + gameObject.status = STRINGS.COMPLETED; + gameObject.timestamp_created = 0; + gameObject.deadline = 0; + gameObject.n_controls = 0; + gameObject.registration_password = ''; + const game = new Game(gameObject); + + // Set game current phase and state using latest phase found in JSON file. + game.setPhaseData({ + name: latestGameState.name, + state: latestGameState, + orders: latestPhaseOrders, + messages: latestPhaseMessages + }); + onLoad(game); + }; + reader.readAsText(file); + }); +} diff --git a/diplomacy/web/src/gui/utils/map_data.js b/diplomacy/web/src/gui/utils/map_data.js new file mode 100644 index 0000000..73d5338 --- /dev/null +++ b/diplomacy/web/src/gui/utils/map_data.js @@ -0,0 +1,98 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +import {Province} from "./province"; + +export class MapData { + constructor(mapInfo, game) { + // mapInfo: {powers: [], supply_centers: [], aliases: {alias: name}, loc_type: {loc => type}, loc_abut: {loc => [abuts]}} + // game: a NetworkGame object. + this.game = game; + this.powers = new Set(mapInfo.powers); + this.supplyCenters = new Set(mapInfo.supply_centers); + this.aliases = Object.assign({}, mapInfo.aliases); + this.provinces = {}; + for (let entry of Object.entries(mapInfo.loc_type)) { + const provinceName = entry[0]; + const provinceType = entry[1]; + this.provinces[provinceName] = new Province(provinceName, provinceType, this.supplyCenters.has(provinceName)); + } + for (let entry of Object.entries(mapInfo.loc_abut)) { + this.getProvince(entry[0]).setNeighbors(entry[1].map(name => this.getProvince(name))); + } + for (let province of Object.values(this.provinces)) { + province.setCoasts(this.provinces); + } + for (let power of Object.values(this.game.powers)) { + for (let center of power.centers) { + this.getProvince(center).setController(power.name, 'C'); + } + for (let loc of power.influence) { + this.getProvince(loc).setController(power.name, 'I'); + } + for (let unit of power.units) { + this.__add_unit(unit, power.name); + } + for (let unit of Object.keys(power.retreats)) { + this.__add_retreat(unit, power.name); + } + } + for (let entry of Object.entries(this.aliases)) { + const alias = entry[0]; + const provinceName = entry[1]; + const province = this.getProvince(provinceName); + if (province) + province.aliases.push(alias); + } + } + + __add_unit(unit, power_name) { + const splitUnit = unit.split(/ +/); + const unitType = splitUnit[0]; + const location = splitUnit[1]; + const province = this.getProvince(location); + province.setController(power_name, 'U'); + province.unit = unitType; + } + + __add_retreat(unit, power_name) { + const splitUnit = unit.split(/ +/); + const location = splitUnit[1]; + const province = this.getProvince(location); + province.retreatController = power_name; + province.retreatUnit = unit; + } + + getProvince(abbr) { + if (abbr === '') + return null; + if (abbr[0] === '_') + abbr = abbr.substr(1, 3); + if (!abbr) + return null; + if (!this.provinces.hasOwnProperty(abbr)) { + const firstLetter = abbr[0]; + if (firstLetter === firstLetter.toLowerCase()) { + abbr = abbr.toUpperCase(); + } else { + abbr = abbr.toLowerCase(); + } + } + if (!this.provinces.hasOwnProperty(abbr)) + abbr = this.aliases[abbr]; + return this.provinces[abbr]; + } +} diff --git a/diplomacy/web/src/gui/utils/order.js b/diplomacy/web/src/gui/utils/order.js new file mode 100644 index 0000000..e314b9f --- /dev/null +++ b/diplomacy/web/src/gui/utils/order.js @@ -0,0 +1,24 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +export class Order { + constructor(orderString, isLocal) { + const pieces = orderString.split(/ +/); + this.loc = pieces[1]; + this.order = orderString; + this.local = Boolean(isLocal); + } +} diff --git a/diplomacy/web/src/gui/utils/order_building.js b/diplomacy/web/src/gui/utils/order_building.js new file mode 100644 index 0000000..3758898 --- /dev/null +++ b/diplomacy/web/src/gui/utils/order_building.js @@ -0,0 +1,211 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ + +function assertLength(expected, given) { + if (expected !== given) + throw new Error(`Length error: expected ${expected}, given ${given}.`); +} + +export class ProvinceCheck { + + static retreated(province, powerName) { + const retreatProvince = province.getRetreated(powerName); + if (!retreatProvince) + throw new Error(`No retreated location at province ${province.name}.`); + // No confusion possible, we select the only occupied location at this province. + return [retreatProvince.retreatUnit]; + } + + static present(province, powerName) { + let unit = null; + let presenceProvince = province.getOccupied(powerName); + if (presenceProvince) { + unit = `${presenceProvince.unit} ${presenceProvince.name}`; + } else { + presenceProvince = province.getRetreated(powerName); + if (!presenceProvince) + throw new Error(`No unit or retreat at province ${province.name}.`); + unit = presenceProvince.retreatUnit; + } + return [unit]; + } + + static occupied(province, powerName) { + const occupiedProvince = province.getOccupied(powerName); + if (!occupiedProvince) + throw new Error(`No occupied location at province ${province.name}.`); + // No confusion possible, we select the only occupied location at this province. + const unit = occupiedProvince.unit; + const name = occupiedProvince.name.toUpperCase(); + return [`${unit} ${name}`]; + } + + static occupiedByAny(province, unusedPowerName) { + return ProvinceCheck.occupied(province, null); + } + + static any(province, unusedPowerName) { + // There may be many locations available for a province (e.g. many coasts). + return province.getLocationNames(); + } + + static buildOrder(path) { + switch (path[0]) { + case 'H': + return ProvinceCheck.holdToString(path); + case 'M': + return ProvinceCheck.moveToString(path); + case 'V': + return ProvinceCheck.moveViaToString(path); + case 'S': + return ProvinceCheck.supportToString(path); + case 'C': + return ProvinceCheck.convoyToString(path); + case 'R': + return ProvinceCheck.retreatToString(path); + case 'D': + return ProvinceCheck.disbandToString(path); + case 'A': + return ProvinceCheck.buildArmyToString(path); + case 'F': + return ProvinceCheck.buildFleetToString(path); + default: + throw new Error('Unable to build order from path ' + JSON.stringify(path)); + } + } + + static holdToString(path) { + assertLength(2, path.length); + return `${path[1]} ${path[0]}`; + } + + static moveToString(path) { + assertLength(3, path.length); + return `${path[1]} - ${path[2]}`; + } + + static moveViaToString(path) { + return ProvinceCheck.moveToString(path) + ' VIA'; + } + + static supportToString(path) { + assertLength(4, path.length); + let order = `${path[1]} ${path[0]} ${path[2]}`; + if (path[2].substr(2) !== path[3]) + order += ` - ${path[3]}`; + return order; + } + + static convoyToString(path) { + assertLength(4, path.length); + return `${path[1]} ${path[0]} ${path[2]} - ${path[3]}`; + } + + static retreatToString(path) { + assertLength(3, path.length); + return `${path[1]} ${path[0]} ${path[2]}`; + } + + static disbandToString(path) { + assertLength(2, path.length); + return `${path[1]} ${path[0]}`; + } + + static buildArmyToString(path) { + assertLength(2, path.length); + return `${path[0]} ${path[1]} B`; + } + + static buildFleetToString(path) { + assertLength(2, path.length); + return `${path[0]} ${path[1]} B`; + } + +} + +export const ORDER_BUILDER = { + H: { + name: 'hold (H)', + steps: [ProvinceCheck.occupied] + }, + M: { + name: 'move (M)', + steps: [ProvinceCheck.occupied, ProvinceCheck.any] + }, + V: { + name: 'move VIA (V)', + steps: [ProvinceCheck.occupied, ProvinceCheck.any] + }, + S: { + name: 'support (S)', + steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any] + }, + C: { + name: 'convoy (C)', + steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any] + }, + R: { + name: 'retreat (R)', + steps: [ProvinceCheck.retreated, ProvinceCheck.any] + }, + D: { + name: 'disband (D)', + steps: [ProvinceCheck.present] + }, + A: { + name: 'build army (A)', + steps: [ProvinceCheck.any] + }, + F: { + name: 'build fleet (F)', + steps: [ProvinceCheck.any] + }, +}; + +export const POSSIBLE_ORDERS = { + // Allowed orders for movement phase step. + M: ['H', 'M', 'V', 'S', 'C'], + // Allowed orders for retreat phase step. + R: ['R', 'D'], + // Allowed orders for adjustment phase step. + A: ['D', 'A', 'F'], + sorting: { + M: {M: 0, V: 1, S: 2, C: 3, H: 4}, + R: {R: 0, D: 1}, + A: {A: 0, F: 1, D: 2} + }, + sortOrderTypes: function (arr, phaseType) { + arr.sort((a, b) => POSSIBLE_ORDERS.sorting[phaseType][a] - POSSIBLE_ORDERS.sorting[phaseType][b]); + } +}; + +export function extendOrderBuilding(powerName, orderType, currentOrderPath, location, onBuilding, onBuilt, onError) { + const selectedPath = [orderType].concat(currentOrderPath, location); + if (selectedPath.length - 1 < ORDER_BUILDER[orderType].steps.length) { + // Checker OK, update. + onBuilding(powerName, selectedPath); + } else { + try { + // Order created. + const orderString = ProvinceCheck.buildOrder(selectedPath); + onBuilt(powerName, orderString); + } catch (error) { + onError(error.toString()); + } + } +} diff --git a/diplomacy/web/src/gui/utils/power_view.jsx b/diplomacy/web/src/gui/utils/power_view.jsx new file mode 100644 index 0000000..df76da4 --- /dev/null +++ b/diplomacy/web/src/gui/utils/power_view.jsx @@ -0,0 +1,65 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +import {STRINGS} from "../../diplomacy/utils/strings"; +import React from "react"; + +function getName(power) { + if (power.isEliminated()) + return <span className="dummy"><em><s>{power.name.toLowerCase()}</s> (eliminated)</em></span>; + return power.name; +} + +function getController(power) { + if (power.isEliminated()) + return <span className="dummy"><em>N/A</em></span>; + const controller = power.getController(); + return <span className={controller === STRINGS.DUMMY ? 'dummy' : 'controller'}>{controller}</span>; +} + +function getOrderFlag(power) { + if (power.isEliminated()) + return <span className="dummy"><em>N/A</em></span>; + const value = ['no', 'empty', 'yes'][power.order_is_set]; + return <span className={value}>{value}</span>; +} + +function getWaitFlag(power) { + if (power.isEliminated()) + return <span className="dummy"><em>N/A</em></span>; + return <span className={power.wait ? 'wait' : 'no-wait'}>{power.wait ? 'yes' : 'no'}</span>; +} + +const GETTERS = { + name: getName, + controller: getController, + order_is_set: getOrderFlag, + wait: getWaitFlag +}; + +export class PowerView { + constructor(power) { + this.power = power; + } + + static wrap(power) { + return new PowerView(power); + } + + get(key) { + return GETTERS[key](this.power); + } +} diff --git a/diplomacy/web/src/gui/utils/province.js b/diplomacy/web/src/gui/utils/province.js new file mode 100644 index 0000000..fe54a82 --- /dev/null +++ b/diplomacy/web/src/gui/utils/province.js @@ -0,0 +1,117 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see <https://www.gnu.org/licenses/>. +// ============================================================================== +const ProvinceType = { + WATER: 'WATER', + COAST: 'COAST', + PORT: 'PORT', + LAND: 'LAND' +}; + +export class Province { + constructor(name, type, isSupplyCenter) { + this.name = name; + this.type = type; + this.coasts = {}; + this.parent = null; + this.neighbors = {}; + this.isSupplyCenter = isSupplyCenter; + this.controller = null; // null or power name. + this.controlType = null; // null, C (center), I (influence) or U (unit). + this.unit = null; // null, A or F + this.retreatController = null; + this.retreatUnit = null; // null or `{unit type} {loc}` + this.aliases = []; + } + + compareControlType(a, b) { + const controlTypeLevels = {C: 0, I: 1, U: 2}; + return controlTypeLevels[a] - controlTypeLevels[b]; + } + + __set_controller(controller, controlType) { + this.controller = controller; + this.controlType = controlType; + for (let coast of Object.values(this.coasts)) + coast.setController(controller, controlType); + } + + setController(controller, controlType) { + if (!['C', 'I', 'U'].includes(controlType)) + throw new Error(`Invalid province control type (${controlType}), expected 'C', 'I' or 'U'.`); + if (this.controller && this.controller !== controller) { + const controlTypeComparison = this.compareControlType(controlType, this.controlType); + if (controlTypeComparison === 0) + throw new Error(`Found 2 powers (${this.controller}, ${controller}) trying to control same province ` + + `(${this.name}) with same control type (${controlType} VS ${this.controlType}).`); + if (controlTypeComparison > 0) + this.__set_controller(controller, controlType); + } else + this.__set_controller(controller, controlType); + } + + setCoasts(provinces) { + const name = this.name.toUpperCase(); + for (let entry of Object.entries(provinces)) { + const pieces = entry[0].split(/[^A-Za-z0-9]+/); + if (pieces.length > 1 && pieces[0].toUpperCase() === name) { + this.coasts[entry[0]] = entry[1]; + entry[1].parent = this; + } + } + } + + setNeighbors(neighborProvinces) { + for (let province of neighborProvinces) + this.neighbors[province.name] = province; + } + + getLocationNames() { + const arr = Object.keys(this.coasts); + arr.splice(0, 0, this.name); + return arr; + } + + getOccupied(powerName) { + if (!this.controller) + return null; + if (powerName && this.controller !== powerName) + return null; + if (this.unit) + return this; + for (let coast of Object.values(this.coasts)) + if (coast.unit) + return coast; + return null; + } + + getRetreated(powerName) { + if (this.retreatController === powerName) + return this; + for (let coast of Object.values(this.coasts)) + if (coast.retreatController === powerName) + return coast; + return null; + } + + isCoast() { + return this.type === ProvinceType.COAST; + } + + isWater() { + return this.type === ProvinceType.WATER; + } +} diff --git a/diplomacy/web/src/gui/utils/saveGameToDisk.js b/diplomacy/web/src/gui/utils/saveGameToDisk.js new file mode 100644 index 0000000..aae69a4 --- /dev/null +++ b/diplomacy/web/src/gui/utils/saveGameToDisk.js @@ -0,0 +1,18 @@ +export function saveGameToDisk(game, onError) { + if (game.client) { + game.client.save() + .then((savedData) => { + const domLink = document.createElement('a'); + domLink.setAttribute( + 'href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(savedData))); + domLink.setAttribute('download', `${game.game_id}.json`); + domLink.style.display = 'none'; + document.body.appendChild(domLink); + domLink.click(); + document.body.removeChild(domLink); + }) + .catch(exc => onError(`Error while saving game: ${exc.toString()}`)); + } else { + onError(`Cannot save this game.`); + } +} |