initial commit
This commit is contained in:
119
types/BattedBall.ts
Normal file
119
types/BattedBall.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
export type BattedBall = {
|
||||
readonly exitVelo: number; // in mph
|
||||
readonly launchAngle: number; // vertical angle in degrees
|
||||
readonly attackAngle: number; // spray angle: pull = -89º, oppo = 89º
|
||||
readonly spinRate: number; // in RPM // calculated using the bat swing from the batter in the at bat.
|
||||
};
|
||||
|
||||
export type BallFlightResult = {
|
||||
readonly hangTime: number;
|
||||
readonly distance: number;
|
||||
readonly landingHorizontalVelo: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates baseball flight trajectory including:
|
||||
* 1. Aerodynamic Drag (Sea Level)
|
||||
* 2. Magnus Effect (Lift from spin)
|
||||
* 3. Ground Friction (Impact physics based on spin type)
|
||||
*/
|
||||
export function calculateFullFlightPath(
|
||||
ball: BattedBall,
|
||||
surfaceFriction: number = 0.4 // 0.3 for turf, 0.4 for grass, 0.45 for dirt
|
||||
): BallFlightResult {
|
||||
// Physical Constants (Imperial: slugs, feet, seconds)
|
||||
const G = 32.174;
|
||||
const RHO = 0.0023769; // Air density at sea level
|
||||
const CD = 0.35; // Drag coefficient
|
||||
const AREA = 0.0458; // Cross-sectional area (sq ft)
|
||||
const MASS = 0.00994; // Mass (slugs)
|
||||
const RADIUS = 0.121; // Radius (ft)
|
||||
const E = 0.5; // Coefficient of restitution (bounciness)
|
||||
|
||||
const MPH_TO_FPS = 1.46667;
|
||||
const FPS_TO_MPH = 0.681818;
|
||||
const DT = 0.005; // Smaller time step for better accuracy
|
||||
|
||||
// Initial State
|
||||
let t = 0;
|
||||
let x = 0;
|
||||
let y = 3; // Standard contact height (ft)
|
||||
let z = 0;
|
||||
|
||||
const v0 = ball.exitVelo * MPH_TO_FPS;
|
||||
const theta = (ball.launchAngle * Math.PI) / 180;
|
||||
const phi = (ball.attackAngle * Math.PI) / 180;
|
||||
const omega = (ball.spinRate * 2 * Math.PI) / 60; // RPM to Rad/s
|
||||
|
||||
// Velocity Components
|
||||
let vy = v0 * Math.sin(theta);
|
||||
let v_horiz_initial = v0 * Math.cos(theta);
|
||||
let vx = v_horiz_initial * Math.cos(phi);
|
||||
let vz = v_horiz_initial * Math.sin(phi);
|
||||
|
||||
// 1. Flight Simulation (Numerical integration using Euler's Method)
|
||||
while (y > 0) {
|
||||
const v = Math.sqrt(vx * vx + vy * vy + vz * vz);
|
||||
if (v < 0.1) break;
|
||||
|
||||
// Aerodynamics: Drag and Magnus (Lift)
|
||||
const spinFactor = (RADIUS * omega) / v;
|
||||
const CL = 1 / (2 + (1 / spinFactor));
|
||||
|
||||
const dragAccelFactor = (0.5 * RHO * v * CD * AREA) / MASS;
|
||||
const liftAccelFactor = (0.5 * RHO * v * CL * AREA) / MASS;
|
||||
|
||||
// Acceleration Vectors
|
||||
// Lift acts perpendicular to velocity; for backspin, it provides upward lift
|
||||
const ax = -dragAccelFactor * vx - liftAccelFactor * (vy / v) * Math.cos(phi);
|
||||
const ay = -G - (dragAccelFactor * vy) + liftAccelFactor * (vx / v);
|
||||
const az = -dragAccelFactor * vz;
|
||||
|
||||
// Update State
|
||||
vx += ax * DT;
|
||||
vy += ay * DT;
|
||||
vz += az * DT;
|
||||
x += vx * DT;
|
||||
y += vy * DT;
|
||||
z += vz * DT;
|
||||
t += DT;
|
||||
|
||||
if (t > 15) break; // Safety timeout
|
||||
}
|
||||
|
||||
// 2. Landing Physics (Impact Friction)
|
||||
// Horizontal velocity at the moment of impact
|
||||
const vx_impact = vx;
|
||||
const vz_impact = vz;
|
||||
const vy_impact = Math.abs(vy);
|
||||
const v_horiz_impact = Math.sqrt(vx_impact * vx_impact + vz_impact * vz_impact);
|
||||
|
||||
/**
|
||||
* Slip Velocity Calculation:
|
||||
* v_slip = v_horizontal + (omega * radius)
|
||||
* Positive omega (backspin) increases slip, causing friction to act against motion.
|
||||
* Negative omega (topspin) decreases slip, potentially making it negative,
|
||||
* which causes friction to accelerate the ball forward.
|
||||
*/
|
||||
const v_slip = v_horiz_impact + (omega * RADIUS);
|
||||
|
||||
// Horizontal Impulse (Change in velocity due to friction)
|
||||
const delta_v = surfaceFriction * vy_impact * (1 + E);
|
||||
|
||||
let finalV_horiz: number;
|
||||
if (v_slip > 0) {
|
||||
// Friction acts as a braking force (Backspin)
|
||||
finalV_horiz = v_horiz_impact - delta_v;
|
||||
} else {
|
||||
// Friction acts as an accelerating force (Topspin)
|
||||
finalV_horiz = v_horiz_impact + delta_v;
|
||||
}
|
||||
|
||||
// Results
|
||||
return {
|
||||
hangTime: Number(t.toFixed(2)),
|
||||
distance: Number(Math.sqrt(x * x + z * z).toFixed(2)),
|
||||
landingHorizontalVelo: Number(Math.max(0, finalV_horiz * FPS_TO_MPH).toFixed(2))
|
||||
};
|
||||
}
|
||||
21
types/GameState.ts
Normal file
21
types/GameState.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Team } from "./Team";
|
||||
|
||||
export type GameState = {
|
||||
readonly inning: number;
|
||||
readonly top: boolean;
|
||||
readonly count: AtBat;
|
||||
readonly outs: number;
|
||||
readonly score: GameScore;
|
||||
// homeTeam: Team;
|
||||
// awayTeam: Team;
|
||||
}
|
||||
|
||||
type AtBat = {
|
||||
readonly balls: number;
|
||||
readonly strikes: number;
|
||||
}
|
||||
|
||||
type GameScore = {
|
||||
readonly home: number;
|
||||
readonly away: number;
|
||||
}
|
||||
30
types/Player.ts
Normal file
30
types/Player.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// A softball player
|
||||
|
||||
export type Player = {
|
||||
readonly gender: "m" | "f" | "nb";
|
||||
readonly firstname: string;
|
||||
readonly lastname: string;
|
||||
readonly hometown: string;
|
||||
readonly skillset: Skillset;
|
||||
}
|
||||
|
||||
type Skillset = {
|
||||
readonly batting: number;
|
||||
readonly pitching: PitchingSkillset;
|
||||
readonly running: number;
|
||||
readonly fielding: number; // micro; muscle memory, movement, athleticism, etc.
|
||||
readonly gamesense: number; // macro
|
||||
}
|
||||
|
||||
type PitchingSkillset = { // the skill a player has in pitching
|
||||
// Player.pitching.armStrength, for example
|
||||
readonly armStrength: number;
|
||||
readonly gripStrength: number;
|
||||
readonly fourSeam: number; // starts at 0. a skill above 1 means a player "knows" a pitch.
|
||||
readonly twoSeam: number;
|
||||
readonly changeup: number;
|
||||
readonly curveball: number;
|
||||
readonly slider: number;
|
||||
readonly splitter: number;
|
||||
readonly knuckleball: number;
|
||||
}
|
||||
23
types/Team.ts
Normal file
23
types/Team.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Player } from "./Player";
|
||||
|
||||
export type Team = {
|
||||
// how to organize a team's list of batters, pitchers, bench players, etc.?
|
||||
readonly battingLineup: readonly Player[];
|
||||
readonly fieldingLineup: FieldingLineup;
|
||||
readonly bullpen: readonly Player[];
|
||||
readonly bench: readonly Player[];
|
||||
readonly batterIndex: number;
|
||||
// where to keep track of players no longer in the game?
|
||||
}
|
||||
|
||||
type FieldingLineup = {
|
||||
readonly pitcher: Player;
|
||||
readonly c: Player;
|
||||
readonly firstBase: Player;
|
||||
readonly secondBase: Player;
|
||||
readonly thirdBase: Player;
|
||||
readonly ss: Player;
|
||||
readonly lf: Player;
|
||||
readonly cf: Player;
|
||||
readonly rf: Player;
|
||||
}
|
||||
8
types/ThrownPitch.ts
Normal file
8
types/ThrownPitch.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export type ThrownPitch = {
|
||||
readonly name: string;
|
||||
readonly speed: number; // in mph
|
||||
readonly spinType: "6-12" | "4-10" | "3-9" | "12-6" | "9-3" | "none";
|
||||
readonly rpm: number; // example: 6-12 with fast rpm is fastball, 6-12 with slow rpm is changeup
|
||||
readonly accuracy: number; // how likely that a ball will end up where the pitcher meant for it to.
|
||||
}
|
||||
Reference in New Issue
Block a user