initial commit
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
106
CLAUDE.md
Normal file
106
CLAUDE.md
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# softball-server
|
||||
|
||||
> Paisios of Athos threw a weak curveball that hit the dirt.
|
||||
> Joan of Arc let it land. 1-0.
|
||||
> Paisios of Athos threw a fastball that hit the outer right corner.
|
||||
> Joan of Arc swung wildly. 1-1.
|
||||
> Paisios of Athos tried to throw a slider. It hung in the middle of the strike zone.
|
||||
> Joan of Arc made contact! The line drive sailed over the infield. Porphyrios of Athos caught it in center field. One out.
|
||||
|
||||
...
|
||||
|
||||
> Paisios of Athos tried to throw a slider. It started inside and broke down and away, missing the zone slightly.
|
||||
> St. George held his swing. Porphyrios of Athos framed the pitch expertly! It was called a strike! 0-1
|
||||
> Paisios of Athos is more confident in his slider.
|
||||
> Paisios of Athos aimed a four-seam fastball middle-middle. It clipped the outer edge of the zone.
|
||||
> St. George held his swing. Porphyrios of Athos framed the pitch, but did not get the call. 1-1.
|
||||
|
||||
> George Powell threw a changeup, aiming middle inside. He missed and threw middle-middle.
|
||||
> Andrew Stankiewicz unloaded confidently, sending a line drive to the gap in the outfield!
|
||||
> It rolled past Paisios (CF) and was picked up by Porphyrios (LF), who threw to second.
|
||||
> Wilyer Abreu made it to third. Andrew Stankiewicz stopped at first.
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
26
bun.lock
Normal file
26
bun.lock
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "softball-server",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
}
|
||||
}
|
||||
3
data/names.ts
Normal file
3
data/names.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// fullnames, first names, and last names
|
||||
//
|
||||
// fullnames are for prebuilt characters
|
||||
2
data/nicknames.ts
Normal file
2
data/nicknames.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// nicknames based on player stats or things that happened to them
|
||||
// can happen during a game or after a game
|
||||
40
game/atBatSimulator.ts
Normal file
40
game/atBatSimulator.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ThrownPitch } from "../types/ThrownPitch";
|
||||
import type { Player } from "../types/Player";
|
||||
import type { BattedBall } from "../types/BattedBall";
|
||||
import type { GameState } from "../types/GameState";
|
||||
|
||||
import pitchGenerator from "../systems/pitchGenerator";
|
||||
|
||||
export default function atBatSimulator(batter: Player, catcher: Player, pitcher: Player, game: GameState): GameState {
|
||||
if (game.outs < 3) {
|
||||
// do stuff
|
||||
const pitch: ThrownPitch = pitchGenerator(pitcher);
|
||||
|
||||
// a pitch can either be:
|
||||
// - batted
|
||||
// - caught for a ball/strike
|
||||
// - thrown wild
|
||||
|
||||
} else {
|
||||
if (game.top) {
|
||||
game.top = false;
|
||||
} else {
|
||||
game.top = true;
|
||||
game.inning++ // games will keep going on forever
|
||||
}
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
function swingSimulator(batter: Player, pitch: ThrownPitch): BattedBall {
|
||||
// see the ball!
|
||||
// decide if you're gonna swing!
|
||||
// aim true! get that timing right!
|
||||
// return a battedball
|
||||
return {
|
||||
exitVelo: 0,
|
||||
launchAngle: 0,
|
||||
attackAngle: 0,
|
||||
spinRate: 0
|
||||
};
|
||||
}
|
||||
30
game/gameRuntime.ts
Normal file
30
game/gameRuntime.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import type { Player } from "../types/Player";
|
||||
import type { GameState } from "../types/GameState";
|
||||
import type { ThrownPitch } from "../types/ThrownPitch";
|
||||
|
||||
import pitchGenerator from "../systems/pitchGenerator";
|
||||
import type { Team } from "../types/Team";
|
||||
|
||||
function startGame(homeTeam: Team, awayTeam: Team) {
|
||||
// simulate
|
||||
}
|
||||
|
||||
function advanceOrEndGame(game: GameState): GameState {
|
||||
// home team always bats last
|
||||
// if it's the top of the 9th and the away team is behind, the game is over
|
||||
if (game.inning == 9 || game.top == true && game.score.home > game.score.away) console.log("game should be over now.");
|
||||
|
||||
// next inning
|
||||
if (game.top) {
|
||||
return { ...game, top: false };
|
||||
} else {
|
||||
return {
|
||||
...game, inning: game.inning + 1, top: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function endGame(game: GameState) {
|
||||
console.log("GAME SHOULD END NOW SO FIGURE OUT WHAT TO DO!")
|
||||
}
|
||||
15
game/inningSimulator.ts
Normal file
15
game/inningSimulator.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { GameState } from "../types/GameState";
|
||||
import type { Team } from "../types/Team";
|
||||
import type { Player } from "../types/Player";
|
||||
import atBatSimulator from "./atBatSimulator";
|
||||
|
||||
export default function inningSimulator(battingTeam: Team, fieldingTeam: Team, game: GameState): GameState {
|
||||
while (game.outs < 3) {
|
||||
const batter: Player = battingTeam.battingLineup[battingTeam.batterIndex] as Player;
|
||||
const catcher: Player = fieldingTeam.fieldingLineup.c;
|
||||
const pitcher: Player = fieldingTeam.fieldingLineup.pitcher;
|
||||
|
||||
game = atBatSimulator(batter, catcher, pitcher, game);
|
||||
}
|
||||
return game;
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "softball-server",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
1
systems/dialogueGenerator.ts
Normal file
1
systems/dialogueGenerator.ts
Normal file
@@ -0,0 +1 @@
|
||||
// dialogue is things the players can say
|
||||
0
systems/hitGenerator.ts
Normal file
0
systems/hitGenerator.ts
Normal file
1
systems/injuryGenerator.ts
Normal file
1
systems/injuryGenerator.ts
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
8
systems/narrationEngine.ts
Normal file
8
systems/narrationEngine.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
// pitch narrator
|
||||
// response narrator (include count)
|
||||
// fielding narrator
|
||||
// baserunning narrator
|
||||
// development narrator
|
||||
// mood/dialogue narrator
|
||||
// ambient narrator
|
||||
24
systems/pitchGenerator.ts
Normal file
24
systems/pitchGenerator.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// pitchGenerator
|
||||
import type { Player } from "../types/Player";
|
||||
import type { ThrownPitch } from "../types/ThrownPitch";
|
||||
|
||||
// a pitch can
|
||||
// be thrown overhand, underhand, with spin that keeps it afloat or makes it a breaking ball,
|
||||
// or be thrown with no spin (difficult) for a knuckleball.
|
||||
// it can hit a batter, hit the dirt, or go wild.
|
||||
|
||||
export default function pitchGenerator(pitcher: Player) {
|
||||
|
||||
// what is the pitcher trying to throw?
|
||||
// what is the count? how good is the batter? what *can* he throw?
|
||||
|
||||
const pitch: ThrownPitch = {
|
||||
name: 'pitch',
|
||||
speed: 0,
|
||||
spinType: "6-12",
|
||||
rpm: 200,
|
||||
accuracy: 0.5
|
||||
}
|
||||
|
||||
return pitch;
|
||||
}
|
||||
56
test/generateFixtures.ts
Normal file
56
test/generateFixtures.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Player } from "../types/Player";
|
||||
import type { Team } from "../types/Team";
|
||||
|
||||
const firstNames = ["Alex", "Jordan", "Casey", "Riley", "Morgan", "Taylor", "Drew", "Jamie", "Sam", "Pat"];
|
||||
const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Wilson", "Moore"];
|
||||
const hometowns = ["Austin, TX", "Portland, OR", "Denver, CO", "Nashville, TN", "Raleigh, NC"];
|
||||
const genders = ["m", "f", "nb"] as const;
|
||||
|
||||
function rand(min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function pick<T>(arr: readonly T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)] as T;
|
||||
}
|
||||
|
||||
export function randomPlayer(): Player {
|
||||
return {
|
||||
gender: pick(genders),
|
||||
firstname: pick(firstNames),
|
||||
lastname: pick(lastNames),
|
||||
hometown: pick(hometowns),
|
||||
skillset: {
|
||||
batting: rand(0.2, 1.0),
|
||||
pitching: {
|
||||
armStrength: rand(0.2, 1.0),
|
||||
gripStrength: rand(0.2, 1.0),
|
||||
fourSeam: rand(0, 1.5),
|
||||
twoSeam: rand(0, 1.2),
|
||||
changeup: rand(0, 1.2),
|
||||
curveball: rand(0, 1.0),
|
||||
slider: rand(0, 1.0),
|
||||
splitter: rand(0, 0.8),
|
||||
knuckleball: rand(0, 0.5),
|
||||
},
|
||||
running: rand(0.2, 1.0),
|
||||
fielding: rand(0.2, 1.0),
|
||||
gamesense: rand(0.2, 1.0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function randomTeam(): Team {
|
||||
const battingLineup: Player[] = Array.from({ length: 9 }, randomPlayer);
|
||||
const [pitcher, c, firstBase, secondBase, thirdBase, ss, lf, cf, rf] = battingLineup as [
|
||||
Player, Player, Player, Player, Player, Player, Player, Player, Player
|
||||
];
|
||||
|
||||
return {
|
||||
battingLineup,
|
||||
fieldingLineup: { pitcher, c, firstBase, secondBase, thirdBase, ss, lf, cf, rf },
|
||||
bullpen: Array.from({ length: 3 }, randomPlayer),
|
||||
bench: Array.from({ length: 3 }, randomPlayer),
|
||||
batterIndex: 0,
|
||||
};
|
||||
}
|
||||
27
test/testAtBat.ts
Normal file
27
test/testAtBat.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { GameState } from "../types/GameState";
|
||||
import atBatSimulator from "../game/atBatSimulator";
|
||||
import { randomTeam } from "./generateFixtures";
|
||||
|
||||
const batting = randomTeam();
|
||||
const fielding = randomTeam();
|
||||
|
||||
const initialState: GameState = {
|
||||
inning: 1,
|
||||
top: true,
|
||||
outs: 0,
|
||||
count: { balls: 0, strikes: 0 },
|
||||
score: { home: 0, away: 0 },
|
||||
};
|
||||
|
||||
const batter = batting.battingLineup[batting.batterIndex]!;
|
||||
const pitcher = fielding.fieldingLineup.pitcher;
|
||||
const catcher = fielding.fieldingLineup.c;
|
||||
|
||||
console.log(`Batter: ${batter.firstname} ${batter.lastname} (batting: ${batter.skillset.batting.toFixed(2)})`);
|
||||
console.log(`Pitcher: ${pitcher.firstname} ${pitcher.lastname} (armStrength: ${pitcher.skillset.pitching.armStrength.toFixed(2)})`);
|
||||
console.log(`Catcher: ${catcher.firstname} ${catcher.lastname}`);
|
||||
console.log("---");
|
||||
|
||||
const result = atBatSimulator(batter, catcher, pitcher, initialState);
|
||||
|
||||
console.log("Result state:", result);
|
||||
14
test/testPitch.ts
Normal file
14
test/testPitch.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import pitchGenerator from "../systems/pitchGenerator";
|
||||
import { randomPlayer } from "./generateFixtures";
|
||||
|
||||
const pitcher = randomPlayer();
|
||||
|
||||
console.log(`Pitcher: ${pitcher.firstname} ${pitcher.lastname}`);
|
||||
console.log(` armStrength: ${pitcher.skillset.pitching.armStrength.toFixed(2)}`);
|
||||
console.log(` gripStrength: ${pitcher.skillset.pitching.gripStrength.toFixed(2)}`);
|
||||
console.log(` fourSeam: ${pitcher.skillset.pitching.fourSeam.toFixed(2)}`);
|
||||
console.log("---");
|
||||
|
||||
const pitch = pitchGenerator(pitcher);
|
||||
|
||||
console.log("ThrownPitch:", pitch);
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
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