import type { Point2D } from "../shared/types"; /** * Ràng buộc ngân sách và tập khả thi của người tiêu dùng. * Không chứa DOM hay React. */ export interface BudgetConstraint { type: "linear" | "rationed" | "price_ceiling" | "inkind"; I: number; Px: number; Py: number; maxX: number; maxY: number; kinkPoint?: Point2D; vertices: Point2D[]; boundaryPoints: Point2D[]; maxAffordableY(x: number): number | null; } export interface LinearBudgetOptions { I: number; Px: number; Py: number; } export function createLinearBudget({ I, Px, Py }: LinearBudgetOptions): BudgetConstraint { const maxX = I / Px; const maxY = I / Py; return { type: "linear", I, Px, Py, maxX, maxY, boundaryPoints: [ { x: 0, y: maxY }, { x: maxX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: maxX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > maxX + 1e-9) return null; return Math.max(0, (I - Px * x) / Py); }, }; } export interface RationedBudgetOptions { I: number; Px: number; Py: number; quotaX: number; } export function createRationedBudget({ I, Px, Py, quotaX, }: RationedBudgetOptions): BudgetConstraint { const freeMaxX = I / Px; const maxY = I / Py; if (quotaX < freeMaxX) { const kinkY = (I - Px * quotaX) / Py; const kinkPoint: Point2D = { x: quotaX, y: kinkY, label: "Kink" }; return { type: "rationed", I, Px, Py, maxX: quotaX, maxY, kinkPoint, boundaryPoints: [ { x: 0, y: maxY }, { x: quotaX, y: kinkY }, { x: quotaX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: quotaX, y: kinkY }, { x: quotaX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > quotaX + 1e-9) return null; return Math.max(0, (I - Px * x) / Py); }, }; } // Hạn mức quotaX >= freeMaxX (không ràng buộc) return { type: "rationed", I, Px, Py, maxX: freeMaxX, maxY, boundaryPoints: [ { x: 0, y: maxY }, { x: freeMaxX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: freeMaxX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > freeMaxX + 1e-9) return null; return Math.max(0, (I - Px * x) / Py); }, }; } export interface PriceCeilingBudgetOptions { I: number; Px_ceiling: number; Py: number; supplyCapX: number; } export function createPriceCeilingBudget({ I, Px_ceiling, Py, supplyCapX, }: PriceCeilingBudgetOptions): BudgetConstraint { const freeMaxX = I / Px_ceiling; const maxY = I / Py; if (supplyCapX < freeMaxX) { const kinkY = (I - Px_ceiling * supplyCapX) / Py; const kinkPoint: Point2D = { x: supplyCapX, y: kinkY, label: "Kink" }; return { type: "price_ceiling", I, Px: Px_ceiling, Py, maxX: supplyCapX, maxY, kinkPoint, boundaryPoints: [ { x: 0, y: maxY }, { x: supplyCapX, y: kinkY }, { x: supplyCapX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: supplyCapX, y: kinkY }, { x: supplyCapX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > supplyCapX + 1e-9) return null; return Math.max(0, (I - Px_ceiling * x) / Py); }, }; } return { type: "price_ceiling", I, Px: Px_ceiling, Py, maxX: freeMaxX, maxY, boundaryPoints: [ { x: 0, y: maxY }, { x: freeMaxX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: freeMaxX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > freeMaxX + 1e-9) return null; return Math.max(0, (I - Px_ceiling * x) / Py); }, }; } export interface InKindBudgetOptions { I0: number; transferX?: number; transfer?: number; Px?: number; Py?: number; } export function createInKindBudget({ I0, transferX, transfer, Px = 1, Py = 1, }: InKindBudgetOptions): BudgetConstraint { const T = transferX !== undefined ? transferX : transfer !== undefined ? transfer : 0; const kinkX = T / Px; const kinkY = I0 / Py; const maxX = (I0 + T) / Px; const maxY = kinkY; if (T > 0) { const kinkPoint: Point2D = { x: kinkX, y: kinkY, label: "Kink" }; return { type: "inkind", I: I0 + T, Px, Py, maxX, maxY, kinkPoint, boundaryPoints: [ { x: 0, y: maxY }, { x: kinkX, y: kinkY }, { x: maxX, y: 0 }, ], vertices: [ { x: 0, y: 0 }, { x: 0, y: maxY }, { x: kinkX, y: kinkY }, { x: maxX, y: 0 }, ], maxAffordableY(x: number): number | null { if (x < -1e-9 || x > maxX + 1e-9) return null; if (x <= kinkX) return maxY; return Math.max(0, (I0 + T - Px * x) / Py); }, }; } // T = 0 tương đương ngân sách tuyến tính gốc return createLinearBudget({ I: I0, Px, Py }); }