<commit_before>/**
 * Room Battle
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * This file wraps the simulator in an implementation of the RoomGame
 * interface. It also abstracts away the multi-process nature of the
 * simulator.
 *
 * For the actual battle simulation, see sim/
 *
 * @license MIT
 */

import {FS, Repl, ProcessManager} from '../lib';
import {execSync} from "child_process";
import {BattleStream} from "../sim/battle-stream";
import * as RoomGames from "./room-game";
import type {Tournament} from './tournaments/index';

type ChannelIndex = 0 | 1 | 2 | 3 | 4;
type PlayerIndex = 1 | 2 | 3 | 4;
export type ChallengeType = 'rated' | 'unrated' | 'challenge' | 'tour';

interface BattleRequestTracker {
	rqid: number;
	request: string;
	/**
	 * - true = user has decided,
	 * - false = user has yet to decide,
	 * - 'cantUndo' = waiting on other user (U-turn, faint-switch) or uncancellable (trapping ability)
	 */
	isWait: 'cantUndo' | true | false;
	choice: string;
}

/** 5 seconds */
const TICK_TIME = 5;
const SECONDS = 1000;

// Timer constants: In seconds, should be multiple of TICK_TIME
const STARTING_TIME = 150;
const MAX_TURN_TIME = 150;
const STARTING_TIME_CHALLENGE = 300;
const STARTING_GRACE_TIME = 60;
const MAX_TURN_TIME_CHALLENGE = 300;

const DISCONNECTION_TIME = 60;
const DISCONNECTION_BANK_TIME = 300;

// time after a player disabling the timer before they can re-enable it
const TIMER_COOLDOWN = 20 * SECONDS;

export class RoomBattlePlayer extends RoomGames.RoomGamePlayer {
	readonly slot: SideID;
	readonly channelIndex: ChannelIndex;
	request: BattleRequestTracker;
	wantsTie: boolean;
	active: boolean;
	eliminated: boolean;
	/**
	 * Total timer.
	 *
	 * Starts at 210 per player in a ladder battle. Goes down by 5
	 * every tick. Goes up by 10 every turn (with some complications -
	 * see `nextRequest`), capped at starting time. The player loses if
	 * this reaches 0.
	 *
	 * The equivalent of "Your Time" in VGC.
	 *
	 */
	secondsLeft: number;
	/**
	 * Turn timer.
	 *
	 * Set equal to the player's overall timer, but capped at 150
	 * seconds in a ladder battle. Goes down by 5 every tick.
	 * Tracked separately from the overall timer, and the player also
	 * loses if this reaches 0.
	 */
	turnSecondsLeft: number;
	/**
	 * Disconnect timer.
	 * Starts at 60 seconds. While the player is disconnected, this
	 * will go down by 5 every tick. Tracked separately from the
	 * overall timer, and the player also loses if this reaches 0.
	 *
	 * Mostly exists so impatient players don't have to wait the full
	 * 150 seconds against a disconnected opponent.
 	*/
	dcSecondsLeft: number;
	/**
	 * Used to track a user's last known connection status, and display
	 * the proper message when it changes.
	 */
	connected: boolean;
	invite: ID;
	constructor(user: User | string | null, game: RoomBattle, num: PlayerIndex) {
		super(user, game, num);
		if (typeof user === 'string') user = null;

		this.slot = `p${num}` as SideID;
		this.channelIndex = (game.gameType === 'multi' && num > 2 ? num - 2 : num) as ChannelIndex;

		this.request = {rqid: 0, request: '', isWait: 'cantUndo', choice: ''};
		this.wantsTie = false;
		this.active = true;
		this.eliminated = false;

		this.secondsLeft = 1;
		this.turnSecondsLeft = 1;
		this.dcSecondsLeft = 1;

		this.connected = true;
		this.invite = '';

		if (user) {
			user.games.add(this.game.roomid);
			user.updateSearch();
			for (const connection of user.connections) {
				if (connection.inRooms.has(game.roomid)) {
					Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
				}
			}
		}
	}
	getUser() {
		return (this.id && Users.get(this.id)) || null;
	}
	unlinkUser() {
		const user = this.getUser();
		if (user) {
			for (const connection of user.connections) {
				Sockets.channelMove(connection.worker, this.game.roomid, 0, connection.socketid);
			}
			user.games.delete(this.game.roomid);
			user.updateSearch();
		}
		this.id = '';
		this.connected = false;
		this.active = false;
	}
	updateChannel(user: User | Connection) {
		if (user instanceof Users.Connection) {
			// "user" is actually a connection
			Sockets.channelMove(user.worker, this.game.roomid, this.channelIndex, user.socketid);
			return;
		}
		for (const connection of user.connections) {
			Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
		}
	}

	toString() {
		return this.id;
	}
	send(data: string) {
		const user = this.getUser();
		if (user) user.send(data);
	}
	sendRoom(data: string) {
		const user = this.getUser();
		if (user) user.sendTo(this.game.roomid, data);
	}
}

export class RoomBattleTimer {
	readonly battle: RoomBattle;
	readonly timerRequesters: Set<ID>;
	timer: NodeJS.Timer | null;
	isFirstTurn: boolean;
	/**
	 * Last tick, as milliseconds since UNIX epoch.
	 * Represents the last time a tick happened.
	 */
	lastTick: number;
	/** Debug mode; true to output detailed timer info every tick */
	debug: boolean;
	lastDisabledTime: number;
	lastDisabledByUser: null | ID;
	settings: GameTimerSettings;
	constructor(battle: RoomBattle) {
		this.battle = battle;

		this.timer = null;
		this.timerRequesters = new Set();
		this.isFirstTurn = true;

		this.lastTick = 0;

		this.debug = false;

		this.lastDisabledTime = 0;
		this.lastDisabledByUser = null;

		const hasLongTurns = Dex.getFormat(battle.format, true).gameType !== 'singles';
		const isChallenge = (!battle.rated && !battle.room.tour);
		const timerEntry = Dex.getRuleTable(Dex.getFormat(battle.format, true)).timer;
		const timerSettings = timerEntry?.[0];

		// so that Object.assign doesn't overwrite anything with `undefined`
		for (const k in timerSettings) {
			// @ts-ignore<commit_msg>Shorten unrated matchmaking timer<commit_after>/**
 * Room Battle
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * This file wraps the simulator in an implementation of the RoomGame
 * interface. It also abstracts away the multi-process nature of the
 * simulator.
 *
 * For the actual battle simulation, see sim/
 *
 * @license MIT
 */

import {FS, Repl, ProcessManager} from '../lib';
import {execSync} from "child_process";
import {BattleStream} from "../sim/battle-stream";
import * as RoomGames from "./room-game";
import type {Tournament} from './tournaments/index';

type ChannelIndex = 0 | 1 | 2 | 3 | 4;
type PlayerIndex = 1 | 2 | 3 | 4;
export type ChallengeType = 'rated' | 'unrated' | 'challenge' | 'tour';

interface BattleRequestTracker {
	rqid: number;
	request: string;
	/**
	 * - true = user has decided,
	 * - false = user has yet to decide,
	 * - 'cantUndo' = waiting on other user (U-turn, faint-switch) or uncancellable (trapping ability)
	 */
	isWait: 'cantUndo' | true | false;
	choice: string;
}

/** 5 seconds */
const TICK_TIME = 5;
const SECONDS = 1000;

// Timer constants: In seconds, should be multiple of TICK_TIME
const STARTING_TIME = 150;
const MAX_TURN_TIME = 150;
const STARTING_TIME_CHALLENGE = 300;
const STARTING_GRACE_TIME = 60;
const MAX_TURN_TIME_CHALLENGE = 300;

const DISCONNECTION_TIME = 60;
const DISCONNECTION_BANK_TIME = 300;

// time after a player disabling the timer before they can re-enable it
const TIMER_COOLDOWN = 20 * SECONDS;

export class RoomBattlePlayer extends RoomGames.RoomGamePlayer {
	readonly slot: SideID;
	readonly channelIndex: ChannelIndex;
	request: BattleRequestTracker;
	wantsTie: boolean;
	active: boolean;
	eliminated: boolean;
	/**
	 * Total timer.
	 *
	 * Starts at 210 per player in a ladder battle. Goes down by 5
	 * every tick. Goes up by 10 every turn (with some complications -
	 * see `nextRequest`), capped at starting time. The player loses if
	 * this reaches 0.
	 *
	 * The equivalent of "Your Time" in VGC.
	 *
	 */
	secondsLeft: number;
	/**
	 * Turn timer.
	 *
	 * Set equal to the player's overall timer, but capped at 150
	 * seconds in a ladder battle. Goes down by 5 every tick.
	 * Tracked separately from the overall timer, and the player also
	 * loses if this reaches 0.
	 */
	turnSecondsLeft: number;
	/**
	 * Disconnect timer.
	 * Starts at 60 seconds. While the player is disconnected, this
	 * will go down by 5 every tick. Tracked separately from the
	 * overall timer, and the player also loses if this reaches 0.
	 *
	 * Mostly exists so impatient players don't have to wait the full
	 * 150 seconds against a disconnected opponent.
 	*/
	dcSecondsLeft: number;
	/**
	 * Used to track a user's last known connection status, and display
	 * the proper message when it changes.
	 */
	connected: boolean;
	invite: ID;
	constructor(user: User | string | null, game: RoomBattle, num: PlayerIndex) {
		super(user, game, num);
		if (typeof user === 'string') user = null;

		this.slot = `p${num}` as SideID;
		this.channelIndex = (game.gameType === 'multi' && num > 2 ? num - 2 : num) as ChannelIndex;

		this.request = {rqid: 0, request: '', isWait: 'cantUndo', choice: ''};
		this.wantsTie = false;
		this.active = true;
		this.eliminated = false;

		this.secondsLeft = 1;
		this.turnSecondsLeft = 1;
		this.dcSecondsLeft = 1;

		this.connected = true;
		this.invite = '';

		if (user) {
			user.games.add(this.game.roomid);
			user.updateSearch();
			for (const connection of user.connections) {
				if (connection.inRooms.has(game.roomid)) {
					Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
				}
			}
		}
	}
	getUser() {
		return (this.id && Users.get(this.id)) || null;
	}
	unlinkUser() {
		const user = this.getUser();
		if (user) {
			for (const connection of user.connections) {
				Sockets.channelMove(connection.worker, this.game.roomid, 0, connection.socketid);
			}
			user.games.delete(this.game.roomid);
			user.updateSearch();
		}
		this.id = '';
		this.connected = false;
		this.active = false;
	}
	updateChannel(user: User | Connection) {
		if (user instanceof Users.Connection) {
			// "user" is actually a connection
			Sockets.channelMove(user.worker, this.game.roomid, this.channelIndex, user.socketid);
			return;
		}
		for (const connection of user.connections) {
			Sockets.channelMove(connection.worker, this.game.roomid, this.channelIndex, connection.socketid);
		}
	}

	toString() {
		return this.id;
	}
	send(data: string) {
		const user = this.getUser();
		if (user) user.send(data);
	}
	sendRoom(data: string) {
		const user = this.getUser();
		if (user) user.sendTo(this.game.roomid, data);
	}
}

export class RoomBattleTimer {
	readonly battle: RoomBattle;
	readonly timerRequesters: Set<ID>;
	timer: NodeJS.Timer | null;
	isFirstTurn: boolean;
	/**
	 * Last tick, as milliseconds since UNIX epoch.
	 * Represents the last time a tick happened.
	 */
	lastTick: number;
	/** Debug mode; true to output detailed timer info every tick */
	debug: boolean;
	lastDisabledTime: number;
	lastDisabledByUser: null | ID;
	settings: GameTimerSettings;
	constructor(battle: RoomBattle) {
		this.battle = battle;

		this.timer = null;
		this.timerRequesters = new Set();
		this.isFirstTurn = true;

		this.lastTick = 0;

		this.debug = false;

		this.lastDisabledTime = 0;
		this.lastDisabledByUser = null;

		const hasLongTurns = Dex.getFormat(battle.format, true).gameType !== 'singles';
		const isChallenge = (battle.challengeType === 'challenge');
		const timerEntry = Dex.getRuleTable(Dex.getFormat(battle.format, true)).timer;
		const timerSettings = timerEntry?.[0];

		// so that Object.assign doesn't overwrite anything with `undefined`
		for (const k in timerSettings) {
			// @ts-ignore
{'fraction_non_alphanumeric': 0.11763176144244106, 'fraction_numerical': 0.011269070735090153, 'mean_word_length': 3.689837398373984, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '315468', 'n_tokens_mistral': 4120, 'n_tokens_neox': 3656, 'n_words': 1180}