From 35f8a5115a9473e1350755449d4a2932b7c057b6 Mon Sep 17 00:00:00 2001 From: danielyxie Date: Mon, 3 Jun 2019 22:21:36 -0700 Subject: [PATCH] Finished implementing player influencing on stock 2nd-order forecasts. Balanced recent stock market changes --- src/Constants.ts | 4 +- src/NetscriptFunctions.js | 14 +++- .../Player/PlayerObjectGeneralMethods.js | 14 ++-- src/StockMarket/IStockMarket.ts | 5 +- src/StockMarket/PlayerInfluencing.ts | 76 ++++++++++++++++++- src/StockMarket/Stock.ts | 16 +++- .../{StockMarket.jsx => StockMarket.tsx} | 75 ++++++++---------- src/StockMarket/StockMarketConstants.ts | 2 +- src/StockMarket/StockMarketHelpers.ts | 8 +- test/StockMarketTests.js | 58 +++++++++++++- 10 files changed, 209 insertions(+), 63 deletions(-) rename src/StockMarket/{StockMarket.jsx => StockMarket.tsx} (82%) diff --git a/src/Constants.ts b/src/Constants.ts index 7db9f67c9..4b9ae078d 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -224,7 +224,9 @@ export let CONSTANTS: IMap = { v0.47.0 * Stock Market changes: ** Transactions no longer influence stock prices (but they still influence forecast) - ** Changed the way stock's behave, particularly with regard to how the stock forecast occasionally "flips" + ** Changed the way stocks behave, particularly with regard to how the stock forecast occasionally "flips" + ** Hacking & growing a server can potentially affect the way the corresponding stock's forecast changes + ** Working for a company positively affects the way the corresponding stock's forecast changes * Scripts now start/stop instantly * Improved performance when starting up many copies of a new script (by Ornedan) diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index dd23c18c7..cb751f9ae 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -90,6 +90,10 @@ import { shortStock, sellShort, } from "./StockMarket/BuyingAndSelling"; +import { + influenceStockThroughServerHack, + influenceStockThroughServerGrow, +} from "./StockMarket/PlayerInfluencing"; import { Stock } from "./StockMarket/Stock"; import { StockMarket, @@ -439,7 +443,7 @@ function NetscriptFunctions(workerScript) { } return out; }, - hack : function(ip, { threads: requestedThreads } = {}){ + hack : function(ip, { threads: requestedThreads, stock } = {}){ updateDynamicRam("hack", getRamCost("hack")); if (ip === undefined) { throw makeRuntimeRejectMsg(workerScript, "Hack() call has incorrect number of arguments. Takes 1 argument"); @@ -501,6 +505,9 @@ function NetscriptFunctions(workerScript) { workerScript.scriptRef.log("Script SUCCESSFULLY hacked " + server.hostname + " for $" + formatNumber(moneyGained, 2) + " and " + formatNumber(expGainedOnSuccess, 4) + " exp (t=" + threads + ")"); } server.fortify(CONSTANTS.ServerFortifyAmount * Math.min(threads, maxThreadNeeded)); + if (stock) { + influenceStockThroughServerHack(server, moneyGained); + } return Promise.resolve(moneyGained); } else { // Player only gains 25% exp for failure? @@ -555,7 +562,7 @@ function NetscriptFunctions(workerScript) { return Promise.resolve(true); }); }, - grow : function(ip, { threads: requestedThreads } = {}){ + grow : function(ip, { threads: requestedThreads, stock } = {}){ updateDynamicRam("grow", getRamCost("grow")); const threads = resolveNetscriptRequestedThreads(workerScript, "grow", requestedThreads); if (ip === undefined) { @@ -596,6 +603,9 @@ function NetscriptFunctions(workerScript) { } workerScript.scriptRef.onlineExpGained += expGain; Player.gainHackingExp(expGain); + if (stock) { + influenceStockThroughServerGrow(server, moneyAfter - moneyBefore); + } return Promise.resolve(moneyAfter/moneyBefore); }); }, diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.js b/src/PersonObjects/Player/PlayerObjectGeneralMethods.js index 385c836cf..6a2f4b404 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.js +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.js @@ -39,6 +39,7 @@ import { SpecialServerIps, SpecialServerNames } from "../../Server/SpecialServer import { applySourceFile } from "../../SourceFile/applySourceFile"; import { SourceFiles } from "../../SourceFile/SourceFiles"; import { SourceFileFlags } from "../../SourceFile/SourceFileFlags"; +import { influenceStockThroughCompanyWork } from "../../StockMarket/PlayerInfluencing"; import Decimal from "decimal.js"; @@ -580,8 +581,8 @@ export function startWork(companyName) { } export function work(numCycles) { - //Cap the number of cycles being processed to whatever would put you at - //the work time limit (8 hours) + // Cap the number of cycles being processed to whatever would put you at + // the work time limit (8 hours) var overMax = false; if (this.timeWorked + (Engine._idleSpeed * numCycles) >= CONSTANTS.MillisecondsPer8Hours) { overMax = true; @@ -589,21 +590,24 @@ export function work(numCycles) { } this.timeWorked += Engine._idleSpeed * numCycles; - this.workRepGainRate = this.getWorkRepGain(); + this.workRepGainRate = this.getWorkRepGain(); this.processWorkEarnings(numCycles); - //If timeWorked == 8 hours, then finish. You can only gain 8 hours worth of exp and money + // If timeWorked == 8 hours, then finish. You can only gain 8 hours worth of exp and money if (overMax || this.timeWorked >= CONSTANTS.MillisecondsPer8Hours) { return this.finishWork(false); } - var comp = Companies[this.companyName], companyRep = "0"; + const comp = Companies[this.companyName]; + let companyRep = "0"; if (comp == null || !(comp instanceof Company)) { console.error(`Could not find Company: ${this.companyName}`); } else { companyRep = comp.playerReputation; } + influenceStockThroughCompanyWork(comp, this.workRepGainRate, numCycles); + const position = this.jobs[this.companyName]; var txt = document.getElementById("work-in-progress-text"); diff --git a/src/StockMarket/IStockMarket.ts b/src/StockMarket/IStockMarket.ts index e743e9173..eccfca12b 100644 --- a/src/StockMarket/IStockMarket.ts +++ b/src/StockMarket/IStockMarket.ts @@ -5,6 +5,7 @@ export type IStockMarket = { [key: string]: Stock; } & { lastUpdate: number; - storedCycles: number; Orders: IOrderBook; -} + storedCycles: number; + ticksUntilCycle: number; +}; diff --git a/src/StockMarket/PlayerInfluencing.ts b/src/StockMarket/PlayerInfluencing.ts index d026263b1..0162475f4 100644 --- a/src/StockMarket/PlayerInfluencing.ts +++ b/src/StockMarket/PlayerInfluencing.ts @@ -2,5 +2,79 @@ * Implementation of the mechanisms that allow the player to affect the * Stock Market */ -import { Server } from "../Server/Server"; +import { Stock } from "./Stock"; import { StockMarket } from "./StockMarket"; + +import { Company } from "../Company/Company"; +import { Server } from "../Server/Server"; + +// Change in second-order forecast due to hacks/grows +const forecastForecastChangeFromHack = 0.1; + +// Change in second-order forecast due to company work +const forecastForecastChangeFromCompanyWork = 0.001; + +/** + * Potentially decreases a stock's second-order forecast when its corresponding + * server is hacked. The chance of the hack decreasing the stock's second-order + * forecast is dependent on what percentage of the server's money is hacked + * @param {Server} server - Server being hack()ed + * @param {number} moneyHacked - Amount of money stolen from the server + */ +export function influenceStockThroughServerHack(server: Server, moneyHacked: number): void { + const orgName = server.organizationName; + let stock: Stock | null = null; + if (typeof orgName === "string" && orgName !== "") { + stock = StockMarket[orgName]; + } + if (!(stock instanceof Stock)) { return; } + + const percTotalMoneyHacked = moneyHacked / server.moneyMax; + if (Math.random() < percTotalMoneyHacked) { + console.log(`Influencing stock ${stock.name}`); + stock.changeForecastForecast(stock.otlkMagForecast - forecastForecastChangeFromHack); + } +} + +/** + * Potentially increases a stock's second-order forecast when its corresponding + * server is grown (grow()). The chance of the grow() to increase the stock's + * second-order forecast is dependent on how much money is added to the server + * @param {Server} server - Server being grow()n + * @param {number} moneyHacked - Amount of money added to the server + */ +export function influenceStockThroughServerGrow(server: Server, moneyGrown: number): void { + const orgName = server.organizationName; + let stock: Stock | null = null; + if (typeof orgName === "string" && orgName !== "") { + stock = StockMarket[orgName]; + } + if (!(stock instanceof Stock)) { return; } + + const percTotalMoneyGrown = moneyGrown / server.moneyMax; + if (Math.random() < percTotalMoneyGrown) { + console.log(`Influencing stock ${stock.name}`); + stock.changeForecastForecast(stock.otlkMagForecast + forecastForecastChangeFromHack); + } +} + +/** + * Potentially increases a stock's second-order forecast when the player works for + * its corresponding company. + * @param {Company} company - Company being worked for + * @param {number} performanceMult - Effectiveness of player's work. Affects influence + * @param {number} cyclesOfWork - # game cycles of work being processed + */ +export function influenceStockThroughCompanyWork(company: Company, performanceMult: number, cyclesOfWork: number): void { + const compName = company.name; + let stock: Stock | null = null; + if (typeof compName === "string" && compName !== "") { + stock = StockMarket[compName]; + } + if (!(stock instanceof Stock)) { return; } + + if (Math.random() < 0.001 * cyclesOfWork) { + const change = forecastForecastChangeFromCompanyWork * performanceMult; + stock.changeForecastForecast(stock.otlkMagForecast + change); + } +} diff --git a/src/StockMarket/Stock.ts b/src/StockMarket/Stock.ts index 9fe82eec1..c4214fc21 100644 --- a/src/StockMarket/Stock.ts +++ b/src/StockMarket/Stock.ts @@ -187,6 +187,18 @@ export class Stock { this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5; } + /** + * Safely set the stock's second-order forecast to a new value + */ + changeForecastForecast(newff: number): void { + this.otlkMagForecast = newff; + if (this.otlkMagForecast > 100) { + this.otlkMagForecast = 100; + } else if (this.otlkMagForecast < 0) { + this.otlkMagForecast = 0; + } + } + /** * Set the stock to a new price. Also updates the stock's previous price tracker */ @@ -232,9 +244,9 @@ export class Stock { */ cycleForecastForecast(changeAmt: number=0.1): void { if (Math.random() < 0.5) { - this.otlkMagForecast = Math.min(this.otlkMagForecast + changeAmt, 100); + this.changeForecastForecast(this.otlkMagForecast + changeAmt); } else { - this.otlkMagForecast = Math.max(this.otlkMagForecast - changeAmt, 0); + this.changeForecastForecast(this.otlkMagForecast - changeAmt); } } diff --git a/src/StockMarket/StockMarket.jsx b/src/StockMarket/StockMarket.tsx similarity index 82% rename from src/StockMarket/StockMarket.jsx rename to src/StockMarket/StockMarket.tsx index 8b6993150..22902bc50 100644 --- a/src/StockMarket/StockMarket.jsx +++ b/src/StockMarket/StockMarket.tsx @@ -4,14 +4,12 @@ import { shortStock, sellShort, } from "./BuyingAndSelling"; +import { IOrderBook } from "./IOrderBook"; +import { IStockMarket } from "./IStockMarket"; import { Order } from "./Order"; import { processOrders } from "./OrderProcessing"; import { Stock } from "./Stock"; import { TicksPerCycle } from "./StockMarketConstants"; -import { - getStockMarket4SDataCost, - getStockMarket4STixApiCost -} from "./StockMarketCosts"; import { InitStockMetadata } from "./data/InitStockMetadata"; import { OrderTypes } from "./data/OrderTypes"; import { PositionTypes } from "./data/PositionTypes"; @@ -21,6 +19,7 @@ import { StockMarketRoot } from "./ui/Root"; import { CONSTANTS } from "../Constants"; import { WorkerScript } from "../Netscript/WorkerScript"; import { Player } from "../Player"; +import { IMap } from "../types"; import { Page, routing } from ".././ui/navigationTracking"; import { numeralWrapper } from ".././ui/numeralFormat"; @@ -28,17 +27,17 @@ import { numeralWrapper } from ".././ui/numeralFormat"; import { dialogBoxCreate } from "../../utils/DialogBox"; import { Reviver } from "../../utils/JSONReviver"; -import React from "react"; -import ReactDOM from "react-dom"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; -export let StockMarket = {}; // Maps full stock name -> Stock object -export let SymbolToStockMap = {}; // Maps symbol -> Stock object +export let StockMarket: IStockMarket | IMap = {}; // Maps full stock name -> Stock object +export let SymbolToStockMap: IMap = {}; // Maps symbol -> Stock object -export function placeOrder(stock, shares, price, type, position, workerScript=null) { +export function placeOrder(stock: Stock, shares: number, price: number, type: OrderTypes, position: PositionTypes, workerScript: WorkerScript | null=null) { const tixApi = (workerScript instanceof WorkerScript); if (!(stock instanceof Stock)) { if (tixApi) { - workerScript.log(`ERROR: Invalid stock passed to placeOrder() function`); + workerScript!.log(`ERROR: Invalid stock passed to placeOrder() function`); } else { dialogBoxCreate(`ERROR: Invalid stock passed to placeOrder() function`); } @@ -46,7 +45,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu } if (typeof shares !== "number" || typeof price !== "number") { if (tixApi) { - workerScript.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument"); + workerScript!.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument"); } else { dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument"); } @@ -55,7 +54,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu const order = new Order(stock.symbol, shares, price, type, position); if (StockMarket["Orders"] == null) { - const orders = {}; + const orders: IOrderBook = {}; for (const name in StockMarket) { const stk = StockMarket[name]; if (!(stk instanceof Stock)) { continue; } @@ -68,7 +67,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu // Process to see if it should be executed immediately const processOrderRefs = { rerenderFn: displayStockMarketContent, - stockMarket: StockMarket, + stockMarket: StockMarket as IStockMarket, symbolToStockMap: SymbolToStockMap, } processOrders(stock, order.type, order.pos, processOrderRefs); @@ -78,7 +77,15 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu } // Returns true if successfully cancels an order, false otherwise -export function cancelOrder(params, workerScript=null) { +interface ICancelOrderParams { + order?: Order; + pos?: PositionTypes; + price?: number; + shares?: number; + stock?: Stock; + type?: OrderTypes; +} +export function cancelOrder(params: ICancelOrderParams, workerScript: WorkerScript | null=null) { var tixApi = (workerScript instanceof WorkerScript); if (StockMarket["Orders"] == null) {return false;} if (params.order && params.order instanceof Order) { @@ -108,42 +115,24 @@ export function cancelOrder(params, workerScript=null) { stockOrders.splice(i, 1); displayStockMarketContent(); if (tixApi) { - workerScript.scriptRef.log("Successfully cancelled order: " + orderTxt); + workerScript!.scriptRef.log("Successfully cancelled order: " + orderTxt); } return true; } } if (tixApi) { - workerScript.scriptRef.log("Failed to cancel order: " + orderTxt); + workerScript!.scriptRef.log("Failed to cancel order: " + orderTxt); } return false; } return false; } -export function loadStockMarket(saveString) { +export function loadStockMarket(saveString: string) { if (saveString === "") { StockMarket = {}; } else { StockMarket = JSON.parse(saveString, Reviver); - - // Backwards compatibility for v0.47.0 - const orderBook = StockMarket["Orders"]; - if (orderBook != null) { - // For each order, set its 'stockSymbol' property equal to the - // symbol of its 'stock' property - for (const stockSymbol in orderBook) { - const ordersForStock = orderBook[stockSymbol]; - if (Array.isArray(ordersForStock)) { - for (const order of ordersForStock) { - if (order instanceof Order && order.stock instanceof Stock) { - order.stockSymbol = order.stock.symbol; - } - } - } - } - console.log(`Converted Stock Market order book to v0.47.0 format`); - } } } @@ -163,7 +152,7 @@ export function initStockMarket() { StockMarket[name] = new Stock(metadata); } - const orders = {}; + const orders: IOrderBook = {}; for (const name in StockMarket) { const stock = StockMarket[name]; if (!(stock instanceof Stock)) { continue; } @@ -196,11 +185,13 @@ export function stockMarketCycle() { if (!(stock instanceof Stock)) { continue; } const roll = Math.random(); - if (roll < 0.4) { + if (roll < 0.2) { stock.flipForecastForecast(); - } else if (roll < 0.55) { + StockMarket.ticksUntilCycle = 4 * TicksPerCycle; + } else if (roll < 0.65) { stock.b = !stock.b; stock.flipForecastForecast(); + StockMarket.ticksUntilCycle = TicksPerCycle; } } } @@ -226,7 +217,6 @@ export function processStockPrices(numCycles=1) { --StockMarket.ticksUntilCycle; if (StockMarket.ticksUntilCycle <= 0) { stockMarketCycle(); - StockMarket.ticksUntilCycle = TicksPerCycle; } var v = Math.random(); @@ -251,7 +241,7 @@ export function processStockPrices(numCycles=1) { const c = Math.random(); const processOrderRefs = { rerenderFn: displayStockMarketContent, - stockMarket: StockMarket, + stockMarket: StockMarket as IStockMarket, symbolToStockMap: SymbolToStockMap, } if (c < chc) { @@ -282,7 +272,7 @@ export function processStockPrices(numCycles=1) { displayStockMarketContent(); } -let stockMarketContainer = null; +let stockMarketContainer: HTMLElement | null = null; function setStockMarketContainer() { stockMarketContainer = document.getElementById("stock-market-container"); document.removeEventListener("DOMContentLoaded", setStockMarketContainer); @@ -301,6 +291,7 @@ export function displayStockMarketContent() { } if (stockMarketContainer instanceof HTMLElement) { + const castedStockMarket = StockMarket as IStockMarket; ReactDOM.render( , stockMarketContainer ) diff --git a/src/StockMarket/StockMarketConstants.ts b/src/StockMarket/StockMarketConstants.ts index ce0a6a4ad..ddcdbb34e 100644 --- a/src/StockMarket/StockMarketConstants.ts +++ b/src/StockMarket/StockMarketConstants.ts @@ -2,4 +2,4 @@ * How many stock market 'ticks' before a 'cycle' is triggered. * A 'tick' is whenver stock prices update */ -export const TicksPerCycle = 100; +export const TicksPerCycle = 80; diff --git a/src/StockMarket/StockMarketHelpers.ts b/src/StockMarket/StockMarketHelpers.ts index c9533cef2..9126edc93 100644 --- a/src/StockMarket/StockMarketHelpers.ts +++ b/src/StockMarket/StockMarketHelpers.ts @@ -6,7 +6,7 @@ import { PositionTypes } from "./data/PositionTypes"; import { CONSTANTS } from "../Constants"; // Amount by which a stock's forecast changes during each price movement -export const forecastChangePerPriceMovement = 0.005; +export const forecastChangePerPriceMovement = 0.01; /** * Calculate the total cost of a "buy" transaction. This accounts for spread and commission. @@ -97,10 +97,8 @@ export function processTransactionForecastMovement(stock: Stock, shares: number) // Forecast always decreases in magnitude - const forecastChange = Math.min(5, forecastChangePerPriceMovement * (numIterations - 1)); - if (stock.otlkMag > 10) { - stock.otlkMag -= forecastChange; - } + const forecastChange = forecastChangePerPriceMovement * (numIterations - 1); + stock.otlkMag = Math.max(6, stock.otlkMag - forecastChange); } /** diff --git a/test/StockMarketTests.js b/test/StockMarketTests.js index e6bb9d60e..62b2ac90c 100644 --- a/test/StockMarketTests.js +++ b/test/StockMarketTests.js @@ -128,6 +128,22 @@ describe("Stock Market Tests", function() { }); }); + describe("#changeForecastForecast()", function() { + it("should get the stock's second-order forecast property", function() { + stock.changeForecastForecast(99); + expect(stock.otlkMagForecast).to.equal(99); + stock.changeForecastForecast(1); + expect(stock.otlkMagForecast).to.equal(1); + }); + + it("should prevent values outside of 0-100", function() { + stock.changeForecastForecast(101); + expect(stock.otlkMagForecast).to.equal(100); + stock.changeForecastForecast(-1); + expect(stock.otlkMagForecast).to.equal(0); + }); + }); + describe("#changePrice()", function() { it("should set both the last price and current price properties", function() { const newPrice = 20e3; @@ -139,7 +155,28 @@ describe("Stock Market Tests", function() { describe("#cycleForecast()", function() { it("should appropriately change the otlkMag by the given amount when b=true", function() { + stock.getForecastIncreaseChance = () => { return 1; } + stock.cycleForecast(5); + expect(stock.otlkMag).to.equal(ctorParams.otlkMag + 5); + stock.getForecastIncreaseChance = () => { return 0; } + stock.cycleForecast(10); + expect(stock.otlkMag).to.equal(ctorParams.otlkMag - 5); + }); + + it("should NOT(!) the stock's 'b' property if it causes 'otlkMag' to go below 0", function() { + stock.getForecastIncreaseChance = () => { return 0; } + stock.cycleForecast(25); + expect(stock.otlkMag).to.equal(5); + expect(stock.b).to.equal(false); + }); + }); + + describe("#cycleForecastForecast()", function() { + it("should increase the stock's second-order forecast by a given amount", function() { + const expected = [65, 75]; + stock.cycleForecastForecast(5); + expect(stock.otlkMagForecast).to.be.oneOf(expected); }); }); @@ -266,6 +303,10 @@ describe("Stock Market Tests", function() { stock.otlkMagForecast = 50; stock.otlkMag = 0; expect(stock.getForecastIncreaseChance()).to.equal(0.5); + + stock.otlkMagForecast = 25; + stock.otlkMag = 5; // Asolute forecast of 45 + expect(stock.getForecastIncreaseChance()).to.equal(0.3); }); }); }); @@ -343,12 +384,14 @@ describe("Stock Market Tests", function() { const stock = StockMarket[stockName]; if (!(stock instanceof Stock)) { continue; } initialValues[stock.symbol] = { - price: stock.price, + b: stock.b, otlkMag: stock.otlkMag, + price: stock.price, } } // Don't know or care how many exact cycles are required + StockMarket.lastUpdate = new Date().getTime() - 5e3; processStockPrices(1e9); // Both price and 'otlkMag' should be different @@ -356,7 +399,14 @@ describe("Stock Market Tests", function() { const stock = StockMarket[stockName]; if (!(stock instanceof Stock)) { continue; } expect(initialValues[stock.symbol].price).to.not.equal(stock.price); - expect(initialValues[stock.symbol].otlkMag).to.not.equal(stock.otlkMag); + // expect(initialValues[stock.symbol].otlkMag).to.not.equal(stock.otlkMag); + expect(initialValues[stock.symbol]).to.satisfy(function(initValue) { + if ((initValue.otlkMag !== stock.otlkMag) || (initValue.b !== stock.b)) { + return true; + } else { + return false; + } + }); } }); }); @@ -882,6 +932,10 @@ describe("Stock Market Tests", function() { }); describe("Order Processing", function() { + // TODO + }); + describe("Player Influencing", function() { + // TODO }); });