mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-30 17:40:47 +00:00
Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.
This commit is contained in:
parent
eea059ce5a
commit
3140458246
42 changed files with 13026 additions and 390 deletions
302
diplomacy/utils/common.ts
Normal file
302
diplomacy/utils/common.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
// diplomacy/utils/common.ts
|
||||
// Common utils symbols used in diplomacy network code.
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import { Buffer } from 'buffer'; // Needed for Base64 operations
|
||||
|
||||
// Placeholder for exceptions - will be imported from a dedicated exceptions file later
|
||||
class CommonKeyException extends Error {
|
||||
constructor(key: string) {
|
||||
super(`Common key found: ${key}`);
|
||||
this.name = 'CommonKeyException';
|
||||
}
|
||||
}
|
||||
|
||||
// Datetime since timestamp 0.
|
||||
export const EPOCH = new Date(Date.UTC(1970, 0, 1, 0, 0, 0));
|
||||
|
||||
// Regex used for conversion from camel case to snake case.
|
||||
const REGEX_CONSECUTIVE_UPPER_CASES = /[A-Z]{2,}/g; // Adjusted for JS: removed compile, added g
|
||||
const REGEX_LOWER_THEN_UPPER_CASES = /([a-z0-9])([A-Z])/g; // Adjusted for JS: removed compile, added g
|
||||
const REGEX_UNDERSCORE_THEN_LETTER = /_([a-z])/g; // Adjusted for JS: removed compile, added g
|
||||
const REGEX_START_BY_LOWERCASE = /^[a-z]/; // Adjusted for JS: removed compile
|
||||
|
||||
/**
|
||||
* Hash long password to allow bcrypt to handle password longer than 72 characters.
|
||||
* Module private method.
|
||||
* @param password - password to hash.
|
||||
* @returns The hashed password, base64 encoded.
|
||||
*/
|
||||
function _sub_hash_password(password: string): string {
|
||||
// Bcrypt only handles passwords up to 72 characters. We use this hashing method as a work around.
|
||||
// Suggested in bcrypt PyPI page (2018/02/08 12:36 EST): https://pypi.python.org/pypi/bcrypt/3.1.0
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(password, 'utf-8');
|
||||
return hash.digest('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if password matches hashed.
|
||||
* NOTE: This is a STUB and NOT a secure bcrypt replacement.
|
||||
* @param password - password to check.
|
||||
* @param hashed - a password hashed with hash_password().
|
||||
* @returns Indicates if the password matches the hash.
|
||||
*/
|
||||
export function is_valid_password(password: string, hashed: string): boolean {
|
||||
console.warn("`is_valid_password` is using a STUB comparison and is NOT SECURE.");
|
||||
// In a real scenario, use a library like bcrypt.compareSync()
|
||||
// This stub assumes `hashed` is the output of our stubbed `hash_password`.
|
||||
const sub_hashed_password = _sub_hash_password(password);
|
||||
// Example: if hash_password just appended a salt, check that.
|
||||
// This is highly insecure and just for structure.
|
||||
return hashed.startsWith(sub_hashed_password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash password. Accepts password longer than 72 characters. Public method.
|
||||
* NOTE: This is a STUB and NOT a secure bcrypt replacement.
|
||||
* @param password - The password to hash
|
||||
* @returns The hashed password.
|
||||
*/
|
||||
export function hash_password(password: string): string {
|
||||
console.warn("`hash_password` is a STUB and is NOT SECURE. It does not use bcrypt salts.");
|
||||
// In a real scenario, use a library like bcrypt.hashSync()
|
||||
const sub_hashed = _sub_hash_password(password);
|
||||
// This is just a placeholder, not a real salt process.
|
||||
return `${sub_hashed}$INSECURE_STUB_SALT`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a token with 2 * n_bytes characters (n_bytes bytes encoded in hexadecimal).
|
||||
*/
|
||||
export function generate_token(n_bytes: number = 128): string {
|
||||
return crypto.randomBytes(n_bytes).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given variable is a dictionary-like object (plain object in JS/TS).
|
||||
* @param dict_to_check - Object to check.
|
||||
* @returns Indicates if the object is a plain object.
|
||||
*/
|
||||
export function is_dictionary(dict_to_check: any): boolean {
|
||||
return typeof dict_to_check === 'object' && dict_to_check !== null && !Array.isArray(dict_to_check) && !(dict_to_check instanceof Date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given variable is a sequence-like object (array in JS/TS).
|
||||
* Note that strings will not be considered as sequences.
|
||||
* @param seq_to_check - Sequence-like object to check.
|
||||
* @returns Indicates if the object is array-like.
|
||||
*/
|
||||
export function is_sequence(seq_to_check: any): boolean {
|
||||
// Strings and dicts are not valid sequences.
|
||||
if (typeof seq_to_check === 'string' || is_dictionary(seq_to_check)) {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(seq_to_check) || (typeof seq_to_check === 'object' && seq_to_check !== null && typeof (seq_to_check as any)[Symbol.iterator] === 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string (expected to be in camel case) to snake case.
|
||||
* @param name - string to convert.
|
||||
* @returns snake case version of given name.
|
||||
*/
|
||||
export function camel_case_to_snake_case(name: string): string {
|
||||
if (name === '') {
|
||||
return name;
|
||||
}
|
||||
// Python: separated_consecutive_uppers = REGEX_CONSECUTIVE_UPPER_CASES.sub(lambda m: '_'.join(c for c in m.group(0)), name)
|
||||
// JS:
|
||||
let separated_consecutive_uppers = name.replace(REGEX_CONSECUTIVE_UPPER_CASES, (match) => {
|
||||
return Array.from(match).join('_');
|
||||
});
|
||||
// Python: return REGEX_LOWER_THEN_UPPER_CASES.sub(r'\1_\2', separated_consecutive_uppers).lower()
|
||||
// JS:
|
||||
return separated_consecutive_uppers.replace(REGEX_LOWER_THEN_UPPER_CASES, '$1_$2').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string (expected to be in snake case) to camel case and convert first letter
|
||||
* to upper case if it's in lowercase.
|
||||
* @param name - string to convert.
|
||||
* @returns camel case version of given name.
|
||||
*/
|
||||
export function snake_case_to_upper_camel_case(name: string): string {
|
||||
if (name === '') {
|
||||
return name;
|
||||
}
|
||||
// Python: first_lower_case_to_upper = REGEX_START_BY_LOWERCASE.sub(lambda m: m.group(0).upper(), name)
|
||||
// JS:
|
||||
let first_lower_case_to_upper = name.replace(REGEX_START_BY_LOWERCASE, (match) => match.toUpperCase());
|
||||
// Python: return REGEX_UNDERSCORE_THEN_LETTER.sub(lambda m: m.group(1).upper(), first_lower_case_to_upper)
|
||||
// JS:
|
||||
return first_lower_case_to_upper.replace(REGEX_UNDERSCORE_THEN_LETTER, (match, charAfterUnderscore) => charAfterUnderscore.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that dictionaries does not share keys.
|
||||
*/
|
||||
export function assert_no_common_keys(dict1: object, dict2: object): void {
|
||||
const keys1 = Object.keys(dict1);
|
||||
const keys2 = Object.keys(dict2);
|
||||
|
||||
let smallest_dict_keys: string[], biggest_dict: object;
|
||||
if (keys1.length < keys2.length) {
|
||||
smallest_dict_keys = keys1;
|
||||
biggest_dict = dict2;
|
||||
} else {
|
||||
smallest_dict_keys = keys2;
|
||||
biggest_dict = dict1;
|
||||
}
|
||||
for (const key of smallest_dict_keys) {
|
||||
if (key in biggest_dict) {
|
||||
throw new CommonKeyException(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current timestamp with microsecond resolution.
|
||||
* Note: JavaScript's Date.now() is milliseconds. For microseconds, multiply by 1000.
|
||||
* For more precise microsecond timing, process.hrtime() could be used in Node.js,
|
||||
* but this simple multiplication matches the Python version's intent if not its exact precision source.
|
||||
*/
|
||||
export function timestamp_microseconds(): number {
|
||||
// Python: delta = datetime.now() - EPOCH
|
||||
// return (delta.days * 24 * 60 * 60 + delta.seconds) * 1000000 + delta.microseconds
|
||||
return Date.now() * 1000;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a new class to be used as string comparator for sorting.
|
||||
*/
|
||||
export function str_cmp_class<T extends string>(compare_function: (a: T, b: T) => number): { new(value: T): { value: T, toString(): string } } {
|
||||
class StringComparator {
|
||||
public value: T;
|
||||
private cmp_fn: (a: T, b: T) => number;
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value; // Already a string in TS context generally
|
||||
this.cmp_fn = compare_function;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
// These methods are what Array.prototype.sort() would look for if comparing instances directly.
|
||||
// However, usually, you pass a compare function to sort.
|
||||
// For compatibility with Python's use (e.g. storing these wrapped objects in a list and calling sort()),
|
||||
// one might need a custom sort that extracts .value or uses these.
|
||||
// For direct use in JS sort: list.sort((a, b) => a.cmp_fn(a.value, b.value))
|
||||
|
||||
// Not directly used by Array.sort in JS, but good for completeness if objects are compared.
|
||||
equals(other: StringComparator | string): boolean {
|
||||
const otherValue = (typeof other === 'string') ? other : other.value;
|
||||
return this.cmp_fn(this.value, otherValue as T) === 0;
|
||||
}
|
||||
|
||||
lessThan(other: StringComparator | string): boolean {
|
||||
const otherValue = (typeof other === 'string') ? other : other.value;
|
||||
return this.cmp_fn(this.value, otherValue as T) < 0;
|
||||
}
|
||||
}
|
||||
// To make it somewhat unique like Python's id()-based naming, though not strictly necessary in TS.
|
||||
// Object.defineProperty(StringComparator, 'name', { value: `StringComparator${Date.now()}${Math.random()}` });
|
||||
return StringComparator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert element to a string and make sure string is wrapped in either simple quotes
|
||||
* (if contains double quotes) or double quotes (if contains simple quotes).
|
||||
* If no quotes, or both types of quotes, defaults to double quotes.
|
||||
*/
|
||||
export function to_string_with_quoting(element: any): string {
|
||||
const s_element = String(element);
|
||||
const hasDouble = s_element.includes('"');
|
||||
const hasSingle = s_element.includes("'");
|
||||
|
||||
if (hasDouble && !hasSingle) {
|
||||
return `'${s_element}'`;
|
||||
}
|
||||
// Default to double quotes if no quotes, or if both types are present (escaping would be needed for correctness in that case)
|
||||
return `"${s_element}"`;
|
||||
}
|
||||
|
||||
|
||||
export class StringableCode {
|
||||
public readonly code: number | null;
|
||||
public readonly message: string;
|
||||
|
||||
constructor(code: number | string, message?: string) {
|
||||
if (typeof code === 'string' && message === undefined) {
|
||||
const message_parts = code.split(':');
|
||||
if (message_parts.length > 1 && /^\d+$/.test(message_parts[0]!)) {
|
||||
this.code = parseInt(message_parts[0]!, 10);
|
||||
this.message = message_parts.slice(1).join(':');
|
||||
} else {
|
||||
this.code = null;
|
||||
this.message = code;
|
||||
}
|
||||
} else if (typeof code === 'number' && message !== undefined) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
} else if (typeof code === 'number' && message === undefined) { // Only code provided
|
||||
this.code = code;
|
||||
this.message = String(code);
|
||||
}
|
||||
else { // Fallback or error
|
||||
this.code = null;
|
||||
this.message = String(code); // Treat code as message if types are unexpected
|
||||
}
|
||||
}
|
||||
|
||||
equals(other: StringableCode | string | number): boolean {
|
||||
if (other instanceof StringableCode) {
|
||||
return this.code === other.code;
|
||||
}
|
||||
// In Python, it compared self._message == str(other).
|
||||
// For more robustness, we might compare code if other is number, message if string.
|
||||
if (typeof other === 'number' && this.code !== null) {
|
||||
return this.code === other;
|
||||
}
|
||||
return this.message === String(other);
|
||||
}
|
||||
|
||||
// __hash__ is not directly applicable in JS/TS objects for Map keys in the same way.
|
||||
// If used as Map keys, the object reference is used, or toString() for string-keyed maps.
|
||||
|
||||
// Python's __mod__ was for string formatting like 'Error: %s' % value
|
||||
format(...values: any[]): StringableCode {
|
||||
let formatted_message = this.message;
|
||||
for (const value of values) {
|
||||
formatted_message = formatted_message.replace(/%s/, String(value)); // Simple %s replacement
|
||||
}
|
||||
// More sophisticated formatting would require a proper sprintf-js or similar.
|
||||
return new StringableCode(this.code, formatted_message);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
get repr(): string { // Getter for representation
|
||||
return `${this.code}:${this.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Stub for Tornado utilities, as Tornado is Python-specific.
|
||||
export class Tornado {
|
||||
/**
|
||||
* Modify exception handler method of given IO loop so that IO loop stops and raises
|
||||
* as soon as an exception is thrown from a callback.
|
||||
* @param io_loop - IO loop (Tornado specific, type any for TS)
|
||||
*/
|
||||
static stop_loop_on_callback_error(io_loop: any): void {
|
||||
console.warn("Tornado.stop_loop_on_callback_error is a STUB and not applicable in typical Node.js environments.");
|
||||
// In a Node.js/TS environment, unhandled exceptions in async operations
|
||||
// would typically be caught by process.on('uncaughtException') or promise .catch() handlers.
|
||||
}
|
||||
}
|
||||
52
diplomacy/utils/constants.ts
Normal file
52
diplomacy/utils/constants.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// diplomacy/utils/constants.ts
|
||||
// Some constant / config values used in Diplomacy package.
|
||||
|
||||
// Number of times to try to connect before throwing an exception.
|
||||
export const NB_CONNECTION_ATTEMPTS = 12;
|
||||
|
||||
// Time to wait between to connection trials.
|
||||
export const ATTEMPT_DELAY_SECONDS = 5;
|
||||
|
||||
// Time to wait between to server backups.
|
||||
export const DEFAULT_BACKUP_DELAY_SECONDS = 10 * 60; // 10 minutes.
|
||||
|
||||
// Default server ping interval. // Used for sockets ping.
|
||||
export const DEFAULT_PING_SECONDS = 30;
|
||||
|
||||
// Time to wait to receive a response for a request sent to server.
|
||||
export const REQUEST_TIMEOUT_SECONDS = 30;
|
||||
|
||||
// Default host name for a server to connect to.
|
||||
export const DEFAULT_HOST = 'localhost';
|
||||
|
||||
// Default port for normal non-securized server.
|
||||
export const DEFAULT_PORT = 8432;
|
||||
|
||||
// Default port for secure SSL server (not yet used).
|
||||
export const DEFAULT_SSL_PORT = 8433;
|
||||
|
||||
// Special username and password to use to connect as a bot recognized by diplomacy module.
|
||||
// This bot is called "private bot".
|
||||
export const PRIVATE_BOT_USERNAME = '#bot@2e723r43tr70fh2239-qf3947-3449-21128-9dh1321d12dm13d83820d28-9dm,xw201=ed283994f4n832483';
|
||||
export const PRIVATE_BOT_PASSWORD = '#bot:password:28131821--mx1fh5g7hg5gg5g´[],s222222223djdjje399333x93901deedd|e[[[]{{|@S{@244f';
|
||||
|
||||
// Time to wait to let a bot set orders for a dummy power.
|
||||
export const PRIVATE_BOT_TIMEOUT_SECONDS = 60;
|
||||
|
||||
// Default rules used to construct a Game object when no rules are provided.
|
||||
export const DEFAULT_GAME_RULES: string[] = ['SOLITAIRE', 'NO_PRESS', 'IGNORE_ERRORS', 'POWER_CHOICE'];
|
||||
|
||||
/**
|
||||
* Constants to define flags for attribute Power.order_is_set.
|
||||
*/
|
||||
export enum OrderSettings {
|
||||
ORDER_NOT_SET = 0,
|
||||
ORDER_SET_EMPTY = 1,
|
||||
ORDER_SET = 2,
|
||||
}
|
||||
|
||||
// Python's OrderSettings.ALL_SETTINGS was primarily for runtime validation.
|
||||
// In TypeScript, the enum itself serves as the definition of possible values.
|
||||
// If specific runtime checks against a set of these values are needed elsewhere,
|
||||
// it can be reconstructed there, e.g., new Set(Object.values(OrderSettings).filter(v => typeof v === 'number'))
|
||||
// For now, ALL_SETTINGS is omitted as it's not directly translatable or idiomatic in the same way for enums.
|
||||
272
diplomacy/utils/exceptions.ts
Normal file
272
diplomacy/utils/exceptions.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// diplomacy/utils/exceptions.ts
|
||||
// Exceptions used in diplomacy code.
|
||||
|
||||
export class DiplomacyException extends Error {
|
||||
constructor(message: string = 'Diplomacy network code exception.') {
|
||||
// Clean up message similar to Python's self.__doc__.strip() if message is empty
|
||||
const finalMessage = message || (new.target.prototype.constructor as any).__doc__?.trim() || 'Diplomacy network code exception.';
|
||||
super(finalMessage);
|
||||
this.name = new.target.name; // Sets the error name to the class name
|
||||
// This is important for `instanceof` checks and for more descriptive error logging.
|
||||
Object.setPrototypeOf(this, new.target.prototype); // Maintain prototype chain
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadyScheduledException extends DiplomacyException {
|
||||
static __doc__ = "Cannot add a data already scheduled.";
|
||||
constructor(message?: string) {
|
||||
super(message || AlreadyScheduledException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class CommonKeyException extends DiplomacyException {
|
||||
static __doc__ = "Common key error.";
|
||||
constructor(key: string) {
|
||||
super(`Forbidden common key in two dicts (${key})`);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyException extends DiplomacyException {
|
||||
static __doc__ = "Key error.";
|
||||
constructor(key: string) {
|
||||
super(`Key error: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class LengthException extends DiplomacyException {
|
||||
static __doc__ = "Length error.";
|
||||
constructor(expected_length: number, given_length: number) {
|
||||
super(`Expected length ${expected_length}, got ${given_length}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export class NaturalIntegerException extends DiplomacyException {
|
||||
static __doc__ = "Expected a positive integer (int >= 0).";
|
||||
constructor(integer_name: string = '') {
|
||||
super(integer_name ? `Integer error: ${integer_name}. ${NaturalIntegerException.__doc__}` : NaturalIntegerException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class NaturalIntegerNotNullException extends NaturalIntegerException {
|
||||
static __doc__ = "Expected a strictly positive integer (int > 0).";
|
||||
constructor(integer_name: string = '') {
|
||||
super(integer_name ? `Integer error: ${integer_name}. ${NaturalIntegerNotNullException.__doc__}` : NaturalIntegerNotNullException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class RandomPowerException extends DiplomacyException {
|
||||
static __doc__ = "No enough playable powers to select random powers.";
|
||||
constructor(nb_powers: number, nb_available_powers: number) {
|
||||
super(`Cannot randomly select ${nb_powers} power(s) in ${nb_available_powers} available power(s).`);
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeException extends DiplomacyException {
|
||||
static __doc__ = "Type error.";
|
||||
constructor(expected_type: string, given_type: string) {
|
||||
super(`Expected type ${expected_type}, got type ${given_type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueException extends DiplomacyException {
|
||||
static __doc__ = "Value error.";
|
||||
constructor(expected_values: any[], given_value: any) {
|
||||
super(`Forbidden value ${given_value}, expected: ${expected_values.map(v => String(v)).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationException extends DiplomacyException {
|
||||
static __doc__ = "Unknown notification.";
|
||||
constructor(message?: string) {
|
||||
super(message || NotificationException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseException extends DiplomacyException {
|
||||
static __doc__ = "Unknown response.";
|
||||
constructor(message?: string) {
|
||||
super(message || ResponseException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestException extends ResponseException {
|
||||
static __doc__ = "Unknown request.";
|
||||
constructor(message?: string) {
|
||||
super(message || RequestException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminTokenException extends ResponseException {
|
||||
static __doc__ = "Invalid token for admin operations.";
|
||||
constructor(message?: string) {
|
||||
super(message || AdminTokenException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class DaidePortException extends ResponseException {
|
||||
static __doc__ = "Daide server not started for the game";
|
||||
constructor(message?: string) {
|
||||
super(message || DaidePortException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameCanceledException extends ResponseException {
|
||||
static __doc__ = "Game was cancelled.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameCanceledException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameCreationException extends ResponseException {
|
||||
static __doc__ = "Cannot create more games on that server.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameCreationException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameFinishedException extends ResponseException {
|
||||
static __doc__ = "This game is finished.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameFinishedException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameIdException extends ResponseException {
|
||||
static __doc__ = "Invalid game ID.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameIdException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameJoinRoleException extends ResponseException {
|
||||
static __doc__ = "A token can have only one role inside a game: player, observer or omniscient.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameJoinRoleException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameRoleException extends ResponseException {
|
||||
static __doc__ = "Game role does not accepts this action.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameRoleException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameMasterTokenException extends ResponseException {
|
||||
static __doc__ = "Invalid token for master operations.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameMasterTokenException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameNotPlayingException extends ResponseException {
|
||||
static __doc__ = "Game not playing.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameNotPlayingException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameObserverException extends ResponseException {
|
||||
static __doc__ = "Disallowed observation for non-master users.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameObserverException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GamePhaseException extends ResponseException {
|
||||
static __doc__ = "Data does not match current game phase.";
|
||||
constructor(expected?: string | null, given?: string | null, message?: string) {
|
||||
let constructedMessage = message || GamePhaseException.__doc__;
|
||||
if (expected !== undefined && expected !== null) { // Allow expected to be null but explicitly passed
|
||||
constructedMessage += ` Expected: ${expected}`;
|
||||
}
|
||||
if (given !== undefined && given !== null) { // Allow given to be null but explicitly passed
|
||||
constructedMessage += ` Given: ${given}`;
|
||||
}
|
||||
super(constructedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export class GamePlayerException extends ResponseException {
|
||||
static __doc__ = "Invalid player.";
|
||||
constructor(message?: string) {
|
||||
super(message || GamePlayerException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameRegistrationPasswordException extends ResponseException {
|
||||
static __doc__ = "Invalid game registration password.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameRegistrationPasswordException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameSolitaireException extends ResponseException {
|
||||
static __doc__ = "A solitaire game does not accepts players.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameSolitaireException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class GameTokenException extends ResponseException {
|
||||
static __doc__ = "Invalid token for this game.";
|
||||
constructor(message?: string) {
|
||||
super(message || GameTokenException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class MapIdException extends ResponseException {
|
||||
static __doc__ = "Invalid map ID.";
|
||||
constructor(message?: string) {
|
||||
super(message || MapIdException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class MapPowerException extends ResponseException {
|
||||
static __doc__ = "Invalid map power.";
|
||||
constructor(power_name: string) {
|
||||
super(`Invalid map power ${power_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderException extends ResponseException {
|
||||
static __doc__ = "Given folder not available in server.";
|
||||
constructor(folder_path: string) {
|
||||
super(`Given folder not available in server: ${folder_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerRegistrationException extends ResponseException {
|
||||
static __doc__ = "Registration currently not allowed on this server.";
|
||||
constructor(message?: string) {
|
||||
super(message || ServerRegistrationException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenException extends ResponseException {
|
||||
static __doc__ = "Invalid token.";
|
||||
constructor(message?: string) {
|
||||
super(message || TokenException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserException extends ResponseException {
|
||||
static __doc__ = "Invalid user.";
|
||||
constructor(message?: string) {
|
||||
super(message || UserException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordException extends ResponseException {
|
||||
static __doc__ = "Password must not be empty.";
|
||||
constructor(message?: string) {
|
||||
super(message || PasswordException.__doc__);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerDirException extends ResponseException {
|
||||
static __doc__ = "Error with working folder.";
|
||||
constructor(server_dir: string) {
|
||||
super(`No server directory available at path ${server_dir}`);
|
||||
}
|
||||
}
|
||||
202
diplomacy/utils/export.ts
Normal file
202
diplomacy/utils/export.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// diplomacy/utils/export.ts
|
||||
// Responsible for exporting games in a standardized format to disk
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { DiplomacyGame } from '../engine/game';
|
||||
import { DiplomacyMap } from '../engine/map';
|
||||
import { GamePhaseData, GamePhaseDataData } from './game_phase_data'; // Assuming GamePhaseDataData is the interface for toDict()
|
||||
// import * as strings from './strings'; // Placeholder
|
||||
// import * as parsing from './parsing'; // Placeholder
|
||||
|
||||
// Temporary placeholders for imported string constants if not yet available
|
||||
const tempStrings = {
|
||||
MAP_NAME: 'map_name',
|
||||
RULES: 'rules',
|
||||
};
|
||||
|
||||
const logger = {
|
||||
warn: (message: string, ...args: any[]) => console.warn('[Export]', message, ...args),
|
||||
error: (message: string, ...args: any[]) => console.error('[Export]', message, ...args),
|
||||
};
|
||||
|
||||
const RULES_TO_SKIP: string[] = ['SOLITAIRE', 'NO_DEADLINE', 'CD_DUMMIES', 'ALWAYS_WAIT', 'IGNORE_ERRORS'];
|
||||
|
||||
export interface SavedGameFormat {
|
||||
id: string;
|
||||
map: string;
|
||||
rules: string[];
|
||||
phases: GamePhaseDataData[]; // Assuming GamePhaseDataData is the output of phase.toDict()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a game to a standardized JSON format
|
||||
* @param game - game to convert.
|
||||
* @param output_path - Optional path to file. If set, the JSON.stringify() of the saved_game is written to that file.
|
||||
* @param output_mode - Optional. The mode to use to write to the output_path (if provided). Defaults to 'a' (append).
|
||||
* @returns A game in the standard format, ready for JSON serialization.
|
||||
*/
|
||||
export function to_saved_game_format(game: DiplomacyGame, output_path?: string, output_mode: string = 'a'): SavedGameFormat {
|
||||
// In Python: phases = Game.get_phase_history(game)
|
||||
// Assuming game instance has getPhaseHistory()
|
||||
const phaseHistoryData: GamePhaseData[] = game.getPhaseHistory ? game.getPhaseHistory() : [];
|
||||
|
||||
// In Python: phases.append(Game.get_phase_data(game))
|
||||
// Assuming game instance has getCurrentPhaseData()
|
||||
const currentPhaseData: GamePhaseData | null = game.getPhaseData ? game.getPhaseData() : null;
|
||||
|
||||
const allPhases: GamePhaseData[] = [...phaseHistoryData];
|
||||
if (currentPhaseData) {
|
||||
allPhases.push(currentPhaseData);
|
||||
}
|
||||
|
||||
const rules = game.rules.filter(rule => !RULES_TO_SKIP.includes(rule));
|
||||
|
||||
const phases_to_dict: GamePhaseDataData[] = allPhases.map(phase => {
|
||||
const phaseDict = phase.toDict(); // Assumes GamePhaseData has toDict()
|
||||
// Extend states fields as in Python
|
||||
if (phaseDict.state) {
|
||||
phaseDict.state.game_id = game.game_id;
|
||||
phaseDict.state.map = game.map_name; // map_name from game instance
|
||||
phaseDict.state.rules = [...rules];
|
||||
}
|
||||
return phaseDict;
|
||||
});
|
||||
|
||||
const saved_game: SavedGameFormat = {
|
||||
id: game.game_id,
|
||||
map: game.map_name, // map_name from game instance
|
||||
rules: rules,
|
||||
phases: phases_to_dict
|
||||
};
|
||||
|
||||
if (output_path) {
|
||||
try {
|
||||
const fileExists = fs.existsSync(output_path);
|
||||
if (output_mode === 'a' && fileExists) {
|
||||
fs.appendFileSync(output_path, JSON.stringify(saved_game) + '\n', 'utf-8');
|
||||
} else {
|
||||
fs.writeFileSync(output_path, JSON.stringify(saved_game) + '\n', 'utf-8');
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(`Error writing saved game to disk: ${e.message}`);
|
||||
// Decide if to throw or just log
|
||||
}
|
||||
}
|
||||
return saved_game;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds a DiplomacyGame object from the saved game format.
|
||||
* @param saved_game - The saved game object.
|
||||
* @returns The game object restored from the saved game.
|
||||
*/
|
||||
export function from_saved_game_format(saved_game: SavedGameFormat): DiplomacyGame {
|
||||
const game_id = saved_game.id || null; // game_id can be null in Python version
|
||||
const kwargs: Partial<DiplomacyGame> = { // Use Partial for constructor if it accepts object
|
||||
map_name: saved_game.map || 'standard',
|
||||
rules: saved_game.rules || [],
|
||||
};
|
||||
|
||||
// Assuming DiplomacyGame constructor can handle these, or we use setters.
|
||||
// The Python version directly passes kwargs to Game(game_id=game_id, **kwargs)
|
||||
const game = new DiplomacyGame(game_id, kwargs); // This matches Python's Game(game_id, **kwargs)
|
||||
|
||||
const phase_history: GamePhaseData[] = [];
|
||||
for (const phase_dct of saved_game.phases || []) {
|
||||
// Assumes GamePhaseData has a static fromDict method
|
||||
phase_history.push(GamePhaseData.fromDict(phase_dct));
|
||||
}
|
||||
|
||||
// Assumes game has setPhaseData method
|
||||
if (game.setPhaseData) {
|
||||
game.setPhaseData(phase_history, true);
|
||||
} else {
|
||||
logger.warn("DiplomacyGame.setPhaseData method not found. Phase history not fully restored.");
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds multiple DiplomacyGame objects from each line in a .jsonl file.
|
||||
* @param input_path - The path to the input file. Expected content is one saved_game json per line.
|
||||
* @param on_error - Optional. What to do if a game conversion fails. Either 'raise', 'warn', 'ignore'.
|
||||
* @returns A list of DiplomacyGame objects.
|
||||
*/
|
||||
export function load_saved_games_from_disk(input_path: string, on_error: 'raise' | 'warn' | 'ignore' = 'raise'): DiplomacyGame[] {
|
||||
const loaded_games: DiplomacyGame[] = [];
|
||||
if (on_error !== 'raise' && on_error !== 'warn' && on_error !== 'ignore') {
|
||||
throw new Error("Expected values for on_error are 'raise', 'warn', 'ignore'.");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(input_path)) {
|
||||
logger.warn(`File ${input_path} does not exist. Aborting.`);
|
||||
return loaded_games;
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(input_path, 'utf-8');
|
||||
const lines = fileContent.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
try {
|
||||
const saved_game: SavedGameFormat = JSON.parse(line.trim());
|
||||
const game = from_saved_game_format(saved_game);
|
||||
loaded_games.push(game);
|
||||
} catch (exc: any) {
|
||||
if (on_error === 'raise') {
|
||||
throw exc;
|
||||
}
|
||||
if (on_error === 'warn') {
|
||||
logger.warn(String(exc));
|
||||
}
|
||||
// If 'ignore', do nothing.
|
||||
}
|
||||
}
|
||||
return loaded_games;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the saved game is valid.
|
||||
* This is an expensive operation because it replays the game.
|
||||
* @param saved_game - The saved game (from to_saved_game_format)
|
||||
* @returns A boolean that indicates if the game is valid
|
||||
*/
|
||||
export function is_valid_saved_game(saved_game: SavedGameFormat): boolean {
|
||||
// This is a complex validation and relies heavily on a fully functional Game and Map class.
|
||||
// It will be stubbed for now and marked as needing full review once Game/Map are more complete.
|
||||
logger.warn("is_valid_saved_game is a STUB and only performs basic structural checks.");
|
||||
|
||||
if (!saved_game || typeof saved_game !== 'object') return false;
|
||||
if (!saved_game.id || typeof saved_game.id !== 'string') return false;
|
||||
if (!saved_game.map || typeof saved_game.map !== 'string') return false;
|
||||
|
||||
try {
|
||||
// Try to create a map object to see if map name is valid at least structurally
|
||||
const map_object = new DiplomacyMap(saved_game.map);
|
||||
if (map_object.name !== saved_game.map && map_object.root_map !== saved_game.map.split('.')[0]) { // Simple check
|
||||
logger.warn(`is_valid_saved_game: Map name mismatch - ${map_object.name} vs ${saved_game.map}`);
|
||||
// return false; // This might be too strict if map names have aliases not yet loaded
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`is_valid_saved_game: Error instantiating map ${saved_game.map}: ${e}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(saved_game.rules)) return false;
|
||||
if (!Array.isArray(saved_game.phases) || saved_game.phases.length === 0) return false;
|
||||
|
||||
// TODO: Implement full game replay validation as in Python:
|
||||
// - Create a game instance.
|
||||
// - For each phase:
|
||||
// - Set phase data.
|
||||
// - Set orders.
|
||||
// - Validate orders against possible_orders.
|
||||
// - Process game.
|
||||
// - Compare resulting state (phase name, hash, units, centers) with next phase in saved_game.
|
||||
// - Handle DIFFERENT_ADJUDICATION rule.
|
||||
// - Validate message history constraints.
|
||||
|
||||
return true; // Placeholder, actual validation is complex.
|
||||
}
|
||||
112
diplomacy/utils/game_phase_data.ts
Normal file
112
diplomacy/utils/game_phase_data.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// diplomacy/utils/game_phase_data.ts
|
||||
/**
|
||||
* Utility class to save all data related to one game phase (phase name, state, messages and orders).
|
||||
*/
|
||||
|
||||
import { Jsonable } from './jsonable';
|
||||
import * as parsing from './parsing';
|
||||
import { StringableCode } from './common';
|
||||
// Placeholder for Message class, assuming it will extend Jsonable
|
||||
// import { Message } from '../engine/message';
|
||||
|
||||
// --- Placeholder for Message ---
|
||||
// This would normally be imported from '../engine/message'
|
||||
// For now, let's define a minimal placeholder that extends Jsonable
|
||||
// to allow `JsonableClassType` to work.
|
||||
class MessagePlaceholder extends Jsonable {
|
||||
static model = {
|
||||
time_sent: parsing.PrimitiveType(Number), // Assuming 'time_sent' is part of Message model for IndexedSequenceType
|
||||
// ... other message fields
|
||||
};
|
||||
public time_sent: number = 0;
|
||||
// Add other properties as needed for Message if they are accessed by GamePhaseData or its model
|
||||
constructor(kwargs: any) {
|
||||
super(kwargs);
|
||||
this.time_sent = kwargs.time_sent || 0;
|
||||
}
|
||||
}
|
||||
// --- End Placeholder for Message ---
|
||||
|
||||
|
||||
// Placeholder for string constants, replace with actual imports or definitions later
|
||||
const STRINGS = {
|
||||
NAME: 'name',
|
||||
STATE: 'state',
|
||||
ORDERS: 'orders',
|
||||
RESULTS: 'results',
|
||||
MESSAGES: 'messages',
|
||||
SUMMARY: 'summary', // Added based on python model keys
|
||||
STATISTICAL_SUMMARY: 'statistical_summary' // Added
|
||||
};
|
||||
|
||||
// MESSAGES_TYPE from Python:
|
||||
// parsing.IndexedSequenceType(
|
||||
// parsing.DictType(int, parsing.JsonableClassType(Message), SortedDict.builder(int, Message)),
|
||||
// 'time_sent'
|
||||
// )
|
||||
// For SortedDict.builder, we'll use a Map which preserves insertion order.
|
||||
// If strict sorting by key is needed beyond that, it has to be handled during processing.
|
||||
const MESSAGES_DICT_TYPE = new parsing.DictType(
|
||||
Number, // Keys are timestamps (int in Python)
|
||||
new parsing.JsonableClassType(MessagePlaceholder as any), // Values are Message objects
|
||||
(mapData: Record<string | number, MessagePlaceholder>) => new Map(Object.entries(mapData).map(([k, v]) => [Number(k), v])) // Builder to ensure Map
|
||||
);
|
||||
|
||||
const MESSAGES_TYPE_PARSER = new parsing.IndexedSequenceType(MESSAGES_DICT_TYPE, 'time_sent');
|
||||
export { MESSAGES_TYPE_PARSER as MESSAGES_TYPE_PLACEHOLDER }; // For game.ts if it uses this alias
|
||||
|
||||
export interface GamePhaseDataData {
|
||||
name: string;
|
||||
state: Record<string, any>;
|
||||
orders: Record<string, string[] | null>;
|
||||
results: Record<string, StringableCode[]>;
|
||||
messages: MessagePlaceholder[]; // Serialized as an array by IndexedSequenceType
|
||||
summary?: string | null;
|
||||
statistical_summary?: string | null;
|
||||
}
|
||||
|
||||
|
||||
export class GamePhaseData extends Jsonable {
|
||||
public name: string;
|
||||
public state: Record<string, any>; // Generic dictionary for game state
|
||||
public orders: Record<string, string[] | null>; // PowerName -> list of order strings or null
|
||||
public results: Record<string, StringableCode[]>; // UnitName -> list of StringableCode results
|
||||
public messages: Map<number, MessagePlaceholder>; // Timestamp -> Message object (Map from MESSAGES_DICT_TYPE builder)
|
||||
public summary: string | null;
|
||||
public statistical_summary: string | null;
|
||||
|
||||
static model: Record<string, any> = {
|
||||
[STRINGS.NAME]: new parsing.PrimitiveType(String),
|
||||
[STRINGS.STATE]: new parsing.PrimitiveType(Object), // Validates as a plain object
|
||||
[STRINGS.ORDERS]: new parsing.DictType(String, new parsing.OptionalValueType(new parsing.SequenceType(String))),
|
||||
[STRINGS.RESULTS]: new parsing.DictType(String, new parsing.SequenceType(new parsing.StringableType(StringableCode))),
|
||||
[STRINGS.MESSAGES]: MESSAGES_TYPE_PARSER,
|
||||
[STRINGS.SUMMARY]: new parsing.OptionalValueType(new parsing.PrimitiveType(String)),
|
||||
[STRINGS.STATISTICAL_SUMMARY]: new parsing.OptionalValueType(new parsing.PrimitiveType(String)),
|
||||
};
|
||||
|
||||
constructor(data: Partial<GamePhaseDataData> = {}) {
|
||||
// Initialize properties to default values first
|
||||
this.name = '';
|
||||
this.state = {};
|
||||
this.orders = {};
|
||||
this.results = {};
|
||||
this.messages = new Map<number, MessagePlaceholder>();
|
||||
this.summary = null;
|
||||
this.statistical_summary = null;
|
||||
|
||||
// Let Jsonable constructor handle kwargs based on the model
|
||||
super(data);
|
||||
|
||||
// Ensure correct types after super call, especially for those with builders or complex initializations
|
||||
// The Jsonable constructor with parsing.update_data should handle defaults from the model.
|
||||
// For MESSAGES_TYPE_PARSER, the `to_type` within IndexedSequenceType (which calls DictType's to_type)
|
||||
// should use the Map builder.
|
||||
if (data.messages && !(this.messages instanceof Map) && Array.isArray(data.messages)) {
|
||||
// If super didn't correctly make it a Map due to parsing_to_type not being fully recursive with builders yet
|
||||
this.messages = MESSAGES_TYPE_PARSER.to_type(data.messages) as Map<number, MessagePlaceholder>;
|
||||
}
|
||||
}
|
||||
// toDict will be inherited from Jsonable, using the static model.
|
||||
// fromDict will be inherited from Jsonable, using the static model.
|
||||
}
|
||||
21
diplomacy/utils/index.ts
Normal file
21
diplomacy/utils/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// diplomacy/utils/index.ts
|
||||
// This file will re-export symbols from other .ts files in this directory
|
||||
|
||||
export * from './common';
|
||||
export * from './constants';
|
||||
export * from './exceptions';
|
||||
export * from './game_phase_data';
|
||||
export * from './jsonable';
|
||||
export * from './keywords';
|
||||
export * from './order_results';
|
||||
export * from './parsing';
|
||||
export * from './priority_dict';
|
||||
export * from './scheduler_event';
|
||||
export * from './splitter';
|
||||
export * from './strings';
|
||||
export * from './time';
|
||||
|
||||
// Files not translated or intentionally skipped:
|
||||
// export.py - Python-specific export mechanisms
|
||||
// sorted_dict.py - Python's OrderedDict; use Map and manage order if critical
|
||||
// sorted_set.py - Python's SortedSet; use Set and sort array if critical
|
||||
152
diplomacy/utils/jsonable.ts
Normal file
152
diplomacy/utils/jsonable.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// diplomacy/utils/jsonable.ts
|
||||
/**
|
||||
* Abstract Jsonable class with automatic attributes checking and conversion to/from JSON dict.
|
||||
* To write a Jsonable sub-class:
|
||||
*
|
||||
* - Define a static `model` with expected attribute names and their types/validation logic.
|
||||
* - Override the constructor:
|
||||
* - Initialize each attribute defined in the model with a default value (e.g., null, undefined).
|
||||
* - Call `super(kwargs)` to have attributes checked and filled.
|
||||
* - Add further initialization code after the call to the parent constructor if needed.
|
||||
*/
|
||||
|
||||
import * as exceptions from './exceptions';
|
||||
import {
|
||||
validate_data as parsing_validate_data,
|
||||
update_data as parsing_update_data,
|
||||
to_json as parsing_to_json,
|
||||
to_type as parsing_to_type
|
||||
} from './parsing'; // Import actual functions
|
||||
|
||||
const logger = {
|
||||
error: (message: string, ...args: any[]) => console.error('[Jsonable]', message, ...args),
|
||||
warn: (message: string, ...args: any[]) => console.warn('[Jsonable]', message, ...args),
|
||||
};
|
||||
|
||||
export abstract class Jsonable {
|
||||
// Descendant classes should define this static model.
|
||||
// Example: static model = { my_attribute: 'string', count: 'number' };
|
||||
// For more complex types, the string could be a key to a parsing function or class.
|
||||
static model: Record<string, any> = {};
|
||||
|
||||
constructor(kwargs: Record<string, any> = {}) {
|
||||
const model = (this.constructor as typeof Jsonable).getModel();
|
||||
|
||||
// Initialize attributes defined in the model to null or undefined first
|
||||
for (const model_key in model) {
|
||||
if (model.hasOwnProperty(model_key)) {
|
||||
(this as any)[model_key] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const updated_kwargs: Record<string, any> = {};
|
||||
for (const model_key in model) {
|
||||
updated_kwargs[model_key] = undefined; // Or a default from model if specified
|
||||
}
|
||||
Object.assign(updated_kwargs, kwargs);
|
||||
|
||||
|
||||
try {
|
||||
// Placeholder for Python's parsing.validate_data
|
||||
// In TS, this would often be handled by constructor types or dedicated validation methods.
|
||||
parsing_validate_data(updated_kwargs, model);
|
||||
} catch (exception: any) {
|
||||
logger.error(`Error occurred while building class ${this.constructor.name}`);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
// Placeholder for Python's parsing.update_data (e.g. for default values)
|
||||
const final_attrs = parsing_update_data(updated_kwargs, model);
|
||||
|
||||
for (const model_key in model) {
|
||||
if (final_attrs.hasOwnProperty(model_key)) {
|
||||
(this as any)[model_key] = final_attrs[model_key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJsonString(): string {
|
||||
return JSON.stringify(this.toDict());
|
||||
}
|
||||
|
||||
toDict(): Record<string, any> {
|
||||
const model = (this.constructor as typeof Jsonable).getModel();
|
||||
const dict: Record<string, any> = {};
|
||||
for (const key in model) {
|
||||
if (model.hasOwnProperty(key) && typeof (this as any)[key] !== 'function') {
|
||||
// Ensure the property exists on 'this' before trying to access it
|
||||
if (Object.prototype.hasOwnProperty.call(this, key)) {
|
||||
dict[key] = parsing_to_json((this as any)[key], model[key]);
|
||||
} else {
|
||||
// Handle cases where model key might not be an actual property (e.g. if using getters without direct props)
|
||||
// Or if it was initialized to undefined and should be represented as null/undefined in dict.
|
||||
dict[key] = parsing_to_json(undefined, model[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional hook for subclasses to update/process the JSON dictionary
|
||||
* before it's used to create an instance.
|
||||
* @param jsonDict The JSON dictionary.
|
||||
*/
|
||||
static updateJsonDict?(jsonDict: Record<string, any>): void;
|
||||
|
||||
static fromDict<T extends Jsonable>(
|
||||
this: {
|
||||
new (...args: any[]): T;
|
||||
getModel(): Record<string, any>;
|
||||
updateJsonDict?(jsonDict: Record<string, any>): void;
|
||||
},
|
||||
jsonDict: Record<string, any>
|
||||
): T {
|
||||
if (typeof jsonDict !== 'object' || jsonDict === null) {
|
||||
throw new exceptions.TypeException('object', typeof jsonDict);
|
||||
}
|
||||
|
||||
const model = this.getModel();
|
||||
const default_json_dict: Record<string, any> = {};
|
||||
for (const key in model) {
|
||||
default_json_dict[key] = null; // Or handle defaults from model more explicitly
|
||||
}
|
||||
Object.assign(default_json_dict, jsonDict);
|
||||
|
||||
if (typeof this.updateJsonDict === 'function') {
|
||||
this.updateJsonDict(default_json_dict); // Allow class to modify dict before parsing
|
||||
}
|
||||
|
||||
const kwargs: Record<string, any> = {};
|
||||
for (const key in model) {
|
||||
if (model.hasOwnProperty(key)) {
|
||||
// Ensure key exists in default_json_dict before parsing, even if it's null/undefined
|
||||
kwargs[key] = parsing_to_type(default_json_dict[key], model[key]);
|
||||
}
|
||||
}
|
||||
return new this(kwargs);
|
||||
}
|
||||
|
||||
static fromJsonString<T extends Jsonable>(
|
||||
this: {
|
||||
new (...args: any[]): T;
|
||||
fromDict(jsonDict: Record<string, any>): T;
|
||||
getModel(): Record<string, any>;
|
||||
updateJsonDict?(jsonDict: Record<string, any>): void;
|
||||
},
|
||||
jsonStr: string
|
||||
): T {
|
||||
return this.fromDict(JSON.parse(jsonStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Descendant classes must implement this to provide their specific model.
|
||||
* Or they can override the static `model` property directly.
|
||||
*/
|
||||
static getModel(): Record<string, any> {
|
||||
// This default implementation relies on `static model` being defined on the subclass.
|
||||
// Caching logic from Python's get_model is omitted for simplicity here,
|
||||
// as direct static property access is common in TS/JS.
|
||||
return this.model || {};
|
||||
}
|
||||
}
|
||||
98
diplomacy/utils/keywords.ts
Normal file
98
diplomacy/utils/keywords.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// diplomacy/utils/keywords.ts
|
||||
// Contains aliases and keywords
|
||||
// Keywords are always single words
|
||||
// Aliases are only converted in a second pass, so if they contain a keyword, you should replace
|
||||
// the keyword with its abbreviation.
|
||||
|
||||
export const KEYWORDS: Record<string, string> = {
|
||||
'>': '',
|
||||
'-': '-',
|
||||
'ARMY': 'A',
|
||||
'FLEET': 'F',
|
||||
'WING': 'W', // Note: WING 'W' is not a standard DATC unit type, might be specific to a variant
|
||||
'THE': '',
|
||||
'NC': '/NC',
|
||||
'SC': '/SC',
|
||||
'EC': '/EC',
|
||||
'WC': '/WC',
|
||||
'MOVE': '',
|
||||
'MOVES': '',
|
||||
'MOVING': '',
|
||||
'ATTACK': '',
|
||||
'ATTACKS': '',
|
||||
'ATTACKING': '',
|
||||
'RETREAT': 'R',
|
||||
'RETREATS': 'R',
|
||||
'RETREATING': 'R',
|
||||
'SUPPORT': 'S',
|
||||
'SUPPORTS': 'S',
|
||||
'SUPPORTING': 'S',
|
||||
'CONVOY': 'C',
|
||||
'CONVOYS': 'C',
|
||||
'CONVOYING': 'C',
|
||||
'HOLD': 'H',
|
||||
'HOLDS': 'H',
|
||||
'HOLDING': 'H',
|
||||
'BUILD': 'B',
|
||||
'BUILDS': 'B',
|
||||
'BUILDING': 'B',
|
||||
'DISBAND': 'D',
|
||||
'DISBANDS': 'D',
|
||||
'DISBANDING': 'D',
|
||||
'DESTROY': 'D',
|
||||
'DESTROYS': 'D',
|
||||
'DESTROYING': 'D',
|
||||
'REMOVE': 'D',
|
||||
'REMOVES': 'D',
|
||||
'REMOVING': 'D',
|
||||
'WAIVE': 'V', // Note: 'V' for Waive is unusual; standard is often just WAIVE or no build/disband order
|
||||
'WAIVES': 'V',
|
||||
'WAIVING': 'V',
|
||||
'WAIVED': 'V',
|
||||
'KEEP': 'K', // Note: 'K' for Keep is non-standard
|
||||
'KEEPS': 'K',
|
||||
'KEEPING': 'K',
|
||||
'PROXY': 'P', // Non-standard
|
||||
'PROXIES': 'P',
|
||||
'PROXYING': 'P',
|
||||
'IS': '',
|
||||
'WILL': '',
|
||||
'IN': '',
|
||||
'AT': '',
|
||||
'ON': '',
|
||||
'TO': '',
|
||||
'OF': '\\',
|
||||
'FROM': '\\',
|
||||
'WITH': '?',
|
||||
'TSR': '=', // Trans-Siberian Railroad?
|
||||
'VIA': 'VIA',
|
||||
'THROUGH': '~',
|
||||
'OVER': '~',
|
||||
'BY': '~',
|
||||
'OR': '|',
|
||||
'BOUNCE': '|',
|
||||
'CUT': '|',
|
||||
'VOID': '?',
|
||||
'DISLODGED': '~',
|
||||
'DESTROYED': '*'
|
||||
};
|
||||
|
||||
export const ALIASES: Record<string, string> = {
|
||||
'NORTH COAST \\': '/NC \\',
|
||||
'SOUTH COAST \\': '/SC \\',
|
||||
'EAST COAST \\': '/EC \\',
|
||||
'WEST COAST \\': '/WC \\',
|
||||
'AN A': 'A',
|
||||
'A F': 'F',
|
||||
'A W': 'W',
|
||||
'NO C': '?',
|
||||
'~ C': '^',
|
||||
'~ =': '=',
|
||||
'? =': '=',
|
||||
'~ LAND': '_',
|
||||
'~ WATER': '_',
|
||||
'~ SEA': '_',
|
||||
'VIA C': 'VIA',
|
||||
'TRANS SIBERIAN RAILROAD': '=',
|
||||
'V B': 'B V' // Waive Build? This alias seems unusual.
|
||||
};
|
||||
54
diplomacy/utils/order_results.ts
Normal file
54
diplomacy/utils/order_results.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// diplomacy/utils/order_results.ts
|
||||
// Contains the results labels and code used by the engine
|
||||
|
||||
import { StringableCode } from './common';
|
||||
|
||||
// Constants
|
||||
const ORDER_RESULT_OFFSET = 10000;
|
||||
|
||||
export class OrderResult extends StringableCode {
|
||||
/**
|
||||
* Represents an order result
|
||||
* @param code - int code of the order result
|
||||
* @param message - human readable string message associated to the order result
|
||||
*/
|
||||
constructor(code: number | null, message: string) { // Allow code to be null as in StringableCode
|
||||
super(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
export const OK = new OrderResult(0, '');
|
||||
/**Order result OK, printed as ``''``*/
|
||||
|
||||
export const NO_CONVOY = new OrderResult(ORDER_RESULT_OFFSET + 1, 'no convoy');
|
||||
/**Order result NO_CONVOY, printed as ``'no convoy'``*/
|
||||
|
||||
export const BOUNCE = new OrderResult(ORDER_RESULT_OFFSET + 2, 'bounce');
|
||||
/**Order result BOUNCE, printed as ``'bounce'``*/
|
||||
|
||||
export const VOID = new OrderResult(ORDER_RESULT_OFFSET + 3, 'void');
|
||||
/**Order result VOID, printed as ``'void'``*/
|
||||
|
||||
export const CUT = new OrderResult(ORDER_RESULT_OFFSET + 4, 'cut');
|
||||
/**Order result CUT, printed as ``'cut'``*/
|
||||
|
||||
export const DISLODGED = new OrderResult(ORDER_RESULT_OFFSET + 5, 'dislodged');
|
||||
/**Order result DISLODGED, printed as ``'dislodged'``*/
|
||||
|
||||
export const DISRUPTED = new OrderResult(ORDER_RESULT_OFFSET + 6, 'disrupted');
|
||||
/**Order result DISRUPTED, printed as ``'disrupted'``*/
|
||||
|
||||
export const DISBAND = new OrderResult(ORDER_RESULT_OFFSET + 7, 'disband');
|
||||
/**Order result DISBAND, printed as ``'disband'``*/
|
||||
|
||||
export const MAYBE = new OrderResult(ORDER_RESULT_OFFSET + 8, 'maybe');
|
||||
/**Order result MAYBE, printed as ``'maybe'``*/
|
||||
|
||||
// Note: The enum OrderResult in diplomacy/engine/interfaces.ts
|
||||
// currently defines these as string enum values (e.g., OrderResult.NO_CONVOY = 'no convoy').
|
||||
// This new OrderResult class provides richer objects with codes and messages.
|
||||
// These two representations will need to be reconciled in a future step.
|
||||
// For instance, game logic might use these class instances, but expose
|
||||
// only `OrderResult.message` to align with the string enum values if needed elsewhere.
|
||||
// Or, the string enum in interfaces.ts might be replaced by using these constants' messages.
|
||||
// Example: `import { NO_CONVOY } from '../utils/order_results'; console.log(NO_CONVOY.message)` -> 'no convoy'
|
||||
542
diplomacy/utils/parsing.ts
Normal file
542
diplomacy/utils/parsing.ts
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
// diplomacy/utils/parsing.ts
|
||||
|
||||
import { DiplomacyException, TypeException, ValueException, CommonKeyException } from './exceptions';
|
||||
import { is_dictionary, is_sequence } from './common';
|
||||
import { Jsonable } from './jsonable'; // Import for JsonableClassType; may cause circular dependency if not handled carefully at runtime
|
||||
|
||||
const logger = {
|
||||
error: (message: string, ...args: any[]) => console.error('[Parsing]', message, ...args),
|
||||
warn: (message: string, ...args: any[]) => console.warn('[Parsing]', message, ...args),
|
||||
};
|
||||
|
||||
// --- Base ParserType Class ---
|
||||
export abstract class ParserType {
|
||||
// JS primitives: string, number, boolean, object (for dicts), null, undefined.
|
||||
// Python also had bytes. We'll map Python's int/float to number.
|
||||
// `object` is a loose term here; more specific checks happen in subclasses.
|
||||
protected static primitives = [String, Number, Boolean, Object, Array, Set, Map, Date];
|
||||
|
||||
abstract validate(element: any): void;
|
||||
|
||||
update(element: any): any {
|
||||
return element;
|
||||
}
|
||||
|
||||
to_type(json_value: any): any {
|
||||
return json_value;
|
||||
}
|
||||
|
||||
to_json(raw_value: any): any {
|
||||
if (raw_value instanceof Date) return raw_value.toISOString();
|
||||
if (raw_value instanceof Set) return Array.from(raw_value);
|
||||
if (raw_value instanceof Map) return Object.fromEntries(raw_value);
|
||||
if (raw_value && typeof (raw_value as any).toDict === 'function') {
|
||||
return (raw_value as any).toDict(); // For Jsonable instances
|
||||
}
|
||||
if (raw_value && typeof (raw_value as any).toJSON === 'function') {
|
||||
return (raw_value as any).toJSON(); // For classes with toJSON (like Date, but handled above)
|
||||
}
|
||||
return raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Concrete ParserType Subclasses ---
|
||||
|
||||
export class PrimitiveType extends ParserType {
|
||||
constructor(public element_type: Function) { // e.g., String, Number, Boolean, Object (for dict)
|
||||
super();
|
||||
if (![String, Number, Boolean, Object].includes(element_type)) {
|
||||
throw new DiplomacyException(`Expected a JS primitive constructor (String, Number, Boolean, Object), got ${element_type}`);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.element_type.name;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (this.element_type === String && typeof element !== 'string') {
|
||||
throw new TypeException(this.element_type.name, typeof element);
|
||||
} else if (this.element_type === Number && typeof element !== 'number') {
|
||||
throw new TypeException(this.element_type.name, typeof element);
|
||||
} else if (this.element_type === Boolean && typeof element !== 'boolean') {
|
||||
throw new TypeException(this.element_type.name, typeof element);
|
||||
} else if (this.element_type === Object && (typeof element !== 'object' || element === null || Array.isArray(element))) {
|
||||
// Basic check for "dictionary-like" plain objects
|
||||
throw new TypeException('object (plain)', Array.isArray(element) ? 'array' : typeof element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultValueType extends ParserType {
|
||||
public element_type_parser: ParserType;
|
||||
|
||||
constructor(element_type: any, public default_json_value: any) {
|
||||
super();
|
||||
this.element_type_parser = get_type(element_type);
|
||||
if (this.element_type_parser instanceof DefaultValueType || this.element_type_parser instanceof OptionalValueType) {
|
||||
throw new DiplomacyException("DefaultValueType cannot wrap another DefaultValueType or OptionalValueType.");
|
||||
}
|
||||
// Validate the default value itself at construction time
|
||||
if (default_json_value !== null && default_json_value !== undefined) {
|
||||
const typed_default = this.element_type_parser.to_type(default_json_value);
|
||||
this.element_type_parser.validate(typed_default);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.element_type_parser.toString()} (default ${JSON.stringify(this.default_json_value)})`;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (element !== null && element !== undefined) {
|
||||
this.element_type_parser.validate(element);
|
||||
}
|
||||
}
|
||||
|
||||
update(element: any): any {
|
||||
if (element !== null && element !== undefined) {
|
||||
return this.element_type_parser.update(element);
|
||||
}
|
||||
return (this.default_json_value === null || this.default_json_value === undefined)
|
||||
? this.default_json_value
|
||||
: this.element_type_parser.to_type(this.default_json_value);
|
||||
}
|
||||
|
||||
to_type(json_value: any): any {
|
||||
const value_to_convert = (json_value === null || json_value === undefined) ? this.default_json_value : json_value;
|
||||
if (value_to_convert === null || value_to_convert === undefined) return value_to_convert;
|
||||
return this.element_type_parser.to_type(value_to_convert);
|
||||
}
|
||||
|
||||
to_json(raw_value: any): any {
|
||||
if (raw_value === null || raw_value === undefined) {
|
||||
// If default_json_value is also null/undefined, return that.
|
||||
// Otherwise, this implies the raw_value represents the default, so serialize default.
|
||||
// This logic can be tricky. Python's copy(self.default_json_value) is safer.
|
||||
return this.default_json_value === null || this.default_json_value === undefined ? this.default_json_value : JSON.parse(JSON.stringify(this.default_json_value));
|
||||
}
|
||||
return this.element_type_parser.to_json(raw_value);
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionalValueType extends DefaultValueType {
|
||||
constructor(element_type: any) {
|
||||
super(element_type, null); // Default is null for optional values
|
||||
}
|
||||
toString(): string {
|
||||
return `${this.element_type_parser.toString()} | null`;
|
||||
}
|
||||
}
|
||||
|
||||
export class SequenceType extends ParserType {
|
||||
public element_type_parser: ParserType;
|
||||
|
||||
constructor(element_type: any, public sequence_builder: ((seq: any[]) => any) | null = null) {
|
||||
super();
|
||||
this.element_type_parser = get_type(element_type);
|
||||
this.sequence_builder = sequence_builder || ((seq) => seq);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Array<${this.element_type_parser.toString()}>`;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (!is_sequence(element)) { // is_sequence from common.ts should check for Array.isArray
|
||||
throw new TypeException('sequence (Array)', typeof element);
|
||||
}
|
||||
for (const seq_element of element) {
|
||||
this.element_type_parser.validate(seq_element);
|
||||
}
|
||||
}
|
||||
|
||||
update(element: any): any {
|
||||
const sequence = (element as any[]).map(seq_element => this.element_type_parser.update(seq_element));
|
||||
return this.sequence_builder!(sequence);
|
||||
}
|
||||
|
||||
to_type(json_value: any[]): any {
|
||||
if (!Array.isArray(json_value)) throw new TypeException('array', typeof json_value);
|
||||
const sequence = json_value.map(seq_element => this.element_type_parser.to_type(seq_element));
|
||||
return this.sequence_builder!(sequence);
|
||||
}
|
||||
|
||||
to_json(raw_value: any[]): any[] {
|
||||
if (!Array.isArray(raw_value)) throw new TypeException('array', typeof raw_value);
|
||||
return raw_value.map(seq_element => this.element_type_parser.to_json(seq_element));
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonableClassType extends ParserType {
|
||||
constructor(public element_type: typeof Jsonable) { // Expects a constructor of a Jsonable subclass
|
||||
super();
|
||||
if (!(typeof element_type === 'function' && element_type.prototype instanceof Jsonable)) {
|
||||
throw new DiplomacyException(`Expected a class extending Jsonable, got ${element_type}`);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.element_type.name;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (!(element instanceof this.element_type)) {
|
||||
throw new TypeException(this.element_type.name, element?.constructor?.name || typeof element);
|
||||
}
|
||||
}
|
||||
|
||||
to_type(json_value: Record<string, any>): Jsonable {
|
||||
if (typeof json_value !== 'object' || json_value === null) {
|
||||
throw new TypeException('object (for Jsonable)', typeof json_value);
|
||||
}
|
||||
return (this.element_type as any).fromDict(json_value);
|
||||
}
|
||||
|
||||
to_json(raw_value: Jsonable): Record<string, any> {
|
||||
if (!(raw_value instanceof this.element_type)) {
|
||||
throw new TypeException(this.element_type.name, raw_value?.constructor?.name || typeof raw_value);
|
||||
}
|
||||
return raw_value.toDict();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
export function get_type(desired_type: any): ParserType {
|
||||
if (desired_type instanceof ParserType) {
|
||||
return desired_type;
|
||||
}
|
||||
if (desired_type === String || desired_type === Number || desired_type === Boolean || desired_type === Object) {
|
||||
return new PrimitiveType(desired_type);
|
||||
}
|
||||
// Basic check for Jsonable subclasses (constructor)
|
||||
if (typeof desired_type === 'function' && desired_type.prototype instanceof Jsonable) {
|
||||
return new JsonableClassType(desired_type as typeof Jsonable);
|
||||
}
|
||||
// Add more sophisticated checks if needed, e.g., for Array to map to SequenceType
|
||||
// This part would need to be more robust to match Python's dynamic get_type fully.
|
||||
// For now, assuming explicit ParserType instances (e.g., new SequenceType(String)) are used in models.
|
||||
|
||||
// Fallback for unhandled types - this indicates an issue with model definition or get_type itself
|
||||
logger.warn(`get_type: Unhandled desired_type: ${desired_type}. Defaulting to PrimitiveType(Object) if it's a constructor, else error.`);
|
||||
if (typeof desired_type === 'function') return new PrimitiveType(Object); // A guess for unknown classes
|
||||
throw new DiplomacyException(`Cannot determine ParserType for: ${desired_type}`);
|
||||
}
|
||||
|
||||
|
||||
export function to_type(json_value: any, parser_type_input: any): any {
|
||||
return get_type(parser_type_input).to_type(json_value);
|
||||
}
|
||||
|
||||
export function to_json(raw_value: any, parser_type_input: any): any {
|
||||
return get_type(parser_type_input).to_json(raw_value);
|
||||
}
|
||||
|
||||
export function validate_data(data: Record<string, any>, model: Record<string, any>): void {
|
||||
if (!is_dictionary(data)) throw new TypeException("object", typeof data);
|
||||
if (!is_dictionary(model)) throw new TypeException("object", typeof model);
|
||||
|
||||
for (const model_key in model) {
|
||||
if (model.hasOwnProperty(model_key)) {
|
||||
const model_type_descriptor = model[model_key];
|
||||
try {
|
||||
get_type(model_type_descriptor).validate(data[model_key]);
|
||||
} catch (exception: any) {
|
||||
logger.error(`Error occurred while checking key ${model_key}`);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function update_data(data: Record<string, any>, model: Record<string, any>): Record<string, any> {
|
||||
const updatedData = { ...data };
|
||||
for (const model_key in model) {
|
||||
if (model.hasOwnProperty(model_key)) {
|
||||
const model_type_descriptor = model[model_key];
|
||||
const data_value = data.hasOwnProperty(model_key) ? data[model_key] : undefined; // Use undefined if key missing for DefaultValueType
|
||||
updatedData[model_key] = get_type(model_type_descriptor).update(data_value);
|
||||
}
|
||||
}
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
// --- Remaining ParserType Subclasses ---
|
||||
|
||||
export class ConverterType extends ParserType {
|
||||
public element_type_parser: ParserType;
|
||||
|
||||
constructor(
|
||||
element_type: any,
|
||||
public converter_function: (value: any) => any,
|
||||
public json_converter_function?: (json_value: any) => any
|
||||
) {
|
||||
super();
|
||||
this.element_type_parser = get_type(element_type);
|
||||
if (this.element_type_parser instanceof ConverterType) {
|
||||
throw new DiplomacyException("ConverterType cannot wrap another ConverterType.");
|
||||
}
|
||||
if (typeof converter_function !== 'function') {
|
||||
throw new DiplomacyException("converter_function must be a function.");
|
||||
}
|
||||
this.json_converter_function = json_converter_function || converter_function;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
this.element_type_parser.validate(this.converter_function(element));
|
||||
}
|
||||
|
||||
update(element: any): any {
|
||||
return this.element_type_parser.update(this.converter_function(element));
|
||||
}
|
||||
|
||||
to_type(json_value: any): any {
|
||||
return this.element_type_parser.to_type(this.json_converter_function!(json_value));
|
||||
}
|
||||
|
||||
to_json(raw_value: any): any {
|
||||
// Raw value is already of the target type after conversion by the main class using this.
|
||||
// So, we directly pass it to the wrapped parser's to_json.
|
||||
// The converter_function is used when setting/updating the attribute on the object.
|
||||
return this.element_type_parser.to_json(raw_value);
|
||||
}
|
||||
}
|
||||
|
||||
export class StringableType extends ParserType {
|
||||
private use_from_string: boolean;
|
||||
|
||||
constructor(public element_type: { new(...args: any[]): any; from_string?: (s: string) => any; name: string }) {
|
||||
super();
|
||||
this.use_from_string = typeof this.element_type.from_string === 'function';
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.element_type.name;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (!(element instanceof this.element_type)) {
|
||||
try {
|
||||
const element_to_str = this.to_json(element); // Converts to string via element.toString()
|
||||
const element_from_str = this.to_type(element_to_str); // Converts back via new() or from_string()
|
||||
const element_from_str_to_str = this.to_json(element_from_str);
|
||||
if (element_to_str !== element_from_str_to_str) {
|
||||
throw new TypeException(this.element_type.name, typeof element, "Value not consistently stringable/parsable.");
|
||||
}
|
||||
} catch (e) {
|
||||
throw new TypeException(this.element_type.name, typeof element, `Validation failed: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
to_type(json_value: string): any {
|
||||
if (typeof json_value !== 'string') throw new TypeException('string', typeof json_value);
|
||||
if (this.use_from_string) {
|
||||
return this.element_type.from_string!(json_value);
|
||||
}
|
||||
return new this.element_type(json_value);
|
||||
}
|
||||
|
||||
to_json(raw_value: any): string {
|
||||
if (!(raw_value instanceof this.element_type) && typeof raw_value?.toString !== 'function') {
|
||||
throw new TypeException(this.element_type.name, typeof raw_value);
|
||||
}
|
||||
return String(raw_value);
|
||||
}
|
||||
}
|
||||
|
||||
export class DictType extends ParserType {
|
||||
public key_type_parser: StringableType; // Keys must be stringable
|
||||
public val_type_parser: ParserType;
|
||||
|
||||
constructor(
|
||||
key_type: any, // Should be a constructor for a stringable type (String, Number, or class for StringableType)
|
||||
val_type: any,
|
||||
public dict_builder: ((dict: Record<string, any> | Map<any,any>) => any) | null = null
|
||||
) {
|
||||
super();
|
||||
this.key_type_parser = (key_type instanceof StringableType) ? key_type : new StringableType(key_type);
|
||||
this.val_type_parser = get_type(val_type);
|
||||
this.dict_builder = dict_builder || ((dict) => dict);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Record<${this.key_type_parser.toString()}, ${this.val_type_parser.toString()}>`;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (!is_dictionary(element) && !(element instanceof Map)) {
|
||||
throw new TypeException('dictionary or Map', typeof element);
|
||||
}
|
||||
const entries = (element instanceof Map) ? element.entries() : Object.entries(element);
|
||||
for (const [key, value] of entries) {
|
||||
this.key_type_parser.validate(key); // Key should be validated against its original type before to_json for Map keys
|
||||
this.val_type_parser.validate(value);
|
||||
}
|
||||
}
|
||||
|
||||
update(element: any): any {
|
||||
const result_dict: Record<string, any> = {};
|
||||
const entries = (element instanceof Map) ? element.entries() : Object.entries(element);
|
||||
for (const [key, value] of entries) {
|
||||
result_dict[this.key_type_parser.update(key)] = this.val_type_parser.update(value);
|
||||
}
|
||||
return this.dict_builder!(result_dict);
|
||||
}
|
||||
|
||||
to_type(json_value: Record<string, any>): any {
|
||||
if (!is_dictionary(json_value)) throw new TypeException('object (dictionary)', typeof json_value);
|
||||
const result_dict: Record<string, any> = {};
|
||||
for (const key in json_value) {
|
||||
if (json_value.hasOwnProperty(key)) {
|
||||
result_dict[this.key_type_parser.to_type(key)] = this.val_type_parser.to_type(json_value[key]);
|
||||
}
|
||||
}
|
||||
return this.dict_builder!(result_dict);
|
||||
}
|
||||
|
||||
to_json(raw_value: Record<string, any> | Map<any,any>): Record<string, any> {
|
||||
if (!is_dictionary(raw_value) && !(raw_value instanceof Map)) {
|
||||
throw new TypeException('dictionary or Map', typeof raw_value);
|
||||
}
|
||||
const result_json_dict: Record<string, any> = {};
|
||||
const entries = (raw_value instanceof Map) ? raw_value.entries() : Object.entries(raw_value);
|
||||
for (const [key, value] of entries) {
|
||||
result_json_dict[this.key_type_parser.to_json(key)] = this.val_type_parser.to_json(value);
|
||||
}
|
||||
return result_json_dict;
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexedSequenceType extends ParserType {
|
||||
public dict_type_parser: DictType;
|
||||
public sequence_type_parser: SequenceType;
|
||||
|
||||
constructor(dict_type: DictType, public key_name: string) {
|
||||
super();
|
||||
if (!(dict_type instanceof DictType)) {
|
||||
throw new DiplomacyException("IndexedSequenceType requires a DictType instance.");
|
||||
}
|
||||
this.dict_type_parser = dict_type;
|
||||
// The elements of the sequence are the values of the dictionary
|
||||
this.sequence_type_parser = new SequenceType(this.dict_type_parser.val_type_parser);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `IndexedSequence<${this.dict_type_parser.val_type_parser.toString()}, key: ${this.key_name}>`;
|
||||
}
|
||||
|
||||
validate(element: any): void { // Element in memory is a Map/object (dictionary)
|
||||
this.dict_type_parser.validate(element);
|
||||
}
|
||||
|
||||
update(element: any): any { // Element in memory is a Map/object
|
||||
return this.dict_type_parser.update(element);
|
||||
}
|
||||
|
||||
to_json(raw_value: Record<string, any> | Map<any, any>): any[] { // raw_value is a Map or object
|
||||
const values = (raw_value instanceof Map) ? Array.from(raw_value.values()) : Object.values(raw_value);
|
||||
return this.sequence_type_parser.to_json(values);
|
||||
}
|
||||
|
||||
to_type(json_value: any[]): any { // json_value is an array from JSON
|
||||
if (!Array.isArray(json_value)) throw new TypeException('array', typeof json_value);
|
||||
const loaded_sequence = this.sequence_type_parser.to_type(json_value);
|
||||
const result_dict: Record<string, any> = {};
|
||||
for (const element of loaded_sequence) {
|
||||
if (element && typeof element === 'object' && this.key_name in element) {
|
||||
const key = (element as any)[this.key_name];
|
||||
result_dict[this.key_type_parser.to_type(String(key))] = element; // Element is already to_type'd by sequence_type_parser
|
||||
} else {
|
||||
logger.warn(`IndexedSequenceType: Element missing key_name '${this.key_name}' or not an object.`, element);
|
||||
}
|
||||
}
|
||||
return this.dict_type_parser.dict_builder!(result_dict); // Apply dict_builder if any
|
||||
}
|
||||
}
|
||||
|
||||
export class EnumerationType extends ParserType {
|
||||
public enum_values: Set<any>;
|
||||
|
||||
constructor(enum_values: any[]) {
|
||||
super();
|
||||
if (!Array.isArray(enum_values) || enum_values.length === 0) {
|
||||
throw new DiplomacyException("EnumerationType requires a non-empty array of values.");
|
||||
}
|
||||
// Ensure all enum_values are primitives for reliable comparison
|
||||
enum_values.forEach(val => {
|
||||
if (typeof val !== 'string' && typeof val !== 'number' && typeof val !== 'boolean') {
|
||||
throw new DiplomacyException("EnumerationType values must be primitives (string, number, boolean).");
|
||||
}
|
||||
});
|
||||
this.enum_values = new Set(enum_values);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Enum<${Array.from(this.enum_values).map(String).join(" | ")}>`;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
if (!this.enum_values.has(element)) {
|
||||
throw new ValueException(Array.from(this.enum_values), element);
|
||||
}
|
||||
}
|
||||
// to_type and to_json are default from ParserType (identity)
|
||||
}
|
||||
|
||||
export class SequenceOfPrimitivesType extends ParserType {
|
||||
public allowed_primitive_constructors: Function[]; // e.g. [String, Number]
|
||||
|
||||
constructor(seq_of_primitives: Function[]) {
|
||||
super();
|
||||
if (!Array.isArray(seq_of_primitives) || seq_of_primitives.length === 0) {
|
||||
throw new DiplomacyException("SequenceOfPrimitivesType requires a non-empty array of primitive constructors.");
|
||||
}
|
||||
seq_of_primitives.forEach(p => {
|
||||
if (![String, Number, Boolean, Object].includes(p)) {
|
||||
throw new DiplomacyException(`Invalid primitive constructor in SequenceOfPrimitivesType: ${p}`);
|
||||
}
|
||||
});
|
||||
this.allowed_primitive_constructors = seq_of_primitives;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Primitives<${this.allowed_primitive_constructors.map(p => p.name).join(" | ")}>`;
|
||||
}
|
||||
|
||||
validate(element: any): void {
|
||||
const typeOfElement = typeof element;
|
||||
const constructorOfElement = element?.constructor;
|
||||
|
||||
const isValid = this.allowed_primitive_constructors.some(primitiveConstructor => {
|
||||
if (primitiveConstructor === String && typeOfElement === 'string') return true;
|
||||
if (primitiveConstructor === Number && typeOfElement === 'number') return true;
|
||||
if (primitiveConstructor === Boolean && typeOfElement === 'boolean') return true;
|
||||
if (primitiveConstructor === Object && typeOfElement === 'object' && element !== null && !Array.isArray(element)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
throw new TypeException(this.allowed_primitive_constructors.map(p => p.name).join(' or '), constructorOfElement?.name || typeOfElement);
|
||||
}
|
||||
}
|
||||
// to_type and to_json are default from ParserType (identity)
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions (update_model, extend_model) ---
|
||||
export function update_model(model: Record<string, any>, additional_keys: Record<string, any>, allow_duplicate_keys: boolean = true): Record<string, any> {
|
||||
if (!is_dictionary(model)) throw new TypeException("object", typeof model);
|
||||
if (!is_dictionary(additional_keys)) throw new TypeException("object", typeof additional_keys);
|
||||
|
||||
if (!allow_duplicate_keys) {
|
||||
assert_no_common_keys(model, additional_keys);
|
||||
}
|
||||
return { ...model, ...additional_keys };
|
||||
}
|
||||
|
||||
export function extend_model(model: Record<string, any>, additional_keys: Record<string, any>): Record<string, any> {
|
||||
return update_model(model, additional_keys, false);
|
||||
}
|
||||
194
diplomacy/utils/priority_dict.ts
Normal file
194
diplomacy/utils/priority_dict.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
// diplomacy/utils/priority_dict.ts
|
||||
|
||||
type HeapEntry<K, V extends number> = [V, K, boolean]; // [priority, key, isValid]
|
||||
|
||||
export class PriorityDict<K, V extends number> {
|
||||
private heap: Array<HeapEntry<K, V>> = [];
|
||||
private entries: Map<K, HeapEntry<K, V>> = new Map();
|
||||
|
||||
constructor(initial?: Record<string | number, V> | Array<[K, V]>) {
|
||||
if (initial) {
|
||||
if (Array.isArray(initial)) { // Array of [key, priority] tuples
|
||||
for (const [key, priority] of initial) {
|
||||
this.set(key, priority);
|
||||
}
|
||||
} else { // Record object
|
||||
for (const key in initial) {
|
||||
if (Object.prototype.hasOwnProperty.call(initial, key)) {
|
||||
// We need to be careful about key type if K is not string or number
|
||||
this.set(key as any as K, initial[key]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _siftup(index: number): void {
|
||||
let parent = Math.floor((index - 1) / 2);
|
||||
while (index > 0 && this.heap[index]![0] < this.heap[parent]![0]) {
|
||||
[this.heap[index], this.heap[parent]] = [this.heap[parent]!, this.heap[index]!];
|
||||
index = parent;
|
||||
parent = Math.floor((index - 1) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
private _siftdown(index: number): void {
|
||||
const N = this.heap.length;
|
||||
while (true) {
|
||||
let leftChildIdx = 2 * index + 1;
|
||||
let rightChildIdx = 2 * index + 2;
|
||||
let smallest = index;
|
||||
|
||||
if (leftChildIdx < N && this.heap[leftChildIdx]![0] < this.heap[smallest]![0]) {
|
||||
smallest = leftChildIdx;
|
||||
}
|
||||
if (rightChildIdx < N && this.heap[rightChildIdx]![0] < this.heap[smallest]![0]) {
|
||||
smallest = rightChildIdx;
|
||||
}
|
||||
|
||||
if (smallest !== index) {
|
||||
[this.heap[index], this.heap[smallest]] = [this.heap[smallest]!, this.heap[index]!];
|
||||
index = smallest;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _heappush(entry: HeapEntry<K, V>): void {
|
||||
this.heap.push(entry);
|
||||
this._siftup(this.heap.length - 1);
|
||||
}
|
||||
|
||||
private _heappop(): HeapEntry<K, V> | undefined {
|
||||
if (this.heap.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.heap.length === 1) {
|
||||
return this.heap.pop();
|
||||
}
|
||||
const top = this.heap[0];
|
||||
this.heap[0] = this.heap.pop()!;
|
||||
this._siftdown(0);
|
||||
return top;
|
||||
}
|
||||
|
||||
set(key: K, priority: V): void {
|
||||
if (this.entries.has(key)) {
|
||||
const oldEntry = this.entries.get(key)!;
|
||||
oldEntry[2] = false; // Mark old entry as invalid
|
||||
}
|
||||
const newEntry: HeapEntry<K, V> = [priority, key, true];
|
||||
this.entries.set(key, newEntry);
|
||||
this._heappush(newEntry);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
if (this.entries.has(key)) {
|
||||
const entry = this.entries.get(key)!;
|
||||
entry[2] = false; // Mark as invalid
|
||||
this.entries.delete(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
// Python's __getitem__ raises KeyError if key not found or entry invalid.
|
||||
// Here, we return undefined for simplicity, typical in JS/TS Map.get.
|
||||
// To strictly match Python, one might throw an error if !entry || !entry[2].
|
||||
return (entry && entry[2]) ? entry[0] : undefined;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
const entry = this.entries.get(key);
|
||||
return !!(entry && entry[2]);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.entries.size;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.entries.size === 0;
|
||||
}
|
||||
|
||||
smallest(): [V, K] | null {
|
||||
while (this.heap.length > 0 && !this.heap[0]![2]) { // Check isValid flag
|
||||
this._heappop(); // Remove invalid entries from the top
|
||||
}
|
||||
if (this.heap.length > 0) {
|
||||
return [this.heap[0]![0], this.heap[0]![1]]; // [priority, key]
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
popSmallest(): [V, K] | null {
|
||||
let smallestEntry: HeapEntry<K,V> | undefined;
|
||||
do {
|
||||
smallestEntry = this._heappop();
|
||||
} while (smallestEntry && !smallestEntry[2]); // Loop until a valid entry is found or heap is empty
|
||||
|
||||
if (smallestEntry) {
|
||||
this.entries.delete(smallestEntry[1]);
|
||||
return [smallestEntry[0], smallestEntry[1]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setDefault(key: K, defaultPriority: V): V {
|
||||
if (!this.has(key)) {
|
||||
this.set(key, defaultPriority);
|
||||
}
|
||||
return this.get(key)!; // Should exist now
|
||||
}
|
||||
|
||||
copy(): PriorityDict<K, V> {
|
||||
const newDict = new PriorityDict<K, V>();
|
||||
// Iterate over valid entries in the current dict to maintain priorities
|
||||
for (const [key, entry] of this.entries) {
|
||||
if (entry[2]) { // if valid
|
||||
newDict.set(key, entry[0]); // entry[0] is priority
|
||||
}
|
||||
}
|
||||
return newDict;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.heap = [];
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
/** Iterate over keys in priority order. */
|
||||
*keys(): IterableIterator<K> {
|
||||
const tempCopy = this.copy(); // Use a copy to not modify the original during iteration
|
||||
while (!tempCopy.isEmpty()) {
|
||||
const smallest = tempCopy.popSmallest();
|
||||
if (smallest) {
|
||||
yield smallest[1]; // Yield key
|
||||
} else {
|
||||
break; // Should not happen if isEmpty is false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over values (priorities) in priority order. */
|
||||
*values(): IterableIterator<V> {
|
||||
for (const key of this.keys()) {
|
||||
yield this.get(key)!; // get(key) returns priority
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over [key, value (priority)] pairs in priority order. */
|
||||
*items(): IterableIterator<[K, V]> {
|
||||
for (const key of this.keys()) {
|
||||
yield [key, this.get(key)!];
|
||||
}
|
||||
}
|
||||
|
||||
// For compatibility with for...of loops directly on the PriorityDict instance
|
||||
[Symbol.iterator](): IterableIterator<K> {
|
||||
return this.keys();
|
||||
}
|
||||
}
|
||||
38
diplomacy/utils/scheduler_event.ts
Normal file
38
diplomacy/utils/scheduler_event.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// diplomacy/utils/scheduler_event.ts
|
||||
// Scheduler event describing scheduler state for a specific data.
|
||||
|
||||
import { Jsonable } from './jsonable';
|
||||
import { PrimitiveType } from './parsing'; // Using PrimitiveType for clarity in model
|
||||
|
||||
export interface SchedulerEventData {
|
||||
time_unit: number;
|
||||
time_added: number;
|
||||
delay: number;
|
||||
current_time: number;
|
||||
}
|
||||
|
||||
export class SchedulerEvent extends Jsonable {
|
||||
public time_unit: number;
|
||||
public time_added: number;
|
||||
public delay: number;
|
||||
public current_time: number;
|
||||
|
||||
// Define the model for Jsonable serialization/deserialization
|
||||
static model: Record<string, any> = {
|
||||
'time_unit': new PrimitiveType(Number),
|
||||
'time_added': new PrimitiveType(Number),
|
||||
'delay': new PrimitiveType(Number),
|
||||
'current_time': new PrimitiveType(Number)
|
||||
};
|
||||
|
||||
constructor(kwargs: Partial<SchedulerEventData> = {}) {
|
||||
// Initialize properties to default values first
|
||||
this.time_unit = 0;
|
||||
this.time_added = 0;
|
||||
this.delay = 0;
|
||||
this.current_time = 0;
|
||||
|
||||
// Let Jsonable constructor handle kwargs based on the model
|
||||
super(kwargs);
|
||||
}
|
||||
}
|
||||
309
diplomacy/utils/sorted_dict.ts
Normal file
309
diplomacy/utils/sorted_dict.ts
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
// diplomacy/utils/sorted_dict.ts
|
||||
// Helper class to provide a dict with sorted keys.
|
||||
|
||||
import { is_dictionary } from './common';
|
||||
import { SortedSet } from './sorted_set';
|
||||
import { TypeException } from './exceptions';
|
||||
|
||||
// Define a type for the comparison function for SortedSet keys
|
||||
export type CompareFn<K> = (a: K, b: K) => number;
|
||||
|
||||
export class SortedDict<K, V> {
|
||||
private _keys: SortedSet<K>;
|
||||
private _couples: Map<K, V>;
|
||||
private _keyTypeConstructor: (new (...args: any[]) => K) | Function | undefined; // For potential runtime checks
|
||||
private _valueTypeConstructor: (new (...args: any[]) => V) | Function;
|
||||
|
||||
/**
|
||||
* Initialize a typed SortedDict.
|
||||
* @param keyTypeOrCompareFn Expected type constructor for keys (e.g., String, Number) or a custom compare function for keys.
|
||||
* @param valueTypeConstructor Expected type constructor for values.
|
||||
* @param initial Optional dictionary-like object or iterable of [K,V] pairs to initialize.
|
||||
*/
|
||||
constructor(
|
||||
keyTypeOrCompareFn: (new (...args: any[]) => K) | CompareFn<K> | Function, // Function as a broader type for constructors
|
||||
valueTypeConstructor: (new (...args: any[]) => V) | Function,
|
||||
initial?: Record<string | number, V> | Map<K, V> | Array<[K, V]> | null
|
||||
) {
|
||||
this._valueTypeConstructor = valueTypeConstructor;
|
||||
|
||||
let compareFn: CompareFn<K> | undefined;
|
||||
if (typeof keyTypeOrCompareFn === 'function' && keyTypeOrCompareFn.length === 2) {
|
||||
// It's likely a compare function if it takes two arguments
|
||||
compareFn = keyTypeOrCompareFn as CompareFn<K>;
|
||||
this._keyTypeConstructor = undefined; // No specific constructor if compareFn is given
|
||||
} else {
|
||||
this._keyTypeConstructor = keyTypeOrCompareFn as (new (...args: any[]) => K) | Function;
|
||||
// Default compareFn will be used by SortedSet if keyType is primitive
|
||||
}
|
||||
|
||||
this._keys = new SortedSet<K>([], compareFn); // Pass compareFn if provided
|
||||
this._couples = new Map<K, V>();
|
||||
|
||||
if (initial) {
|
||||
if (initial instanceof Map) {
|
||||
for (const [key, value] of initial) {
|
||||
this.put(key, value);
|
||||
}
|
||||
} else if (Array.isArray(initial)) { // Array of [K,V] pairs
|
||||
for (const [key, value] of initial) {
|
||||
this.put(key, value);
|
||||
}
|
||||
} else if (is_dictionary(initial)) { // Record<string|number, V>
|
||||
for (const key in initial) {
|
||||
if (Object.prototype.hasOwnProperty.call(initial, key)) {
|
||||
// This type assertion is tricky. If K is not string/number, this might be problematic.
|
||||
// User needs to ensure `initial` keys are compatible with K or provide a Map/Array of pairs.
|
||||
this.put(key as any as K, (initial as Record<string|number,V>)[key]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static builder<K, V>(
|
||||
keyTypeOrCompareFn: (new (...args: any[]) => K) | CompareFn<K> | Function,
|
||||
valueTypeConstructor: (new (...args: any[]) => V) | Function
|
||||
): (dictionary?: Record<string | number, V> | Map<K,V> | Array<[K,V]> | null) => SortedDict<K, V> {
|
||||
return (dictionary?: Record<string | number, V> | Map<K,V> | Array<[K,V]> | null) =>
|
||||
new SortedDict<K, V>(keyTypeOrCompareFn, valueTypeConstructor, dictionary);
|
||||
}
|
||||
|
||||
get keyType(): Function | undefined {
|
||||
return this._keyTypeConstructor;
|
||||
}
|
||||
|
||||
get valueType(): Function {
|
||||
return this._valueTypeConstructor;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const items = Array.from(this.items()).map(([k, v]) => `${String(k)}:${String(v)}`).join(', ');
|
||||
return `SortedDict{${items}}`;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._keys.size;
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): IterableIterator<K> {
|
||||
return this._keys[Symbol.iterator]();
|
||||
}
|
||||
|
||||
equals(other: SortedDict<K, V>): boolean {
|
||||
if (!(other instanceof SortedDict)) return false;
|
||||
if (this.keyType !== other.keyType || this.valueType !== other.valueType) return false;
|
||||
if (this.size !== other.size) return false;
|
||||
|
||||
for (const key of this._keys) {
|
||||
if (!other.has(key) || this.get(key) !== other.get(key)) { // Simple equality check for values
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get(key: K, defaultValue?: V): V | undefined {
|
||||
return this._couples.get(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
// Runtime type check for value (optional, TypeScript handles compile-time)
|
||||
// if (this._valueTypeConstructor && !(value instanceof (this._valueTypeConstructor as any))) {
|
||||
// throw new TypeException(this._valueTypeConstructor.name, typeof value);
|
||||
// }
|
||||
this._keys.add(key);
|
||||
this._couples.set(key, value);
|
||||
}
|
||||
|
||||
// To align with Map interface and Python's __setitem__
|
||||
put(key: K, value: V): void {
|
||||
this.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: K): V | null {
|
||||
if (this._couples.has(key)) {
|
||||
this._keys.remove(key);
|
||||
const value = this._couples.get(key)!;
|
||||
this._couples.delete(key);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// To align with Python's remove
|
||||
remove(key: K): V | null {
|
||||
return this.delete(key);
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this._couples.has(key);
|
||||
}
|
||||
|
||||
firstKey(): K | undefined {
|
||||
return this._keys.at(0);
|
||||
}
|
||||
|
||||
firstValue(): V | undefined {
|
||||
const firstK = this.firstKey();
|
||||
return firstK !== undefined ? this._couples.get(firstK) : undefined;
|
||||
}
|
||||
|
||||
lastKey(): K | undefined {
|
||||
return this._keys.at(this._keys.size - 1);
|
||||
}
|
||||
|
||||
lastValue(): V | undefined {
|
||||
const lastK = this.lastKey();
|
||||
return lastK !== undefined ? this._couples.get(lastK) : undefined;
|
||||
}
|
||||
|
||||
lastItem(): [K, V] | undefined {
|
||||
const lastK = this.lastKey();
|
||||
if (lastK !== undefined) {
|
||||
return [lastK, this._couples.get(lastK)!];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
return this._keys[Symbol.iterator]();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<V> {
|
||||
for (const key of this._keys) {
|
||||
yield this._couples.get(key)!;
|
||||
}
|
||||
}
|
||||
|
||||
*reversedValues(): IterableIterator<V> {
|
||||
for (const key of this._keys.reversed()) {
|
||||
yield this._couples.get(key)!;
|
||||
}
|
||||
}
|
||||
|
||||
*items(): IterableIterator<[K, V]> {
|
||||
for (const key of this._keys) {
|
||||
yield [key, this._couples.get(key)!];
|
||||
}
|
||||
}
|
||||
|
||||
*reversedItems(): IterableIterator<[K,V]> {
|
||||
for (const key of this._keys.reversed()) {
|
||||
yield [key, this._couples.get(key)!];
|
||||
}
|
||||
}
|
||||
|
||||
private _get_keys_interval(key_from?: K | null, key_to?: K | null): [number, number] {
|
||||
if (this.size === 0) return [0, -1];
|
||||
|
||||
let position_from: number | null;
|
||||
if (key_from === null || key_from === undefined) {
|
||||
position_from = 0;
|
||||
} else {
|
||||
position_from = this._keys.indexOf(key_from);
|
||||
if (position_from === null) { // key_from not in dict, find next
|
||||
const nextKey = this._keys.getNextValue(key_from);
|
||||
if (nextKey === null) return [0, -1]; // No key greater than key_from
|
||||
position_from = this._keys.indexOf(nextKey);
|
||||
}
|
||||
}
|
||||
|
||||
let position_to: number | null;
|
||||
if (key_to === null || key_to === undefined) {
|
||||
position_to = this.size - 1;
|
||||
} else {
|
||||
position_to = this._keys.indexOf(key_to);
|
||||
if (position_to === null) { // key_to not in dict, find previous
|
||||
const prevKey = this._keys.getPreviousValue(key_to);
|
||||
if (prevKey === null) return [0, -1]; // No key smaller than key_to
|
||||
position_to = this._keys.indexOf(prevKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (position_from === null || position_to === null || position_from > position_to) {
|
||||
return [0, -1]; // Invalid interval
|
||||
}
|
||||
return [position_from, position_to];
|
||||
}
|
||||
|
||||
|
||||
sub_keys(key_from?: K | null, key_to?: K | null): K[] {
|
||||
const [pos_from, pos_to] = this._get_keys_interval(key_from, key_to);
|
||||
if (pos_from > pos_to) return [];
|
||||
|
||||
const resultKeys: K[] = [];
|
||||
let currentIndex = 0;
|
||||
for (const key of this._keys) { // Iterate through SortedSet
|
||||
if (currentIndex >= pos_from && currentIndex <= pos_to) {
|
||||
resultKeys.push(key);
|
||||
}
|
||||
if (currentIndex > pos_to) break;
|
||||
currentIndex++;
|
||||
}
|
||||
return resultKeys;
|
||||
}
|
||||
|
||||
sub(key_from?: K | null, key_to?: K | null): V[] {
|
||||
const keys_in_interval = this.sub_keys(key_from, key_to);
|
||||
return keys_in_interval.map(key => this._couples.get(key)!);
|
||||
}
|
||||
|
||||
remove_sub(key_from?: K | null, key_to?: K | null): void {
|
||||
const keys_to_remove = this.sub_keys(key_from, key_to);
|
||||
for (const key of keys_to_remove) {
|
||||
this.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
key_from_index(index: number): K | undefined {
|
||||
return this._keys.at(index);
|
||||
}
|
||||
|
||||
get_previous_key(key: K): K | null {
|
||||
return this._keys.getPreviousValue(key);
|
||||
}
|
||||
|
||||
get_next_key(key: K): K | null {
|
||||
return this._keys.getNextValue(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._keys.clear();
|
||||
this._couples.clear();
|
||||
}
|
||||
|
||||
fill(dict?: Record<string | number, V> | Map<K,V> | Array<[K,V]> | null): void {
|
||||
if (dict) {
|
||||
if (dict instanceof Map) {
|
||||
for (const [key, value] of dict) {
|
||||
this.put(key, value);
|
||||
}
|
||||
} else if (Array.isArray(dict)) {
|
||||
for (const [key, value] of dict) {
|
||||
this.put(key, value);
|
||||
}
|
||||
} else if (is_dictionary(dict)) {
|
||||
for (const key in dict) {
|
||||
if (Object.prototype.hasOwnProperty.call(dict, key)) {
|
||||
this.put(key as any as K, (dict as Record<string|number,V>)[key]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copy(): SortedDict<K, V> {
|
||||
// The constructor of SortedSet needs a compareFn if K is not primitive.
|
||||
// This copy method should ideally pass the original compareFn.
|
||||
// For now, assuming SortedSet handles this or K is primitive.
|
||||
const newDict = new SortedDict<K,V>(
|
||||
(this._keys as any).compareFn || this._keyTypeConstructor || String, // Pass compareFn or keyType
|
||||
this._valueTypeConstructor
|
||||
);
|
||||
for (const [key, value] of this.items()) {
|
||||
newDict.put(key, value);
|
||||
}
|
||||
return newDict;
|
||||
}
|
||||
}
|
||||
207
diplomacy/utils/sorted_set.ts
Normal file
207
diplomacy/utils/sorted_set.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// diplomacy/utils/sorted_set.ts
|
||||
// Sorted set implementation.
|
||||
|
||||
import { TypeException } from './exceptions'; // Assuming TypeException is defined
|
||||
import { is_sequence } from './common'; // Assuming is_sequence is defined
|
||||
|
||||
export class SortedSet<T> {
|
||||
private list: T[] = [];
|
||||
private compareFn: (a: T, b: T) => number;
|
||||
|
||||
/**
|
||||
* Initialize a typed sorted set.
|
||||
* @param content Optional. Sequence of values to initialize sorted set with.
|
||||
* @param compareFn Optional. A custom comparison function.
|
||||
* Defaults to basic <, >, === comparison suitable for numbers and strings.
|
||||
* For objects, a custom compareFn is highly recommended.
|
||||
*/
|
||||
constructor(content?: Iterable<T>, compareFn?: (a: T, b: T) => number) {
|
||||
this.compareFn = compareFn || SortedSet.defaultCompareFn;
|
||||
|
||||
if (content) {
|
||||
if (!is_sequence(content) && typeof (content as any)[Symbol.iterator] !== 'function') {
|
||||
throw new TypeException('Iterable', typeof content);
|
||||
}
|
||||
for (const element of content) {
|
||||
this.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static defaultCompareFn<T>(a: T, b: T): number {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find insertion point for x in a to maintain sorted order.
|
||||
* If x is already present in a, the insertion point will be before (to the left of) any existing entries.
|
||||
* @param value The value to find insertion point for.
|
||||
* @returns The index where value should be inserted.
|
||||
*/
|
||||
private _bisect_left(value: T): number {
|
||||
let low = 0;
|
||||
let high = this.list.length;
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (this.compareFn(this.list[mid]!, value) < 0) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find insertion point for x in a to maintain sorted order.
|
||||
* If x is already present in a, the insertion point will be after (to the right of) any existing entries.
|
||||
* @param value The value to find insertion point for.
|
||||
* @returns The index where value should be inserted.
|
||||
*/
|
||||
private _bisect_right(value: T): number {
|
||||
let low = 0;
|
||||
let high = this.list.length;
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (this.compareFn(value, this.list[mid]!) < 0) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
static builder<T>(compareFn?: (a: T, b: T) => number): (iterable?: Iterable<T>) => SortedSet<T> {
|
||||
return (iterable?: Iterable<T>) => new SortedSet<T>(iterable, compareFn);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `SortedSet(${this.list.map(String).join(', ')})`;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
equals(other: SortedSet<T>): boolean {
|
||||
if (!(other instanceof SortedSet)) return false;
|
||||
if (this.size !== other.size) return false;
|
||||
// Assuming same compareFn implies structural equality for this check.
|
||||
// A stricter check might involve comparing compareFn references if that's critical.
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
if (this.compareFn(this.list[i]!, other.list[i]!) !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
at(index: number): T | undefined {
|
||||
return this.list[index];
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.list[Symbol.iterator]();
|
||||
}
|
||||
|
||||
*reversed(): IterableIterator<T> {
|
||||
for (let i = this.list.length - 1; i >= 0; i--) {
|
||||
yield this.list[i]!;
|
||||
}
|
||||
}
|
||||
|
||||
has(element: T): boolean {
|
||||
if (this.list.length === 0) return false;
|
||||
const position = this._bisect_left(element);
|
||||
return position < this.list.length && this.compareFn(this.list[position]!, element) === 0;
|
||||
}
|
||||
|
||||
add(element: T): number | undefined {
|
||||
const position = this._bisect_left(element);
|
||||
if (position === this.list.length || this.compareFn(this.list[position]!, element) !== 0) {
|
||||
this.list.splice(position, 0, element);
|
||||
return position;
|
||||
}
|
||||
return undefined; // Element already exists
|
||||
}
|
||||
|
||||
getNextValue(element: T): T | null {
|
||||
if (this.list.length === 0) return null;
|
||||
const position = this._bisect_right(element);
|
||||
if (position < this.list.length) {
|
||||
// If element itself is present, bisect_right gives the index after it.
|
||||
// If element is not present, bisect_right gives the index where it would be inserted (i.e., the next greater element).
|
||||
return this.list[position]!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousValue(element: T): T | null {
|
||||
if (this.list.length === 0) return null;
|
||||
const position = this._bisect_left(element);
|
||||
if (position > 0) {
|
||||
return this.list[position - 1]!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pop(index: number): T | undefined {
|
||||
if (index < 0 || index >= this.list.length) return undefined;
|
||||
return this.list.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
remove(element: T): T | null {
|
||||
const position = this._bisect_left(element);
|
||||
if (position < this.list.length && this.compareFn(this.list[position]!, element) === 0) {
|
||||
return this.list.splice(position, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
indexOf(element: T): number | null {
|
||||
const position = this._bisect_left(element);
|
||||
if (position < this.list.length && this.compareFn(this.list[position]!, element) === 0) {
|
||||
return position;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
// --- Set operations ---
|
||||
// These will return new SortedSet instances. They assume 'other' is also a SortedSet.
|
||||
// For simplicity, using the default or same compareFn.
|
||||
|
||||
union(other: SortedSet<T>): SortedSet<T> {
|
||||
const newSet = new SortedSet<T>([...this.list], this.compareFn);
|
||||
for (const elem of other) {
|
||||
newSet.add(elem);
|
||||
}
|
||||
return newSet;
|
||||
}
|
||||
|
||||
intersection(other: SortedSet<T>): SortedSet<T> {
|
||||
const newSet = new SortedSet<T>([], this.compareFn);
|
||||
for (const elem of this.list) {
|
||||
if (other.has(elem)) {
|
||||
newSet.add(elem);
|
||||
}
|
||||
}
|
||||
return newSet;
|
||||
}
|
||||
|
||||
difference(other: SortedSet<T>): SortedSet<T> {
|
||||
const newSet = new SortedSet<T>([], this.compareFn);
|
||||
for (const elem of this.list) {
|
||||
if (!other.has(elem)) {
|
||||
newSet.add(elem);
|
||||
}
|
||||
}
|
||||
return newSet;
|
||||
}
|
||||
}
|
||||
209
diplomacy/utils/splitter.ts
Normal file
209
diplomacy/utils/splitter.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// diplomacy/utils/splitter.ts
|
||||
// Contains utils to retrieve splitted subjects fields
|
||||
|
||||
export abstract class AbstractStringSplitter {
|
||||
protected _input_str: string;
|
||||
protected _parts: (string | null)[];
|
||||
protected _last_index: number = 0;
|
||||
private _length: number;
|
||||
|
||||
constructor(input: string | string[], length: number) {
|
||||
this._input_str = Array.isArray(input) ? input.join(' ') : input;
|
||||
this._parts = new Array(length).fill(null);
|
||||
this._length = length; // Store length for internal use if needed, though _parts.length serves this.
|
||||
this._split();
|
||||
}
|
||||
|
||||
get input_str(): string {
|
||||
return this._input_str;
|
||||
}
|
||||
|
||||
get parts(): (string | null)[] {
|
||||
return this._parts.slice(0, this._last_index);
|
||||
}
|
||||
|
||||
join(): string {
|
||||
return this.parts.filter(p => p !== null).join(' ');
|
||||
}
|
||||
|
||||
protected abstract _split(): void;
|
||||
|
||||
public getLength(): number { // Equivalent to Python's __len__
|
||||
return this._last_index;
|
||||
}
|
||||
}
|
||||
|
||||
export class OrderSplitter extends AbstractStringSplitter {
|
||||
private _unit_index: number | null = null;
|
||||
private _order_type_index: number | null = null;
|
||||
private _supported_unit_index: number | null = null;
|
||||
private _support_order_type_index: number | null = null;
|
||||
private _destination_index: number | null = null;
|
||||
private _via_flag_index: number | null = null;
|
||||
|
||||
constructor(input: string | string[]) {
|
||||
super(input, 6); // Max 6 parts for an order based on Python example
|
||||
}
|
||||
|
||||
get unit(): string | null {
|
||||
return this._unit_index !== null ? this._parts[this._unit_index] : null;
|
||||
}
|
||||
set unit(value: string | null) {
|
||||
if (this._unit_index === null) {
|
||||
this._unit_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._unit_index] = value;
|
||||
}
|
||||
|
||||
get order_type(): string | null {
|
||||
return this._order_type_index !== null ? this._parts[this._order_type_index] : null;
|
||||
}
|
||||
set order_type(value: string | null) {
|
||||
if (this._order_type_index === null) {
|
||||
this._order_type_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._order_type_index] = value;
|
||||
}
|
||||
|
||||
get supported_unit(): string | null {
|
||||
return this._supported_unit_index !== null ? this._parts[this._supported_unit_index] : null;
|
||||
}
|
||||
set supported_unit(value: string | null) {
|
||||
if (this._supported_unit_index === null) {
|
||||
this._supported_unit_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._supported_unit_index] = value;
|
||||
}
|
||||
|
||||
get support_order_type(): string | null {
|
||||
return this._support_order_type_index !== null ? this._parts[this._support_order_type_index] : null;
|
||||
}
|
||||
set support_order_type(value: string | null) {
|
||||
if (this._support_order_type_index === null) {
|
||||
this._support_order_type_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._support_order_type_index] = value;
|
||||
}
|
||||
|
||||
get destination(): string | null {
|
||||
return this._destination_index !== null ? this._parts[this._destination_index] : null;
|
||||
}
|
||||
set destination(value: string | null) {
|
||||
if (this._destination_index === null) {
|
||||
this._destination_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._destination_index] = value;
|
||||
}
|
||||
|
||||
get via_flag(): string | null {
|
||||
return this._via_flag_index !== null ? this._parts[this._via_flag_index] : null;
|
||||
}
|
||||
set via_flag(value: string | null) {
|
||||
if (this._via_flag_index === null) {
|
||||
this._via_flag_index = this._last_index++;
|
||||
}
|
||||
this._parts[this._via_flag_index] = value;
|
||||
}
|
||||
|
||||
protected _split(): void {
|
||||
const words = typeof this._input_str === 'string' ? this._input_str.trim().split(/\s+/) : [...this._input_str];
|
||||
|
||||
if (words.length === 1) {
|
||||
this.order_type = words.pop()!;
|
||||
return;
|
||||
}
|
||||
if (words.length < 2 && words.length !==1) return; // Not enough parts for a unit
|
||||
|
||||
this.unit = `${words.shift()} ${words.shift()}`;
|
||||
if (words.length === 0) { // Implicit hold, e.g. "A PAR"
|
||||
this.order_type = "H"; // Default to Hold
|
||||
return;
|
||||
}
|
||||
|
||||
this.order_type = words.shift()!;
|
||||
|
||||
if (this.order_type === '-' || this.order_type === 'R') { // Move or Retreat
|
||||
if (words.length > 0) this.destination = words.shift()!; // Python used pop, which takes from end. Here shift from start.
|
||||
// Order syntax usually is Unit Loc Op Dest ...
|
||||
} else if (this.order_type === 'S' || this.order_type === 'C') { // Support or Convoy
|
||||
if (words.length >= 2) {
|
||||
this.supported_unit = `${words.shift()} ${words.shift()}`;
|
||||
}
|
||||
if (words.length > 0) {
|
||||
this.support_order_type = words.shift()!; // This is actually the '-' for move or target for hold
|
||||
if (this.support_order_type === '-') { // Support to move or Convoy
|
||||
if (words.length > 0) this.destination = words.shift()!;
|
||||
} else { // Support to Hold, support_order_type is actually the destination
|
||||
this.destination = this.support_order_type;
|
||||
this.support_order_type = null; // No separate support_order_type for S H
|
||||
}
|
||||
}
|
||||
}
|
||||
// Build 'B' and Disband 'D' orders are simpler: Unit Loc Op
|
||||
// e.g. A PAR B. Unit: A PAR, Op: B. No other parts needed typically by this splitter.
|
||||
// The Python version's examples show this structure.
|
||||
|
||||
if (words.length > 0 && words[words.length -1 ] === 'VIA') { // Check last remaining element
|
||||
this.via_flag = words.pop()!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PhaseSplitter extends AbstractStringSplitter {
|
||||
private _season_index: number | null = null;
|
||||
private _year_index: number | null = null;
|
||||
private _phase_type_index: number | null = null;
|
||||
|
||||
constructor(input: string) { // Phase string like S1901M
|
||||
super(input, 3);
|
||||
}
|
||||
|
||||
get season(): string | null {
|
||||
return this._season_index !== null ? this._parts[this._season_index] : null;
|
||||
}
|
||||
set season(value: string | null) {
|
||||
if (this._season_index === null) this._season_index = this._last_index++;
|
||||
this._parts[this._season_index] = value;
|
||||
}
|
||||
|
||||
get year(): number | null {
|
||||
const val = this._year_index !== null ? this._parts[this._year_index] : null;
|
||||
return val !== null ? parseInt(val, 10) : null;
|
||||
}
|
||||
set year(value: number | null) {
|
||||
if (this._year_index === null) this._year_index = this._last_index++;
|
||||
this._parts[this._year_index] = value !== null ? String(value) : null;
|
||||
}
|
||||
|
||||
get phase_type(): string | null {
|
||||
return this._phase_type_index !== null ? this._parts[this._phase_type_index] : null;
|
||||
}
|
||||
set phase_type(value: string | null) {
|
||||
if (this._phase_type_index === null) this._phase_type_index = this._last_index++;
|
||||
this._parts[this._phase_type_index] = value;
|
||||
}
|
||||
|
||||
protected _split(): void {
|
||||
if (this._input_str && this._input_str.length >= 4) { // e.g. S1901M (min length)
|
||||
this.season = this._input_str[0];
|
||||
// Year can be 2 digits (01) or 4 digits (1901)
|
||||
// The Python code `int(self._input_str[1:-1])` implies year is everything between first and last char.
|
||||
const yearMatch = this._input_str.substring(1, this._input_str.length -1).match(/\d+/);
|
||||
if (yearMatch) {
|
||||
this.year = parseInt(yearMatch[0], 10);
|
||||
} else {
|
||||
// Handle error or default if year is not found, e.g. for "FORMING"
|
||||
this.year = null;
|
||||
}
|
||||
this.phase_type = this._input_str[this._input_str.length - 1];
|
||||
} else {
|
||||
// Handle cases like "FORMING", "COMPLETED" or invalid short strings
|
||||
// For "FORMING" or "COMPLETED", specific setters might not be called.
|
||||
// This splitter is primarily for seasonal phases.
|
||||
// To make it robust for "FORMING", etc., we could set one part:
|
||||
if (this._input_str === "FORMING" || this._input_str === "COMPLETED") {
|
||||
this.phase_type = this._input_str; // Or season, depending on desired output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
diplomacy/utils/time.ts
Normal file
165
diplomacy/utils/time.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// ==============================================================================
|
||||
// Copyright (C) 2019 - Philip Paquette
|
||||
//
|
||||
// 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/>.
|
||||
// ==============================================================================
|
||||
|
||||
/**
|
||||
* Time functions
|
||||
* - Contains generic time functions (e.g. to calculate deadlines)
|
||||
*/
|
||||
import { utcToZonedTime, zonedToUtc } from 'date-fns-tz';
|
||||
import { getUnixTime, fromUnixTime, getHours, getMinutes, getSeconds, differenceInSeconds } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Converts a time in format 00W00D00H00M00S in number of seconds
|
||||
* @param offsetStr The string to convert (e.g. '20D')
|
||||
* @returns Its equivalent in seconds = 1728000
|
||||
*/
|
||||
export function strToSeconds(offsetStr: string): number {
|
||||
const mult: { [key: string]: number } = { 'W': 7 * 24 * 60 * 60, 'D': 24 * 60 * 60, 'H': 60 * 60, 'M': 60, 'S': 1, ' ': 1 };
|
||||
let buffer = 0;
|
||||
let currentSum = 0;
|
||||
const str = String(offsetStr);
|
||||
|
||||
for (const char of str) {
|
||||
if (char >= '0' && char <= '9') {
|
||||
buffer = buffer * 10 + parseInt(char, 10);
|
||||
} else if (char.toUpperCase() in mult) {
|
||||
currentSum += buffer * mult[char.toUpperCase()];
|
||||
buffer = 0;
|
||||
} else {
|
||||
buffer = 0;
|
||||
}
|
||||
}
|
||||
currentSum += buffer;
|
||||
return currentSum;
|
||||
}
|
||||
|
||||
function getMidnightTsReference(): number {
|
||||
const serverNow = new Date();
|
||||
const midnightUtcOfServerCurrentDay = new Date(Date.UTC(
|
||||
serverNow.getUTCFullYear(),
|
||||
serverNow.getUTCMonth(),
|
||||
serverNow.getUTCDate(),
|
||||
0, 0, 0, 0
|
||||
));
|
||||
return getUnixTime(midnightUtcOfServerCurrentDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates time at a specific interval (e.g. 20M) (i.e. Rounds to the next :20, :40, :60)
|
||||
*
|
||||
* Note: The reference "day" for truncation (midnight_ts) is based on the server's current UTC day,
|
||||
* matching the behavior of Python's `datetime.date.today()` in the original script.
|
||||
*
|
||||
* @param timestamp The unix epoch to truncate (e.g. 1498746120)
|
||||
* @param truncInterval The truncation interval (e.g. 60*60 or '1H')
|
||||
* @param timeZone The time to use for conversion (defaults to GMT otherwise, which is UTC)
|
||||
* @returns A timestamp truncated to the nearest (future) interval
|
||||
*/
|
||||
export function truncTime(timestamp: number, truncInterval: string | number, timeZone: string = 'GMT'): number {
|
||||
const intervalSeconds = typeof truncInterval === 'string' ? strToSeconds(truncInterval) : truncInterval;
|
||||
if (intervalSeconds === 0) return timestamp;
|
||||
|
||||
const originalDateUtc = fromUnixTime(timestamp);
|
||||
const zonedDate = timeZone === 'GMT' ? originalDateUtc : utcToZonedTime(originalDateUtc, timeZone);
|
||||
|
||||
const zonedDateAsUtc = zonedToUtc(zonedDate, timeZone);
|
||||
const tzOffsetSeconds = differenceInSeconds(zonedDate, zonedDateAsUtc);
|
||||
|
||||
const midnightTsRef = getMidnightTsReference();
|
||||
const rawMidnightOffset = timestamp - midnightTsRef;
|
||||
const midnightOffset = ((rawMidnightOffset % (24 * 3600)) + (24 * 3600)) % (24 * 3600); // JS modulo fix for negatives
|
||||
|
||||
const truncOffset = Math.ceil((midnightOffset + tzOffsetSeconds) / intervalSeconds) * intervalSeconds;
|
||||
const truncTs = timestamp - midnightOffset + truncOffset - tzOffsetSeconds;
|
||||
|
||||
return Math.floor(truncTs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next timestamp at a specific 'hh:mm'
|
||||
*
|
||||
* Note: The reference "day" (midnight_ts) is based on the server's current UTC day,
|
||||
* matching the behavior of Python's `datetime.date.today()` in the original script.
|
||||
*
|
||||
* @param timestamp The unix timestamp to convert
|
||||
* @param timeAt The next 'hh:mm' to have the time rounded to (e.g., "14:30"), or a string compatible with strToSeconds, or 0 to skip
|
||||
* @param timeZone The time to use for conversion (defaults to GMT otherwise, which is UTC)
|
||||
* @returns A timestamp at the nearest (future) hh:mm
|
||||
*/
|
||||
export function nextTimeAt(timestamp: number, timeAt: string | number, timeZone: string = 'GMT'): number {
|
||||
if (!timeAt && timeAt !== 0) { // Allow 0 as a valid input for timeAt if it means 0 seconds past midnight
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
let targetSecondsPastMidnightRef: number;
|
||||
if (typeof timeAt === 'string' && timeAt.includes(':')) {
|
||||
const parts = timeAt.split(':');
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
targetSecondsPastMidnightRef = hours * 3600 + minutes * 60;
|
||||
} else if (typeof timeAt === 'string') {
|
||||
targetSecondsPastMidnightRef = strToSeconds(timeAt);
|
||||
} else { // number
|
||||
targetSecondsPastMidnightRef = timeAt;
|
||||
}
|
||||
|
||||
// Normalize targetSecondsPastMidnightRef to be within a 0 to 24*3600 range
|
||||
// This mirrors the Python logic's effective behavior with modulo in `at_offset` calculation.
|
||||
if (targetSecondsPastMidnightRef < 0 || targetSecondsPastMidnightRef >= 24 * 3600) {
|
||||
targetSecondsPastMidnightRef = ((targetSecondsPastMidnightRef % (24 * 3600)) + (24 * 3600)) % (24 * 3600);
|
||||
}
|
||||
|
||||
const originalDateUtc = fromUnixTime(timestamp);
|
||||
const zonedDate = timeZone === 'GMT' ? originalDateUtc : utcToZonedTime(originalDateUtc, timeZone);
|
||||
|
||||
const zonedDateAsUtc = zonedToUtc(zonedDate, timeZone);
|
||||
const tzOffsetSeconds = differenceInSeconds(zonedDate, zonedDateAsUtc);
|
||||
|
||||
const midnightTsRef = getMidnightTsReference();
|
||||
const rawMidnightOffset = timestamp - midnightTsRef;
|
||||
const midnightOffset = ((rawMidnightOffset % (24 * 3600)) + (24 * 3600)) % (24 * 3600); // JS modulo fix
|
||||
|
||||
// Python: at_offset = (-midnight_offset + interval - tz_offset) % (24 * 3600)
|
||||
// interval here is targetSecondsPastMidnightRef
|
||||
let atOffset = (-midnightOffset + targetSecondsPastMidnightRef - tzOffsetSeconds);
|
||||
atOffset = ((atOffset % (24*3600)) + (24*3600)) % (24*3600);
|
||||
|
||||
// at_ts = timestamp + at_offset (Python)
|
||||
// The at_offset is the duration to add to the current timestamp's position *within the server's current day structure*
|
||||
// to reach the target time.
|
||||
// The Python code is: timestamp + ((- ( (timestamp - midnight_UTC_server_day_start) % 86400 ) + target_time_of_day_seconds - tz_offset_seconds) % 86400 )
|
||||
// This seems to calculate an offset to add to the original timestamp to hit the target time of day.
|
||||
// The crucial part is that `at_offset` is calculated based on `midnight_offset` which is `timestamp` relative to `midnightTsRef` (server's current day).
|
||||
// So, `at_offset` is the duration to add to `timestamp` to get it to the desired `time_at` in the context of the `time_zone`,
|
||||
// ensuring it's the *next* occurrence.
|
||||
|
||||
const atTs = timestamp + atOffset;
|
||||
|
||||
// If atTs is still behind timestamp (e.g. target time was 10:00, current is 11:00, at_offset might be -1h)
|
||||
// then we need to add a day. The modulo arithmetic in Python's at_offset handles this implicitly.
|
||||
// If (target_seconds_in_day - current_seconds_in_day_for_timestamp) is negative, it means target is "earlier" today.
|
||||
// Python's % (24*3600) makes it positive, effectively pushing it to the "next day" if needed.
|
||||
// My atOffset calculation with ((X % M) + M) % M ensures it's a positive offset to add.
|
||||
// Let's test:
|
||||
// timestamp = 11:00. target_time_of_day = 10:00. tz_offset=0. midnight_offset for 11:00 is 11*3600.
|
||||
// at_offset = (-(11*3600) + (10*3600) - 0) = -3600.
|
||||
// at_offset_mod = ((-3600 % 86400) + 86400) % 86400 = (-3600 + 86400) % 86400 = 82800. (23 hours)
|
||||
// So, 11:00 + 23 hours = 10:00 next day. This is correct.
|
||||
|
||||
return Math.floor(atTs);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue