Finished implementing player influencing on stock 2nd-order forecasts. Balanced recent stock market changes

This commit is contained in:
danielyxie 2019-06-03 22:21:36 -07:00
parent 8398fd47f0
commit 35f8a5115a
10 changed files with 209 additions and 63 deletions

@ -224,7 +224,9 @@ export let CONSTANTS: IMap<any> = {
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)

@ -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);
});
},

@ -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");

@ -5,6 +5,7 @@ export type IStockMarket = {
[key: string]: Stock;
} & {
lastUpdate: number;
storedCycles: number;
Orders: IOrderBook;
}
storedCycles: number;
ticksUntilCycle: number;
};

@ -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);
}
}

@ -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);
}
}

@ -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<any> = {}; // Maps full stock name -> Stock object
export let SymbolToStockMap: IMap<Stock> = {}; // 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(
<StockMarketRoot
buyStockLong={buyStock}
@ -311,7 +302,7 @@ export function displayStockMarketContent() {
placeOrder={placeOrder}
sellStockLong={sellStock}
sellStockShort={sellShort}
stockMarket={StockMarket}
stockMarket={castedStockMarket}
/>,
stockMarketContainer
)

@ -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;

@ -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);
}
/**

@ -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
});
});