diff --git a/.gitattributes b/.gitattributes index dfe0770..d66c128 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ -# Auto detect text files and perform LF normalization -* text=auto +case/secret_tundra.json filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index d85fd5d..6ff6e79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Data *.json +*.psd # Logs logs diff --git a/README.md b/README.md index 7a51acb..9c28e2e 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,19 @@ -# RoadSign for DOTA 2 +## T-Foresight -## How to start it +#### This is the repository for submission T-Foresight: Interpret Moving Strategies based on Context-Aware Trajectory Prediction. -1. install `NodeJS` -2. install `yarn` -3. run `yarn install` command -4. run `yarn run dev` command -5. visit `http://127.0.0.1:5173` +This repository provides the source codes. -## Main techniques used +### How to run it -- [React](https://react.dev/): UI framework -- [MobX](https://mobx.js.org/README.html): Data management -- [MUI](https://mui.com/material-ui/getting-started/): UI elements -- [Konva](https://konvajs.org/docs/react/Intro.html): Canvas drawing -- [I18N](https://react.i18next.com/): Change the language (we need an English version in our paper, and a Chinese version for user study) +We have a frontend (T-Foresight, in the folder `frontend\`) and a backend (the implementation of our proposed analytics workflow, in the folder `backend\`). +To save your review time, we provide the analysis progress of our domain experts for review (in the folder `case\`), so that you can just run the frontend and explore our system. -## Project structures +**Frontend**. Our frontend is built based on Vite, React, and MobX. You can run it in three steps. +1. *Install dependencies*. Please run `cd frontend` to change the root directory as `frontend\` and then run `yarn install` to install necessary dependencies. +2. *Run the interface*. Run `run dev` to start the user interface and open `http://localhost:5173` in your explorer (Chrome is recommended). Now, you can see the user interface. +3. *Load the data*. At the app bar, click on `IMPORT DATA` and select the file `case\secret_tundra.json` to load the game. Then click on `IMPORT CASE` and select one of the other two files, either `secret_tundra-case1.json` or `secret_tundra-case2.json`, to load the analysis progress. Now, you can explore the case! -- `src/components` is for some common and reusable UI elements -- `src/model` declares the data structure -- `src/store` declares the data center -- `src/utils` consists of some tool functions -- `src/views` contains the main UI designs - -## Example files - -Here are some example files with detailed comments, which demonstrate how the code works. - -- `src/store/store.js` demonstrates how we manage the data -- `src/store/App.jsx` demonstrates how we render the views based on data -- `src/model/D2Data.d.ts` demonstrates how we define data types +**Backend**. Our backend is build based on FastAPI. You can use it in two steps. +1. *Install dependencies*. Please run `cd backend` to change the root directory as `backend\` and run `pip install -r requirements.text` to install necessary packages. +2. *Run the server*. Please run `uvicorn main:app --reload` to start the server. But we cannot upload our dataset since it is so large. So you cannot use the functions of the backend. diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..441c95c --- /dev/null +++ b/backend/main.py @@ -0,0 +1,53 @@ +from fastapi import FastAPI + +from src.alg.data import search_inst, search_similar_inst +from src.alg.interpret import aggregate_workers, proj, extend_stage +from src.model.model import Model + +app = FastAPI() + + +@app.post("/prediction") +async def root(data): + game_name = data["gameName"] + team_id = data["teamId"] + player_id = data["playerId"] + frame = data["frame"] + context_limit = data["contextLimit"] + + inst = search_inst('./data', game_name, team_id, player_id, frame) + similar_inst = search_similar_inst('./data', inst, team_id, player_id, context_limit) + + model = Model.load('./model/v3') + feature_vectors = [[] for _ in model.workers] + stages = [] + for simi_inst in similar_inst: + predictions = model.predict(simi_inst) + feature_vectors_4_single_inst = [[] for _ in model.workers] + for i, worker_pred in enumerate(predictions): + fv = [] + for g_name in worker_pred['attention']: + for i_name in worker_pred['attention'][g_name]: + fv.append(worker_pred['attention'][g_name][i_name]) + for pos in worker_pred['trajectory']: + fv.append(pos[0]) + fv.append(pos[1]) + feature_vectors[i].extend(fv) + feature_vectors_4_single_inst[i].extend(fv) + + pred_groups = aggregate_workers(feature_vectors_4_single_inst) + extend_stage(stages, pred_groups) + + pred_groups = aggregate_workers(feature_vectors) + pred_projection = proj(pred_groups, feature_vectors) + + return { + "predictions": model.predict(inst), + "predGroups": pred_groups, + "predProjection": pred_projection, + "predInstances": { + "stages": stages, + "numPredictors": model.K, + "totalInstances": len(similar_inst) + }, + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..cbc7ad5 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,12 @@ +fastapi==0.11.0 +numpy==1.16.3 +numba==0.43.1 +scipy==1.2.1 +matplotlib==3.0.3 +scikit-learn==0.20.3 + +--find-links https://download.pytorch.org/whl/cu118 + +torch==2.2.2 +torchvision==0.17.2 +torchaudio==2.2.2 diff --git a/backend/src/alg/catsne.py b/backend/src/alg/catsne.py new file mode 100644 index 0000000..310495e --- /dev/null +++ b/backend/src/alg/catsne.py @@ -0,0 +1,1002 @@ +#! python3 +# -*-coding:Utf-8 -* + +######################################################################################################## +######################################################################################################## + +# catsne.py + +# This code implements cat-SNE, a class-aware version of t-SNE, as well as quality assessment criteria for both supervised and unsupervised dimensionality reduction. +# Cat-SNE was presented at the ESANN 2019 conference. + +# Please cite as: +# - de Bodt, C., Mulders, D., López-Sánchez, D., Verleysen, M., & Lee, J. A. (2019). Class-aware t-SNE: cat-SNE. In ESANN (pp. 409-414). +# - BibTeX entry: +# @inproceedings{cdb2019catsne, +# title={Class-aware {t-SNE}: {cat-SNE}}, +# author={de Bodt, C. and Mulders, D. and L\'opez-S\'anchez, D. and Verleysen, M. and Lee, J. A.}, +# booktitle={ESANN}, +# pages={409--414}, +# year={2019} +# } + +# The most important functions of this file are: +# - catsne: enables applying cat-SNE to reduce the dimension of a data set. The documentation of the function describes its parameters. +# - eval_dr_quality: enables evaluating the quality of an embedding in an unsupervised way. It computes quality assessment criteria measuring the neighborhood preservation from the high-dimensional space to the low-dimensional one. The documentation of the function explains the meaning of the criteria and how to interpret them. +# - knngain: enables evaluating the quality of an embedding in a supervised way. It computes criteria related to the accuracy of a KNN classifier in the low-dimensional space. The documentation of the function explains the meaning of the criteria and how to interpret them. +# - viz_qa: a plot function to easily visualize the quality criteria. +# At the end of the file, a demo presents how the code and the above functions can be used. Running this code will run the demo. Importing this module will not run the demo. + +# Notations: +# - DR: dimensionality reduction +# - HD: high-dimensional +# - LD: low-dimensional +# - HDS: HD space +# - LDS: LD space + +# References: +# [1] de Bodt, C., Mulders, D., López-Sánchez, D., Verleysen, M., & Lee, J. A. (2019). Class-aware t-SNE: cat-SNE. In ESANN (pp. 409-414). +# [2] Lee, J. A., & Verleysen, M. (2009). Quality assessment of dimensionality reduction: Rank-based criteria. Neurocomputing, 72(7-9), 1431-1443. +# [3] Lee, J. A., & Verleysen, M. (2010). Scale-independent quality criteria for dimensionality reduction. Pattern Recognition Letters, 31(14), 2248-2257. +# [4] Lee, J. A., Renard, E., Bernard, G., Dupont, P., & Verleysen, M. (2013). Type 1 and 2 mixtures of Kullback–Leibler divergences as cost functions in dimensionality reduction based on similarity preservation. Neurocomputing, 112, 92-108. +# [5] Lee, J. A., Peluffo-Ordóñez, D. H., & Verleysen, M. (2015). Multi-scale similarities in stochastic neighbour embedding: Reducing dimensionality while preserving both local and global structure. Neurocomputing, 169, 246-261. +# [6] Maaten, L. V. D., & Hinton, G. (2008). Visualizing data using t-SNE. Journal of Machine Learning Research, 9(Nov), 2579-2605. +# [7] Jacobs, R. A. (1988). Increased rates of convergence through learning rate adaptation. Neural networks, 1(4), 295-307. + +# author: Cyril de Bodt (ICTEAM - UCLouvain) +# @email: cyril __dot__ debodt __at__ uclouvain.be +# Last modification date: May 15th, 2019 +# Copyright (c) 2019 Universite catholique de Louvain (UCLouvain), ICTEAM. All rights reserved. + +# This code was created and tested with Python 3.7.3 (Anaconda distribution, Continuum Analytics, Inc.). It uses the following modules: +# - numpy: version 1.16.3 tested +# - numba: version 0.43.1 tested +# - scipy: version 1.2.1 tested +# - matplotlib: version 3.0.3 tested +# - scikit-learn: version 0.20.3 tested + +# You can use, modify and redistribute this software freely, but not for commercial purposes. +# The use of the software is at your own risk; the authors are not responsible for any damage as a result from errors in the software. + +######################################################################################################## +######################################################################################################## + +import matplotlib +import matplotlib.pyplot as plt +import numba +import numpy as np +import scipy.spatial.distance +import sklearn.datasets +import sklearn.decomposition + + +############################## +############################## +# General functions used by others in the code. +############################## + +@numba.jit(nopython=True) +def close_to_zero(v): + """ + Check whether v is close to zero or not. + In: + - v: a scalar or numpy array. + Out: + A boolean or numpy array of boolean of the same shape as v, with True when the entry is close to 0 and False otherwise. + """ + return np.absolute(v) <= 10.0 ** (-8.0) + + +@numba.jit(nopython=True) +def arange_except_i(N, i): + """ + Create a 1-D numpy array of integers from 0 to N-1 with step 1, except i. + In: + - N: a strictly positive integer. + - i: a positive integer which is strictly smaller than N. + Out: + A 1-D numpy array of integers from 0 to N-1 with step 1, except i. + """ + arr = np.arange(N) + return np.hstack((arr[:i], arr[i + 1:])) + + +@numba.jit(nopython=True) +def fill_diago(M, v): + """ + Replace the elements on the diagonal of a square matrix M with some value v. + In: + - M: a 2-D numpy array storing a square matrix. + - v: some value. + Out: + M, but in which the diagonal elements have been replaced with v. + """ + for i in range(M.shape[0]): + M[i, i] = v + return M + + +@numba.jit(nopython=True) +def compute_gradn(grad): + """ + Compute the norm of a gradient. + In: + - grad: numpy array of float storing a gradient. + Out: + Infinite norm of the gradient. + """ + return np.absolute(grad).max() + + +@numba.jit(nopython=True) +def compute_rel_obj_diff(prev_obj, obj, n_eps): + """ + Compute the relative objective function difference between two steps in a gradient descent. + In: + - prev_obj: objective function value at previous iteration. + - obj: current objective function value. + - n_eps: a small float that should be equal to np.finfo(dtype=np.float64).eps. + Out: + np.abs(prev_obj - obj)/max(np.abs(prev_obj), np.abs(obj)) + """ + return np.abs(prev_obj - obj) / np.maximum(n_eps, max(np.abs(prev_obj), np.abs(obj))) + + +############################## +############################## +# Cat-SNE [1]. +# The main function which should be used is 'catsne'. +# See its documentation for details. +# The demo at the end of this file presents how to use catsne function. +############################## + +@numba.jit(nopython=True) +def sne_sim(dsi, vi, i, compute_log=True): + """ + Compute the SNE asymmetric similarities, as well as their log. + N refers to the number of data points. + In: + - dsi: numpy 1-D array of floats with N squared distances with respect to data point i. Element k is the squared distance between data points k and i. + - vi: bandwidth of the exponentials in the similarities with respect to i. + - i: index of the data point with respect to which the similarities are computed, between 0 and N-1. + - compute_log: boolean. If True, the logarithms of the similarities are also computed, and otherwise not. + Out: + A tuple with two elements: + - A 1-D numpy array of floats with N elements. Element k is the SNE similarity between data points i and k. + - If compute_log is True, a 1-D numpy array of floats with N element. Element k is the log of the SNE similarity between data points i and k. By convention, element i is set to 0. If compute_log is False, it is set to np.empty(shape=N, dtype=np.float64). + """ + N = dsi.size + si = np.empty(shape=N, dtype=np.float64) + si[i] = 0.0 + log_si = np.empty(shape=N, dtype=np.float64) + indj = arange_except_i(N=N, i=i) + dsij = dsi[indj] + log_num_sij = (dsij.min() - dsij) / vi + si[indj] = np.exp(log_num_sij) + den_si = si.sum() + si /= den_si + if compute_log: + log_si[i] = 0.0 + log_si[indj] = log_num_sij - np.log(den_si) + return si, log_si + + +@numba.jit(nopython=True) +def sne_bsf(dsi, vi, i, log_perp): + """ + Function on which a binary search is performed to find the HD bandwidth of the i^th data point in SNE. + In: + - dsi, vi, i: same as in sne_sim function. + - log_perp: logarithm of the targeted perplexity. + Out: + A float corresponding to the current value of the entropy of the similarities with respect to i, minus log_perp. + """ + si, log_si = sne_sim(dsi=dsi, vi=vi, i=i, compute_log=True) + return -np.dot(si, log_si) - log_perp + + +@numba.jit(nopython=True) +def sne_bs(dsi, i, log_perp, x0=1.0): + """ + Binary search to find the root of sne_bsf over vi. + In: + - dsi, i, log_perp: same as in sne_bsf function. + - x0: starting point for the binary search. Must be strictly positive. + Out: + A strictly positive float vi such that sne_bsf(dsi, vi, i, log_perp) is close to zero. + """ + fx0 = sne_bsf(dsi=dsi, vi=x0, i=i, log_perp=log_perp) + if close_to_zero(v=fx0): + return x0 + elif not np.isfinite(fx0): + raise ValueError("Error in function sne_bs: fx0 is nan.") + elif fx0 > 0: + x_up, x_low = x0, x0 / 2.0 + fx_low = sne_bsf(dsi=dsi, vi=x_low, i=i, log_perp=log_perp) + if close_to_zero(v=fx_low): + return x_low + elif not np.isfinite(fx_low): + # WARNING: can not find a valid root! + return x_up + while fx_low > 0: + x_up, x_low = x_low, x_low / 2.0 + fx_low = sne_bsf(dsi=dsi, vi=x_low, i=i, log_perp=log_perp) + if close_to_zero(v=fx_low): + return x_low + if not np.isfinite(fx_low): + return x_up + else: + x_up, x_low = x0 * 2.0, x0 + fx_up = sne_bsf(dsi=dsi, vi=x_up, i=i, log_perp=log_perp) + if close_to_zero(v=fx_up): + return x_up + elif not np.isfinite(fx_up): + return x_low + while fx_up < 0: + x_up, x_low = 2.0 * x_up, x_up + fx_up = sne_bsf(dsi=dsi, vi=x_up, i=i, log_perp=log_perp) + if close_to_zero(v=fx_up): + return x_up + while True: + x = (x_up + x_low) / 2.0 + fx = sne_bsf(dsi=dsi, vi=x, i=i, log_perp=log_perp) + if close_to_zero(v=fx): + return x + elif fx > 0: + x_up = x + else: + x_low = x + + +@numba.jit(nopython=True) +def catsne_hd_sim(ds_hd, labels, theta, n_eps): + """ + Compute the symmetrized HD similarities of cat-SNE, as defined in [1]. + In: + - ds_hd: 2-D numpy array of floats with shape (N, N), where N is the number of data points. Element [i,j] must be the squared HD distance between data points i and j. + - labels, theta: see catsne function. + - n_eps: should be equal to np.finfo(dtype=np.float64).eps. + Out: + A tuple with: + - A 2-D numpy array of floats with shape (N, N) and in which element [i,j] is the symmetrized HD similarity between data points i and j, as defined in [1]. + - A 1-D numpy array of floats with N elements. Element i indicates the probability mass associated to data points with the same class as i in the HD Gaussian neighborhood around i. + """ + # Number of data points + N = ds_hd.shape[0] + # Computing the N**2 HD similarities + sigma_ij = np.empty(shape=(N, N), dtype=np.float64) + L = int(round(np.log2(np.float64(N) / 2.0))) + log_perp = np.log(2.0 ** (np.linspace(L, 1, L).astype(np.float64))) + max_ti = np.empty(shape=N, dtype=np.float64) + for i in range(N): + vi = 1.0 + h = 0 + go = True + max_ti[i] = -1.0 + labi = labels == labels[i] + labi[i] = False + while go and (h < L): + vi = sne_bs(dsi=ds_hd[i, :], i=i, log_perp=log_perp[h], x0=vi) + si = sne_sim(dsi=ds_hd[i, :], vi=vi, i=i, compute_log=False)[0] + h += 1 + ssi = np.sum(si[labi]) + if ssi > max_ti[i]: + max_ti[i] = ssi + sigma_ij[i, :] = si + if max_ti[i] > theta: + go = False + # Symmetrized version + sigma_ij += sigma_ij.T + # Returning the normalization of sigma_ij, and max_ti. + return sigma_ij / np.maximum(n_eps, sigma_ij.sum()), max_ti + + +@numba.jit(nopython=True) +def catsne_ld_sim(ds_ld, n_eps): + """ + Compute the LD similarities of cat-SNE, as well as their log, as defined in [1]. + In: + - ds_ld: 2-D numpy array of floats with shape (N, N), where N is the number of data points. Element [i,j] must be the squared LD distance between data points i and j. + - n_eps: same as in catsne_g function. + Out: + A tuple with three elements: + - A 2-D numpy array of floats with shape (N, N) and in which element [i,j] is the LD similarity between data points i and j. + - A 2-D numpy array of floats with shape (N, N) and in which element [i,j] is the log of the LD similarity between data points i and j. By convention, the log of 0 is set to 0. + - 1.0/(1.0+ds_ld) + """ + ds_ldp = 1.0 + ds_ld + idsld = 1.0 / np.maximum(n_eps, ds_ldp) + s_ijt = idsld.copy() + log_s_ijt = -np.log(ds_ldp) + s_ijt = fill_diago(M=s_ijt, v=0.0) + log_s_ijt = fill_diago(M=log_s_ijt, v=0.0) + den_s_ijt = s_ijt.sum() + s_ijt /= np.maximum(n_eps, den_s_ijt) + log_s_ijt -= np.log(den_s_ijt) + return s_ijt, log_s_ijt, idsld + + +@numba.jit(nopython=True) +def catsne_obj(sigma_ijt, log_s_ijt): + """ + Compute the cat-SNE objective function. + In: + - sigma_ijt: 2-D numpy array of floats, in which element [i,j] contains the HD similarity between data points i and j, as defined in [1]. + - log_s_ijt: 2-D numpy array of floats, in which element [i,j] contains the log of the LD similarity between data points i and j, as defined in [1]. + Out: + The value of the cat-SNE objective function. + """ + return -(sigma_ijt.ravel()).dot(log_s_ijt.ravel()) + + +def catsne_g(X_lds, sigma_ijt, nit, eei, eef, n_eps): + """ + Compute the gradient of the objective function of cat-SNE at some LD coordinates, as well as the current value of the objective function. + In: + - X_lds: 2-D numpy array of floats with N rows, where N is the number of data points. It contains one example per row and one feature per column. It stores the current LD coordinates. + - sigma_ijt: 2-D numpy array of floats with shape (N, N), where element [i,j] contains the HD similarity between data points i and j. + - nit: number of gradient descent steps which have already been performed. + - eei: number of gradient steps to perform with early exageration. + - eef: early exageration factor. + - n_eps: a small float to avoid making divisions with a denominator close to 0. + Out: + A tuple with two elements: + - grad: a 2-D numpy array of floats with the same shape as X_lds, containing the gradient at X_lds. + - obj: objective function value at X_lds. + """ + # Computing the LD similarities. + s_ijt, log_s_ijt, idsld = catsne_ld_sim( + ds_ld=scipy.spatial.distance.squareform(X=scipy.spatial.distance.pdist(X=X_lds, metric='sqeuclidean'), + force='tomatrix'), n_eps=n_eps) + + # Computing the current objective function value + if nit < eei: + obj = catsne_obj(sigma_ijt=sigma_ijt / eef, log_s_ijt=log_s_ijt) + else: + obj = catsne_obj(sigma_ijt=sigma_ijt, log_s_ijt=log_s_ijt) + # Computing the gradient. + c_ij = 4 * (sigma_ijt - s_ijt) * idsld + grad = (X_lds.T * c_ij.dot(np.ones(shape=X_lds.shape[0]))).T - c_ij.dot(X_lds) + # Returning + return grad, obj + + +def dbd_rule(delta_bar, grad, stepsize, kappa=0.2, phi=0.8, tdb=0.5): + """ + Delta-bar-delta stepsize adaptation rule in a gradient descent procedure, as proposed in [7]. + In: + - delta_bar: numpy array which stores the current value of the delta bar. + - grad: numpy array which stores the value of the gradient at the current coordinates. + - stepsize: numpy array which stores the current values of the step sizes associated with the variables. + - kappa: linear stepsize increase when delta_bar and the gradient are of the same sign. + - phi: exponential stepsize decrease when delta_bar and the gradient are of different signs. + - tdb: parameter for the update of delta_bar. + Out: + A tuple with two elements: + - A numpy array with the update of delta_bar. + - A numpy array with the update of stepsize. + """ + dbdp = np.sign(delta_bar) * np.sign(grad) + stepsize[dbdp > 0] += kappa + stepsize[dbdp < 0] *= phi + delta_bar = (1 - tdb) * grad + tdb * delta_bar + return delta_bar, stepsize + + +@numba.jit(nopython=True) +def mgd_step(X, up_X, nit, mom_t, mom_init, mom_fin, stepsize, grad): + """ + Momentum gradient descent step. + In: + - X: numpy array containing the current value of the variables. + - up_X: numpy array with the same shape as X storing the update made on the variables at the previous gradient step. + - nit: number of gradient descent iterations which have already been performed. + - mom_t: number of gradient descent steps to perform before changing the momentum coefficient. + - mom_init: momentum coefficient to use when nit=mom_t. + - stepsize: step size to use in the gradient descent. Either a scalar, or a numpy array with the same shape as X. + - grad: numpy array with the same shape as X, storing the gradient of the objective function at the current coordinates. + Out: + A tuple with two elements: + - A numpy array with the updated coordinates, after having performed the momentum gradient descent step. + - A numpy array storing the update performed on the variables. + """ + if nit < mom_t: + mom = mom_init + else: + mom = mom_fin + up_X = mom * up_X - (1 - mom) * stepsize * grad + X += up_X + return X, up_X + + +def catsne_mgd(ds_hd, labels, theta, n_eps, eei, eef, X_lds, ftol, N, dim_lds, mom_t, nit_max, gtol, mom_init, mom_fin): + """ + Performing momentum gradient descent in cat-SNE. + In: + - ds_hd: 2-D numpy array of floats with shape (N, N), where N is the number of data points. Element [i,j] must be the squared HD distance between data points i and j. + - labels, theta, eei, eef, ftol, dim_lds, mom_t, nit_max, gtol, mom_init, mom_fin: as in catsne function. + - n_eps: a small float to avoid making divisions with a denominator close to 0. + - X_lds: 2-D numpy array of floats with N rows. It contains one example per row and one feature per column. It stores the initial LD coordinates. + - N: number of data points. + Out: + A tuple with: + - a 2-D numpy array of floats with shape (N, dim_lds), containing the LD representations of the data points in its rows. + - a 1-D numpy array of floats with N elements. Element at index i indicates the probability mass around X_hds[i,:] which lies on neighbors of the same class. + """ + # Computing the HD similarities. + sigma_ijt, max_ti = catsne_hd_sim(ds_hd=ds_hd, labels=labels, theta=theta, n_eps=n_eps) + # Current number of gradient descent iterations. + nit = 0 + # Early exageration + if eei > nit: + sigma_ijt *= eef + # Computing the current gradient and objective function values. + grad, obj = catsne_g(X_lds=X_lds, sigma_ijt=sigma_ijt, nit=nit, eei=eei, eef=eef, n_eps=n_eps) + gradn = compute_gradn(grad=grad) + # LD coordinates achieving the smallest value of the objective function. + best_X_lds = X_lds.copy() + # Smallest value of the objective function. + best_obj = obj + # Objective function value at previous iteration. + prev_obj = (1 + 100 * ftol) * obj + rel_obj_diff = compute_rel_obj_diff(prev_obj=prev_obj, obj=obj, n_eps=n_eps) + # Step size parameters. The steps are adapted during the gradient descent as in [6], using the Delta-Bar-Delta learning rule from [7]. + epsilon, kappa, phi, tdb = 500, 0.2, 0.8, 0.5 + stepsize, delta_bar = epsilon * np.ones(shape=(N, dim_lds), dtype=np.float64), np.zeros(shape=(N, dim_lds), + dtype=np.float64) + # Update of X_lds + up_X_lds = np.zeros(shape=(N, dim_lds), dtype=np.float64) + # Gradient descent. + while (nit <= eei) or (nit <= mom_t) or ((nit < nit_max) and (gradn > gtol) and (rel_obj_diff > ftol)): + # Computing the step sizes, following the delta-bar-delta rule, from [7]. + delta_bar, stepsize = dbd_rule(delta_bar=delta_bar, grad=grad, stepsize=stepsize, kappa=kappa, phi=phi, tdb=tdb) + # Performing the gradient descent step with momentum. + X_lds, up_X_lds = mgd_step(X=X_lds, up_X=up_X_lds, nit=nit, mom_t=mom_t, mom_init=mom_init, mom_fin=mom_fin, + stepsize=stepsize, grad=grad) + # Centering the result + X_lds -= X_lds.mean(axis=0) + # Incrementing the iteration counter + nit += 1 + # Checking whether early exageration is over + if nit == eei: + sigma_ijt /= eef + # Updating the previous objective function value + prev_obj = obj + # Computing the gradient at the current LD coordinates and the current objective function value. + grad, obj = catsne_g(X_lds=X_lds, sigma_ijt=sigma_ijt, nit=nit, eei=eei, eef=eef, n_eps=n_eps) + gradn = compute_gradn(grad=grad) + rel_obj_diff = compute_rel_obj_diff(prev_obj=prev_obj, obj=obj, n_eps=n_eps) + # Updating best_obj and best_X_lds + if best_obj > obj: + best_obj, best_X_lds = obj, X_lds.copy() + # Returning + return best_X_lds, max_ti + + +def catsne(X_hds, labels, theta=0.9, init='ran', dim_lds=2, nit_max=1000, rand_state=None, hd_metric='euclidean', + D_hd_metric=None, gtol=10.0 ** (-5.0), ftol=10.0 ** (-8.0), eef=4, eei=100, mom_init=0.5, mom_fin=0.8, + mom_t=250): + """ + Apply cat-SNE to reduce the dimensionality of a data set by accounting for class labels. + Euclidean distance is employed in the LDS, as in t-SNE. + In: + - X_hds: 2-D numpy array of floats with shape (N, M), containing the HD data set, with one row per example and one column per dimension. N is hence the number of data points and M the dimension of the HDS. It is assumed that the rows of X_hds are all distinct. If hd_metric is set to 'precomputed', then X_hds must be a 2-D numpy array of floats with shape (N,N) containing the pairwise distances between the data points. This matrix is assumed to be symmetric. + - labels: 1-D numpy array with N elements, containing integers indicating the class labels of the data points. + - theta: treshold on the probability mass, around each HD datum, which lies on neighbors with the same class, to fit the precisions of the HD Gaussian neighborhoods. See [1] for further details. This parameter must range in [0.5,1[. + - init: specify the initialization of the LDS. It is either equal to 'ran', in which case the LD coordinates of the data points are initialized randomly using a Gaussian distribution centered around the origin and with a small variance, or to 'pca', in which case the LD coordinates of the data points are initialized using the PCA projection of the HD samples, or to a 2-D numpy array with N rows, in which case the initial LD coordinates of the data points are specified in the rows of init. In case hd_metric is set to 'precomputed', init can not be set to 'pca'. + - dim_lds: dimension of the LDS. Must be an integer strictly greater than 0. In case init is a 2-D array, dim_lds must be equal to init.shape[1]. + - nit_max: integer strictly greater than 0 wich specifies the maximum number of gradient descent iterations. + - rand_state: instance of numpy.random.RandomState. If None, set to numpy.random. + - hd_metric: metric to compute the HD distances. It must be one of the following: + --- a string. In this case, it must be one of the following: + ------ a valid value for the 'metric' parameter of the scipy.spatial.distance.pdist function. + ------ 'precomputed', in which case X_hds must be a 2-D numpy array of floats with shape (N,N) containing the symmetric pairwise distances between the data points. init must, in this case, be different from 'pca'. + --- a callable. In this case, it must take two rows of X_hds as parameters and return the distance between the corresponding data points. The distance function is assumed to be symmetric. + - D_hd_metric: optional dictionary to specify additional arguments to scipy.spatial.distance.pdist, depending on the employed metric. + - gtol: tolerance on the infinite norm of the gradient during the gradient descent. + - ftol: tolerance on the relative updates of the objective function during the gradient descent. + - eef: early exageration factor. + - eei: number of gradient descent steps to perform with early exageration. + - mom_init: initial momentum factor value in the gradient descent. + - mom_fin: final momentum factor value in the gradient descent. + - mom_t: iteration at which the momentum factor value changes during the gradient descent. + Out: + A tuple with: + - a 2-D numpy array of floats with shape (N, dim_lds), containing the LD representations of the data points in its rows. + - a 1-D numpy array of floats with N elements. Element at index i indicates the probability mass around X_hds[i,:] which lies on neighbors of the same class. + """ + # Number of data points + N = X_hds.shape[0] + # Checking theta + if (theta < 0.5) or (theta >= 1): + raise ValueError( + "Error in function catsne: theta={theta} whereas it must range in [0.5,1[.".format(theta=theta)) + # Checking rand_state + if rand_state is None: + rand_state = np.random + # Checking init and initializing the LDS + if isinstance(init, str): + if init == 'ran': + X_lds = (10.0 ** (-4)) * rand_state.randn(N, dim_lds) + elif init == 'pca': + if isinstance(hd_metric, str) and (hd_metric == "precomputed"): + raise ValueError( + "Error in function catsne: init cannot be set to 'pca' when hd_metric is set to 'precomputed'.") + X_lds = sklearn.decomposition.PCA(n_components=dim_lds, copy=True, random_state=rand_state).fit_transform( + X_hds) + else: + raise ValueError( + "Error in function catsne: init={init} whereas it must either be equal to 'ran' or to 'pca'.".format( + init=init)) + else: + # init must be a 2-D numpy array with N rows and dim_lds columns + if init.ndim != 2: + raise ValueError( + "Error in function catsne: init.ndim={v} whereas init must be a 2-D numpy array.".format(v=init.ndim)) + if init.shape[0] != N: + raise ValueError( + "Error in function catsne: init.shape[0]={v} whereas it must equal N={N}.".format(v=init.shape[0], N=N)) + if init.shape[1] != dim_lds: + raise ValueError( + "Error in function catsne: init.shape[1]={v} whereas it must equal dim_lds={dim_lds}.".format( + v=init.shape[1], dim_lds=dim_lds)) + X_lds = init + # Computing the squared HD distances + if isinstance(hd_metric, str): + if hd_metric == "precomputed": + ds_hd = X_hds ** 2.0 + else: + if D_hd_metric is None: + D_hd_metric = {} + ds_hd = scipy.spatial.distance.squareform( + X=scipy.spatial.distance.pdist(X=X_hds, metric=hd_metric, **D_hd_metric), force='tomatrix') ** 2.0 + else: + # hd_metric is a callable + ds_hd = np.empty(shape=(N, N), dtype=np.float64) + for i in range(N): + ds_hd[i, i] = 0.0 + for j in range(i): + ds_hd[i, j] = hd_metric(X_hds[i, :], X_hds[j, :]) ** 2.0 + ds_hd[j, i] = ds_hd[i, j] + # Small float + n_eps = np.finfo(dtype=np.float64).eps + # Performing momentum gradient descent and returning + return catsne_mgd(ds_hd=ds_hd, labels=labels, theta=theta, n_eps=n_eps, eei=eei, eef=eef, X_lds=X_lds, ftol=ftol, + N=N, dim_lds=dim_lds, mom_t=mom_t, nit_max=nit_max, gtol=gtol, mom_init=mom_init, mom_fin=mom_fin) + + +############################## +############################## +# Unsupervised DR quality assessment: rank-based criteria measuring the HD neighborhood preservation in the LDS [2, 3]. +# The main function which should be used is 'eval_dr_quality'. +# See its documentation for details. It explains the meaning of the quality criteria and how to interpret them. +# The demo at the end of this file presents how to use eval_dr_quality function. +############################## + +def coranking(d_hd, d_ld): + """ + Computation of the co-ranking matrix, as described in [3]. + The time complexity of this function is O(N**2 log(N)), where N is the number of data points. + In: + - d_hd: 2-D numpy array representing the redundant matrix of pairwise distances in the HDS. + - d_ld: 2-D numpy array representing the redundant matrix of pairwise distances in the LDS. + Out: + The (N-1)x(N-1) co-ranking matrix, where N = d_hd.shape[0]. + """ + # Computing the permutations to sort the rows of the distance matrices in HDS and LDS. + perm_hd = d_hd.argsort(axis=-1, kind='mergesort') + perm_ld = d_ld.argsort(axis=-1, kind='mergesort') + + N = d_hd.shape[0] + i = np.arange(N, dtype=np.int64) + # Computing the ranks in the LDS + R = np.empty(shape=(N, N), dtype=np.int64) + for j in range(N): + R[perm_ld[j, i], j] = i + # Computing the co-ranking matrix + Q = np.zeros(shape=(N, N), dtype=np.int64) + for j in range(N): + Q[i, R[perm_hd[j, i], j]] += 1 + # Returning + return Q[1:, 1:] + + +@numba.jit(nopython=True) +def eval_auc(arr): + """ + Evaluates the AUC, as defined in [5]. + In: + - arr: 1-D numpy array storing the values of a curve from K=1 to arr.size. + Out: + The AUC under arr, as defined in [5], with a log scale for K=1 to arr.size. + """ + i_all_k = 1.0 / (np.arange(arr.size) + 1.0) + return np.float64(arr.dot(i_all_k)) / (i_all_k.sum()) + + +@numba.jit(nopython=True) +def eval_rnx(Q): + """ + Evaluate R_NX(K) for K = 1 to N-2, as defined in [4]. N is the number of data points in the data set. + The time complexity of this function is O(N^2). + In: + - Q: a 2-D numpy array representing the (N-1)x(N-1) co-ranking matrix of the embedding. + Out: + A 1-D numpy array with N-2 elements. Element i contains R_NX(i+1). + """ + N_1 = Q.shape[0] + N = N_1 + 1 + # Computing Q_NX + qnxk = np.empty(shape=N_1, dtype=np.float64) + acc_q = 0.0 + for K in range(N_1): + acc_q += (Q[K, K] + np.sum(Q[K, :K]) + np.sum(Q[:K, K])) + qnxk[K] = acc_q / ((K + 1) * N) + # Computing R_NX + arr_K = np.arange(N_1)[1:].astype(np.float64) + rnxk = (N_1 * qnxk[:N_1 - 1] - arr_K) / (N_1 - arr_K) + # Returning + return rnxk + + +def eval_dr_quality(d_hd, d_ld): + """ + Compute the DR quality assessment criteria R_{NX}(K) and AUC, as defined in [2, 3, 4, 5]. + These criteria measure the neighborhood preservation around the data points from the HDS to the LDS. + Based on the HD and LD distances, the sets v_i^K (resp. n_i^K) of the K nearest neighbors of data point i in the HDS (resp. LDS) can first be computed. + Their average normalized agreement develops as Q_{NX}(K) = (1/N) * \sum_{i=1}^{N} |v_i^K \cap n_i^K|/K, where N refers to the number of data points and \cap to the set intersection operator. + Q_{NX}(K) ranges between 0 and 1; the closer to 1, the better. + As the expectation of Q_{NX}(K) with random LD coordinates is equal to K/(N-1), which is increasing with K, R_{NX}(K) = ((N-1)*Q_{NX}(K)-K)/(N-1-K) enables more easily comparing different neighborhood sizes K. + R_{NX}(K) ranges between -1 and 1, but a negative value indicates that the embedding performs worse than random. Therefore, R_{NX}(K) typically lies between 0 and 1. + The R_{NX}(K) values for K=1 to N-2 can be displayed as a curve with a log scale for K, as closer neighbors typically prevail. + The area under the resulting curve (AUC) is a scalar score which grows with DR quality, quantified at all scales with an emphasis on small ones. + The AUC lies between -1 and 1, but a negative value implies performances which are worse than random. + In: + - d_hd: 2-D numpy array of floats with shape (N, N), representing the redundant matrix of pairwise distances in the HDS. + - d_ld: 2-D numpy array of floats with shape (N, N), representing the redundant matrix of pairwise distances in the LDS. + Out: a tuple with + - a 1-D numpy array with N-2 elements. Element i contains R_{NX}(i+1). + - the AUC of the R_{NX}(K) curve with a log scale for K, as defined in [5]. + Remark: + - The time complexity to evaluate the quality criteria is O(N**2 log(N)). It is the time complexity to compute the co-ranking matrix. R_{NX}(K) can then be evaluated for all K=1, ..., N-2 in O(N**2). + """ + # Computing the co-ranking matrix of the embedding, and the R_{NX}(K) curve. + rnxk = eval_rnx(Q=coranking(d_hd=d_hd, d_ld=d_ld)) + # Computing the AUC, and returning. + return rnxk, eval_auc(rnxk) + + +############################## +############################## +# Supervised DR quality assessment: accuracy of a KNN classifier in the LDS. +# The main function which should be used is 'knngain'. +# See its documentation for details. It explains the meaning of the quality criteria and how to interpret them. +# The demo at the end of this file presents how to use knngain function. +############################## + +@numba.jit(nopython=True) +def knngain(d_hd, d_ld, labels): + """ + Compute the KNN gain curve and its AUC, as defined in [1]. + If c_i refers to the class label of data point i, v_i^K (resp. n_i^K) to the set of the K nearest neighbors of data point i in the HDS (resp. LDS), and N to the number of data points, the KNN gain develops as G_{NN}(K) = (1/N) * \sum_{i=1}^{N} (|{j \in n_i^K such that c_i=c_j}|-|{j \in v_i^K such that c_i=c_j}|)/K. + It averages the gain (or loss, if negative) of neighbors of the same class around each point, after DR. + Hence, a positive value correlates with likely improved KNN classification performances. + As the R_{NX}(K) curve from the unsupervised DR quality assessment, the KNN gain G_{NN}(K) can be displayed with respect to K, with a log scale for K. + A global score summarizing the resulting curve is provided by its area (AUC). + In: + - d_hd: 2-D numpy array of floats with shape (N, N), representing the redundant matrix of pairwise distances in the HDS. + - d_ld: 2-D numpy array of floats with shape (N, N), representing the redundant matrix of pairwise distances in the LDS. + - labels: 1-D numpy array with N elements, containing integers indicating the class labels of the data points. + Out: + A tuple with: + - a 1-D numpy array of floats with N-1 elements, storing the KNN gain for K=1 to N-1. + - the AUC of the KNN gain curve, with a log scale for K. + """ + # Number of data points + N = d_hd.shape[0] + N_1 = N - 1 + k_hd = np.zeros(shape=N_1, dtype=np.int64) + k_ld = np.zeros(shape=N_1, dtype=np.int64) + # For each data point + for i in range(N): + c_i = labels[i] + di_hd = d_hd[i, :].argsort(kind='mergesort') + di_ld = d_ld[i, :].argsort(kind='mergesort') + # Making sure that i is first in di_hd and di_ld + for arr in [di_hd, di_ld]: + for idj, j in enumerate(arr): + if j == i: + idi = idj + break + if idi != 0: + arr[idi] = arr[0] + arr = arr[1:] + for k in range(N_1): + if c_i == labels[di_hd[k]]: + k_hd[k] += 1 + if c_i == labels[di_ld[k]]: + k_ld[k] += 1 + # Computing the KNN gain + gn = (k_ld.cumsum() - k_hd.cumsum()).astype(np.float64) / ((1.0 + np.arange(N_1)) * N) + # Returning the KNN gain and its AUC + return gn, eval_auc(gn) + + +############################## +############################## +# Plot functions. +# Their documentations detail their parameters. +# The demo at the end of this file presents how to use these functions. +############################## + +def viz_digits(X, lab, tit='', cmap='gnuplot2', stit=30, slab=15, wlab='bold', max_ti=None): + """ + Visualize a 2-D embedding of digits data set. + In: + - X: a numpy array with shape (N, 2), where N is the number of data points in the data set. + - lab: a 1-D numpy array with N elements indicating the digits. + - tit: title of the figure. + - cmap: colormap for the digits. + - stit: fontsize of the title of the figure. + - slab: fontsize of the digits. + - wlab: weight to plot the digits. + - max_ti: 2nd element in the tuple returned by catsne. It changes the size of the data points proportionally to the probability mass of their neighbors with the same class in the HDS. If None, it is set to np.ones(shape=N, dtype=np.int64), meaning that all data points have equal size. + Out: + A figure is shown. + """ + # Checking X + if len(X.shape) != 2: + raise ValueError( + "Error in function viz_digits: X must be a numpy array with shape (N, 2), where N is the number of data points.") + if X.shape[1] != 2: + raise ValueError("Error in function viz_digits: X must have 2 columns.") + + N = X.shape[0] + if max_ti is None: + max_ti = np.ones(shape=N, dtype=np.int64) + + # Computing the limits of the axes + xmin = X[:, 0].min() + xmax = X[:, 0].max() + expand_value = (xmax - xmin) * 0.05 + x_lim = np.asarray([xmin - expand_value, xmax + expand_value]) + + ymin = X[:, 1].min() + ymax = X[:, 1].max() + expand_value = (ymax - ymin) * 0.05 + y_lim = np.asarray([ymin - expand_value, ymax + expand_value]) + + fig = plt.figure() + ax = fig.add_subplot(111) + + # Setting the limits of the axes + ax.set_xlim(x_lim) + ax.set_ylim(y_lim) + + # Visualizing the digits + cmap_obj = matplotlib.cm.get_cmap(cmap) + normalizer = matplotlib.colors.Normalize(vmin=lab.min(), vmax=lab.max()) + for i in range(N): + ax.text(x=X[i, 0], y=X[i, 1], s=str(lab[i]), fontsize=slab - 10.0 * (1.0 - max_ti[i]), + color=cmap_obj(normalizer(lab[i])), fontdict={'weight': wlab, 'size': slab - 10.0 * (1.0 - max_ti[i])}, + horizontalalignment='center', verticalalignment='center') + + # Removing the ticks on the x- and y-axes + ax.set_xticks([], minor=False) + ax.set_xticks([], minor=True) + ax.set_xticklabels([], minor=False) + ax.set_yticks([], minor=False) + ax.set_yticks([], minor=True) + ax.set_yticklabels([], minor=False) + + ax.set_title(tit, fontsize=stit) + plt.tight_layout() + + # Showing the figure + plt.show() + plt.close() + + +def viz_qa(Ly, ymin=None, ymax=None, Lmarkers=None, Lcols=None, Lleg=None, Lls=None, Lmedw=None, Lsdots=None, lw=2, + markevery=0.1, tit='', xlabel='', ylabel='', alpha_plot=0.9, alpha_leg=0.8, stit=25, sax=20, sleg=15, zleg=1, + loc_leg='best', ncol_leg=1, lMticks=10, lmticks=5, wMticks=2, wmticks=1, nyMticks=11, mymticks=4, grid=True, + grid_ls='solid', grid_col='lightgrey', grid_alpha=0.7, xlog=True): + """ + Plot the DR quality criteria curves. + In: + - Ly: list of 1-D numpy arrays. The i^th array gathers the y-axis values of a curve from x=1 to x=Ly[i].size, with steps of 1. + - ymin, ymax: minimum and maximum values of the y-axis. If None, ymin (resp. ymax) is set to the smallest (resp. greatest) value among [y.min() for y in Ly] (resp. [y.max() for y in Ly]). + - Lmarkers: list with the markers for each curve. If None, some pre-defined markers are used. + - Lcols: list with the colors of the curves. If None, some pre-defined colors are used. + - Lleg: list of strings, containing the legend entries for each curve. If None, no legend is shown. + - Lls: list of the linestyles ('solid', 'dashed', ...) of the curves. If None, 'solid' style is employed for all curves. + - Lmedw: list with the markeredgewidths of the curves. If None, some pre-defined value is employed. + - Lsdots: list with the sizes of the markers. If None, some pre-defined value is employed. + - lw: linewidth for all the curves. + - markevery: approximately 1/markevery markers are displayed for each curve. Set to None to mark every dot. + - tit: title of the plot. + - xlabel, ylabel: labels for the x- and y-axes. + - alpha_plot: alpha for the curves. + - alpha_leg: alpha for the legend. + - stit: fontsize for the title. + - sax: fontsize for the labels of the axes. + - sleg: fontsize for the legend. + - zleg: zorder for the legend. Set to 1 to plot the legend behind the data, and to None to keep the default value. + - loc_leg: location of the legend ('best', 'upper left', ...). + - ncol_leg: number of columns to use in the legend. + - lMticks: length of the major ticks on the axes. + - lmticks: length of the minor ticks on the axes. + - wMticks: width of the major ticks on the axes. + - wmticks: width of the minor ticks on the axes. + - nyMticks: number of major ticks on the y-axis (counting ymin and ymax). + - mymticks: there are 1+mymticks*(nyMticks-1) minor ticks on the y axis. + - grid: True to add a grid, False otherwise. + - grid_ls: linestyle of the grid. + - grid_col: color of the grid. + - grid_alpha: alpha of the grid. + - xlog: True to produce a semilogx plot and False to produce a plot. + Out: + A figure is shown. + """ + # Number of curves + nc = len(Ly) + # Checking the parameters + if ymin is None: + ymin = np.min(np.asarray([arr.min() for arr in Ly])) + if ymax is None: + ymax = np.max(np.asarray([arr.max() for arr in Ly])) + if Lmarkers is None: + Lmarkers = ['x'] * nc + if Lcols is None: + Lcols = ['blue'] * nc + if Lleg is None: + Lleg = [None] * nc + add_leg = False + else: + add_leg = True + if Lls is None: + Lls = ['solid'] * nc + if Lmedw is None: + Lmedw = [float(lw) / 2.0] * nc + if Lsdots is None: + Lsdots = [12] * nc + + # Setting the limits of the y-axis + y_lim = [ymin, ymax] + + # Defining the ticks on the y-axis + yMticks = np.linspace(start=ymin, stop=ymax, num=nyMticks, endpoint=True, retstep=False) + ymticks = np.linspace(start=ymin, stop=ymax, num=1 + mymticks * (nyMticks - 1), endpoint=True, retstep=False) + yMticksLab = [int(round(v * 100.0)) / 100.0 for v in yMticks] + + # Initial values for xmin and xmax + xmin, xmax = 1, -np.inf + + fig = plt.figure() + ax = fig.add_subplot(111) + if xlog: + fplot = ax.semilogx + else: + fplot = ax.plot + + # Plotting the data + for id, y in enumerate(Ly): + x = np.arange(start=1, step=1, stop=y.size + 0.5, dtype=np.int64) + xmax = max(xmax, x[-1]) + fplot(x, y, label=Lleg[id], alpha=alpha_plot, color=Lcols[id], linestyle=Lls[id], lw=lw, marker=Lmarkers[id], + markeredgecolor=Lcols[id], markeredgewidth=Lmedw[id], markersize=Lsdots[id], dash_capstyle='round', + solid_capstyle='round', dash_joinstyle='round', solid_joinstyle='round', markerfacecolor=Lcols[id], + markevery=markevery) + + # Setting the limits of the axes + ax.set_xlim([xmin, xmax]) + ax.set_ylim(y_lim) + + # Setting the major and minor ticks on the y-axis + ax.set_yticks(yMticks, minor=False) + ax.set_yticks(ymticks, minor=True) + ax.set_yticklabels(yMticksLab, minor=False, fontsize=sax) + + # Defining the legend + if add_leg: + leg = ax.legend(loc=loc_leg, fontsize=sleg, markerfirst=True, fancybox=True, framealpha=alpha_leg, + ncol=ncol_leg) + if zleg is not None: + leg.set_zorder(zleg) + + # Setting the size of the ticks labels on the x axis + for tick in ax.xaxis.get_major_ticks(): + tick.label.set_fontsize(sax) + + # Setting ticks length and width + ax.tick_params(axis='both', length=lMticks, width=wMticks, which='major') + ax.tick_params(axis='both', length=lmticks, width=wmticks, which='minor') + + # Setting the positions of the labels + ax.xaxis.set_tick_params(labelright=False, labelleft=True) + ax.yaxis.set_tick_params(labelright=False, labelleft=True) + + # Adding the grids + if grid: + ax.xaxis.grid(True, linestyle=grid_ls, which='major', color=grid_col, alpha=grid_alpha) + ax.yaxis.grid(True, linestyle=grid_ls, which='major', color=grid_col, alpha=grid_alpha) + ax.set_axisbelow(True) + + ax.set_title(tit, fontsize=stit) + ax.set_xlabel(xlabel, fontsize=sax) + ax.set_ylabel(ylabel, fontsize=sax) + plt.tight_layout() + + # Showing the figure + plt.show() + plt.close() + + +############################## +############################## +# Demo presenting how to use the main functions of this module. +############################## + +if __name__ == '__main__': + print("========================================================================") + print("===== Starting the demo of cat-SNE and of the DR quality criteria. =====") + print("========================================================================") + + # Metric to compute the HD distances in cat-SNE. + hd_metric = 'euclidean' + # Maximum number of iterations for cat-SNE. + nit_max = 1000 + # Dimension of the LDS. + dim_lds = 2 + # Random initialization of the LDS. + init = 'ran' + + # List of the theta thresholds to employ with cat-SNE. + Ltheta = [0.7, 0.8, 0.9] + # Lists to provide as parameters to viz_qa, to visualize the DR quality criteria and the KNN gain. + L_rnx, L_kg = [], [] + Lmarkers = ['x', 'o', 's'] + Lcols = ['green', 'red', 'blue'] + Lleg_rnx, Lleg_kg = [], [] + Lls = [] + Lmedw = [1.5, 1.0, 1.0] + Lsdots = [14, 12, 12] + + print("Loading the digits data set.") + X_hds, labels = sklearn.datasets.load_digits(n_class=10, return_X_y=True) + # Subsampling the data set, to accelerate the demo. + rand_state = np.random.RandomState(0) + id_subs = rand_state.choice(a=X_hds.shape[0], size=1000, replace=False) + X_hds, labels = X_hds[id_subs, :], labels[id_subs] + # Computing the pairwise HD distances for the quality assessment. + d_hd = scipy.spatial.distance.squareform(X=scipy.spatial.distance.pdist(X=X_hds, metric=hd_metric), + force='tomatrix') + + # For each theta + for theta in Ltheta: + print("Applying cat-SNE with threshold theta = {theta}".format(theta=theta)) + X_lds, max_ti = catsne(X_hds=X_hds, labels=labels, theta=theta, init=init, dim_lds=dim_lds, nit_max=nit_max, + rand_state=np.random.RandomState(0), hd_metric=hd_metric) + # Computing the pairwise distances in the LDS for the quality assessment. + d_ld = scipy.spatial.distance.squareform(X=scipy.spatial.distance.pdist(X=X_lds, metric='euclidean'), + force='tomatrix') + # Displaying the LD embedding + viz_digits(X=X_lds, lab=labels, tit='cat-SNE ($\\theta$={theta})'.format(theta=theta), max_ti=max_ti) + print("Computing the DR quality of the result of cat-SNE with threshold theta = {theta}".format(theta=theta)) + rnxk, auc_rnx = eval_dr_quality(d_hd=d_hd, d_ld=d_ld) + print("Computing the KNN gain of the result of cat-SNE with threshold theta = {theta}".format(theta=theta)) + kg, auc_kg = knngain(d_hd=d_hd, d_ld=d_ld, labels=labels) + # Updating the lists for viz_qa + L_rnx.append(rnxk) + L_kg.append(kg) + Lleg_rnx.append("{a} cat-SNE ($\\theta={theta}$)".format(a=int(round(auc_rnx * 1000)) / 1000.0, theta=theta)) + Lleg_kg.append("{a} cat-SNE ($\\theta={theta}$)".format(a=int(round(auc_kg * 1000)) / 1000.0, theta=theta)) + Lls.append('solid') + + # Displaying the DR quality criteria + viz_qa(Ly=L_rnx, Lmarkers=Lmarkers, Lcols=Lcols, Lleg=Lleg_rnx, Lls=Lls, Lmedw=Lmedw, Lsdots=Lsdots, + tit='DR quality', xlabel='Neighborhood size $K$', ylabel='$R_{NX}(K)$') + # Displaying the KNN gain + viz_qa(Ly=L_kg, Lmarkers=Lmarkers, Lcols=Lcols, Lleg=Lleg_kg, Lls=Lls, Lmedw=Lmedw, Lsdots=Lsdots, tit='KNN gain', + xlabel='Neighborhood size $K$', ylabel='$G_{NN}(K)$') \ No newline at end of file diff --git a/backend/src/alg/data.py b/backend/src/alg/data.py new file mode 100644 index 0000000..2cde097 --- /dev/null +++ b/backend/src/alg/data.py @@ -0,0 +1,98 @@ +import json +import os +from copy import deepcopy + +from src.alg.limits import contexts_list + + +def decompress(compressed_game_data): + pre_rec = { + "tick": 0, + "game_time": 0, + "roshan_hp": 0, + "is_night": False, + "events": [], + "heroStates": [[], []], + "teamStates": [], + } + + def update_frame(gr): + pre_rec["tick"] = gr["tick"] + pre_rec["game_time"] = gr["game_time"] + if gr.has_key("is_night"): + pre_rec["is_night"] = gr["is_night"] + if gr.has_key("roshan_hp"): + pre_rec["roshan_hp"] = gr["roshan_hp"] + pre_rec["events"] = gr["events"] + for i in range(2): + for j in range(5): + pre_rec["heroStates"][i][j] = { + **pre_rec["heroStates"][i][j], + **gr["heroStates"][i][j] + } + pre_rec["teamStates"][i] = { + **pre_rec["teamStates"][i], + **gr["teamStates"][i] + } + return deepcopy(pre_rec) + + return { + "gameInfo": compressed_game_data["gameInfo"], + "gameRecords": [update_frame(rec) for rec in compressed_game_data["gameRecords"]] + } + + +def load_match(filepath): + with open(filepath, encoding='utf-8') as f: + data = json.load(f) + return decompress(data) + + +def gen_inst(match, team_id, player_id, frame, lc=10, lx=30, ly=10): + return { + "c": [ + match['gameRecords'][i] + for i in range(frame - lc + 1, frame + 1) + ], + "tx": [ + [match['gameRecords'][i]["heroStates"][tid][pid]['position'] for tid in range(2) for pid in range(5)] + for i in range(frame - lx + 1, frame + 1) + ], + "ty": [ + match['gameRecords'][i]["heroStates"][team_id][player_id]['position'] + for i in range(frame + 1, frame + ly + 1) + ], + } + + +def search_inst(folder, filename, team_id, player_id, frame): + match = load_match(os.path.join(folder, filename)) + return gen_inst(match, team_id, player_id, frame) + + +def is_similar_inst(inst1, inst2, context_limits): + limits = set() + for limit in context_limits: + ctx_group = limit['ctxGroup'] + ctx_item = limit['ctxItem'] + limits.add(f"{ctx_group}||{ctx_item}") + + for cg_name in contexts_list: + cg = context_limits[cg_name] + for ci_name in cg: + ci = cg[ci_name] + diff = ci['diff'](inst1, inst2) + if diff > ci['limits'][int(f"{cg_name}||{ci_name}" in limits)]: + return False + return True + + +def search_similar_inst(folder, target_inst, team_id, player_id, context_limits): + res = [] + for game in os.listdir(folder): + match = load_match(os.path.join(folder, game)) + for frame in range(len(match['gameRecords'])): + inst = gen_inst(match, team_id, player_id, frame) + if is_similar_inst(inst, target_inst, context_limits): + res.append(inst) + return res diff --git a/backend/src/alg/interpret.py b/backend/src/alg/interpret.py new file mode 100644 index 0000000..b6481fb --- /dev/null +++ b/backend/src/alg/interpret.py @@ -0,0 +1,42 @@ +from sklearn.cluster import AffinityPropagation +import numpy as np + +from src.alg.catsne import catsne + + +def aggregate_workers(feature_vectors): + feature_vectors = np.array(feature_vectors) + clustering = AffinityPropagation(random_state=42).fit(feature_vectors) + labels = clustering.labels_.to_list() + pred_groups = [[] for _ in range(max(labels) + 1)] + for i, g in enumerate(labels): + pred_groups[g].append(i) + return pred_groups + + +def proj(groups, feature_vectors): + labels = [0 for i in range(20)] + for gid, group in enumerate(groups): + for i in group: + labels[i] = gid + pos, _ = catsne(feature_vectors, labels) + return pos + + +def extend_stage(stages, pred_groups): + g_key = lambda gs: '|'.join([','.join(g) for g in gs]) + new_stage_flag = True + for group in pred_groups: + group.sort() + pred_groups.sort(key=lambda x: x[0]) + group_key = g_key(pred_groups[0]) + for stage in stages: + if group_key == g_key(stage['groups']): + stage['instances'] += 1 + new_stage_flag = False + break + if new_stage_flag: + stages.append({ + 'groups': pred_groups, + "instances": 1, + }) diff --git a/backend/src/alg/limits.py b/backend/src/alg/limits.py new file mode 100644 index 0000000..577597c --- /dev/null +++ b/backend/src/alg/limits.py @@ -0,0 +1,108 @@ +import math + + +def dis(pos_a, pos_b): + dx = pos_a[0] - pos_b[0] + dy = pos_a[1] - pos_b[1] + return math.sqrt(dx * dx + dy * dy) + + +def tower_limit(tid, tower_id): + return { + "diff": lambda a, b: abs( + a['c'][-1]['teamStates'][tid]['towers'][tower_id] + - b['c'][-1]['teamStates'][tid]['towers'][tower_id] + ), + "limits": [2500, 500], + } + + +def creep_limit(tid, creep_id): + return { + "diff": lambda a, b: dis( + a['c'][-1]['teamStates'][tid]['creeps'][creep_id], + b['c'][-1]['teamStates'][tid]['creeps'][creep_id] + ), + "limits": [30000, 2000], + } + + +def team_limits(tid): + return { + "towerTop1": tower_limit(tid, 0), + "towerTop2": tower_limit(tid, 1), + "towerTop3": tower_limit(tid, 2), + "towerMid1": tower_limit(tid, 3), + "towerMid2": tower_limit(tid, 4), + "towerMid3": tower_limit(tid, 5), + "towerBot1": tower_limit(tid, 6), + "towerBot2": tower_limit(tid, 7), + "towerBot3": tower_limit(tid, 8), + "towerBase1": tower_limit(tid, 9), + "towerBase2": tower_limit(tid, 10), + "creepTop": creep_limit(tid, 0), + "creepMid": creep_limit(tid, 1), + "creepBot": creep_limit(tid, 2), + } + + +def player_limits(tid, pid): + def ps(inst): + return inst['c'][-1]['heroStates'][tid][pid] + + return { + "health": { + "diff": lambda x, y: abs(ps(x)['hp'] - ps(y)['hp']), + 'limits': [2000, 500], + }, + "mana": { + "diff": lambda x, y: abs(ps(x)['mp'] - ps(y)['mp']), + 'limits': [2000, 300], + }, + "position": { + "diff": lambda x, y: dis(ps(x)['pos'], ps(y)['pos']), + 'limits': [5000, 1500], + }, + "level": { + "diff": lambda x, y: abs(ps(x)['lvl'] - ps(y)['lvl']), + 'limits': [5, 2], + }, + "isAlive": { + "diff": lambda x, y: abs(int(ps(x)['ls']) - int(ps(y)['ls'])), + 'limits': [1, 0], + }, + "gold": { + "diff": lambda x, y: abs(ps(x)['gold']) - int(ps(y)['gold']), + 'limits': [4000, 2000], + }, + } + + +contexts_list = { + 't0': team_limits, + 't1': team_limits, + 'p00': player_limits(0, 0), + 'p01': player_limits(0, 1), + 'p02': player_limits(0, 2), + 'p03': player_limits(0, 3), + 'p04': player_limits(0, 4), + 'p10': player_limits(1, 0), + 'p11': player_limits(1, 1), + 'p12': player_limits(1, 2), + 'p13': player_limits(1, 3), + 'p14': player_limits(1, 4), + 'g': { + "gameTime": { + "diff": lambda x, y: abs(x['c'][-1]['gameTime'] - y['c'][-1]['gameTime']), + 'limits': [600, 120], + }, + "isNight": { + "diff": lambda x, y: abs(int(x['c'][-1]['isNight']) - int(y['c'][-1]['isNight'])), + 'limits': [1, 0], + }, + "roshanHP": { + "diff": lambda x, y: abs(x['c'][-1]['roshanHP'] - y['c'][-1]['roshanHP']), + 'limits': [5000, 1000], + }, + } +} diff --git a/backend/src/model/mng.py b/backend/src/model/mng.py new file mode 100644 index 0000000..9ac0791 --- /dev/null +++ b/backend/src/model/mng.py @@ -0,0 +1,64 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.nn.modules.transformer import TransformerEncoder, TransformerEncoderLayer, TransformerDecoder, TransformerDecoderLayer +import numpy as np + +class Manager(nn.Module): + def __init__(self, in_dim, out_dim, ninp=32, nhead=8, nhid=32, nlayers=6, dropout=0.2): + super(Manager, self).__init__() + self.model_type = 'Manager' + self.src_mask = None + encoder_layers = TransformerEncoderLayer(ninp, nhead, nhid, dropout) + self.transformer_encoder = TransformerEncoder(encoder_layers, nlayers) + self.encoder = nn.Linear(in_dim, ninp) + self.ninp = ninp + self.in_dim = in_dim + self.out_dim = out_dim + self.decoder = nn.Linear(ninp, out_dim) + self.att_map = np.array(out_dim, ninp) + + # self.init_weights() + + def _generate_square_subsequent_mask(self, sz): + mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1) + mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0)) + return mask + + def init_weights(self): + # initrange = 0.1 + # nn.init.uniform_(self.encoder.weight, -initrange, initrange) + # nn.init.zeros_(self.decoder.bias) + # nn.init.uniform_(self.decoder.weight, -initrange, initrange) + nn.init.xavier_uniform_(self.encoder.weight) + nn.init.zeros_(self.encoder.bias) + nn.init.xavier_uniform_(self.decoder.weight) + nn.init.zeros_(self.decoder.bias) + + def forward(self, src, has_mask=True): + if has_mask: + device = src.device + if self.src_mask is None or self.src_mask.size(0) != len(src): + mask = self._generate_square_subsequent_mask(len(src)).to(device) + self.src_mask = mask + else: + self.src_mask = None + + output = self.encoder(src) + # output = self.var_embedding(output) + # output = self.pos_encoder(output) + output = self.transformer_encoder(output, self.src_mask) + output = self.decoder(output) + output = F.softmax(output, dim=-1) + + temp = np.diag(np.ones(self.in_dim)) + for block in self.blocks: + qkv = block.qkv(src).reshape() + q, k, v = qkv.unbind(0) + attn = (q @ k.transpose(-2, -1)) * self.scale + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + temp = np.matmul(attn, temp) + self.att_map = temp + + return output \ No newline at end of file diff --git a/backend/src/model/model.py b/backend/src/model/model.py new file mode 100644 index 0000000..673f64e --- /dev/null +++ b/backend/src/model/model.py @@ -0,0 +1,37 @@ +import json +import os + +import torch + +from src.model.mng import Manager +from src.model.worker import Worker + + +class Model: + @staticmethod + def load(model_folder, M=283, lc=10, lx=30, ly=10): + with open(os.path.join(model_folder, "config.json")) as f: + config = json.load(f) + mng = Manager(in_dim=M * lc, out_dim=20) + mng.load_state_dict(torch.load(os.path.join(model_folder, "mng.pt"))) + workers = [] + for i in range(config['num_workers']): + worker = Worker(in_dim=lx, out_dim=ly) + worker.load_state_dict(torch.load(os.path.join(model_folder, "w{i}.pt"))) + workers.append(worker) + model = Model(mng, workers) + return model + + def __init__(self, mng, workers): + self.K = len(workers) + self.mng = mng + self.workers = workers + + def predict(self, inst): + probs = self.mng(inst['c']) + return [{ + "idx": i, + "trajectory": self.workers[i](inst['tx']), + "probability": probs[i], + "attention": self.mng.att_map[i], + } for i in range(self.K)] diff --git a/backend/src/model/worker.py b/backend/src/model/worker.py new file mode 100644 index 0000000..173f7dd --- /dev/null +++ b/backend/src/model/worker.py @@ -0,0 +1,149 @@ +import math +import torch +import torch.nn as nn +from torch.nn.modules.transformer import TransformerEncoder, TransformerEncoderLayer, TransformerDecoder, TransformerDecoderLayer + + +class PositionalEncoding(nn.Module): + def __init__(self, d_model, dropout=0.1, max_len=5000): + super(PositionalEncoding, self).__init__() + self.dropout = nn.Dropout(p=dropout) + + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0).transpose(0, 1) + self.register_buffer('pe', pe) + + def forward(self, x): + x = x + self.pe[:x.size(0), :] + return self.dropout(x) + + +class Worker(nn.Module): + def __init__(self, in_dim, out_dim, ninp=32, nhead=8, nhid=32, nlayers=6, dropout=0.2): + super(Worker, self).__init__() + self.model_type = 'TICA' + self.ninp = ninp + self.encoder_input_layer = nn.Linear(in_dim, ninp) + self.decoder_input_layer = nn.Linear(out_dim, ninp) + self.linear_mapping = nn.Linear(ninp, out_dim) + # self.normalize_layer = nn.Hardtanh(min_val=0, max_val=1) + self.normalize_layer = nn.Sigmoid() + self.pos_encoder = PositionalEncoding(ninp, dropout) + encoder_layer = TransformerEncoderLayer(ninp, nhead, nhid, dropout) + self.transformer_encoder = TransformerEncoder(encoder_layer, nlayers) + decoder_layer = TransformerDecoderLayer(ninp, nhead, nhid, dropout) + self.transformer_decoder = TransformerDecoder(decoder_layer, nlayers) + + self.src_mask = None + self.tgt_mask = None + + if torch.cuda.is_available(): + self.device = torch.device('cuda') + else: + self.device = torch.device('cpu') + + self.min_coord = torch.Tensor([8240, 8220]).to(self.device) + self.max_coord = torch.Tensor([24510, 24450]).to(self.device) + + self.init_weights() + + # def _generate_square_subsequent_mask(self, sz): + # mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1) + # mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0)) + # return mask + + def generate_square_subsequent_mask(self, dim1, dim2): + return torch.triu(torch.ones(dim1, dim2) * float('-inf'), diagonal=1) + + def init_weights(self): + # initrange = 0.1 + # nn.init.uniform_(self.encoder.weight, -initrange, initrange) + # nn.init.zeros_(self.decoder.bias) + # nn.init.uniform_(self.decoder.weight, -initrange, initrange) + nn.init.xavier_uniform_(self.encoder_input_layer.weight) + nn.init.zeros_(self.encoder_input_layer.bias) + nn.init.xavier_uniform_(self.decoder_input_layer.weight) + nn.init.zeros_(self.decoder_input_layer.bias) + nn.init.xavier_uniform_(self.linear_mapping.weight) + nn.init.zeros_(self.linear_mapping.bias) + + def tanh(self, z): + return (torch.exp(z) - torch.exp(-z)) / (torch.exp(z) + torch.exp(-z)) + + def normalize_coord(self, coord): + norm_coord = torch.zeros_like(coord).to(self.device) + norm_coord[:, :, 0::2] = (coord[:, :, 0::2] - self.min_coord[0]) / (self.max_coord[0] - self.min_coord[0]) + norm_coord[:, :, 1::2] = (coord[:, :, 1::2] - self.min_coord[1]) / (self.max_coord[1] - self.min_coord[1]) + return norm_coord + + def restore_coord(self, coord): + res_coord = coord * (self.max_coord - self.min_coord) + self.min_coord + return res_coord + + def cal_pos(self, output, src, tgt, role): + idx = (int(role) // 10) * 5 + int(role) % 10 - 6 + if tgt is None: + pos = src[-1, :, idx * 2:idx * 2 + 2].unsqueeze(0) + else: + pos = torch.cat((src[-1, :, idx * 2:idx * 2 + 2].unsqueeze(0), tgt[:-1]), dim=0) + # pos = [] + # for i in range(output.shape[0]): + # if i == 0: + # pos.append(src[-1, :, idx*2:idx*2+2]) + # else: + # pos.append(tgt[i-1, :, :]) + # dpos = torch.stack((output[:, :, 1] * 500 * torch.cos(torch.deg2rad(output[:, :, 0] * 360)), \ + # output[:, :, 1] * 500 * torch.sin(torch.deg2rad(output[:, :, 0] * 360))), \ + # dim=-1).to(self.device) + dpos = torch.stack((500 * output[:, :, 0], 500 * output[:, :, 1]), dim=-1) + # pos = torch.stack(pos, dim=0) + output = pos + dpos + return output + + def forward(self, src, tgt, role, test=False, has_mask=True): + mem = self.normalize_coord(src) + mem = self.encoder_input_layer(mem) + mem = self.pos_encoder(mem) + mem = self.transformer_encoder(mem) + + role = int(role) + idx = (role // 10) * 5 + role % 10 - 6 + if test: + if tgt is None: + output = src[-1, :, idx * 2:idx * 2 + 2].unsqueeze(0) + else: + output = torch.cat((src[-1, :, idx * 2:idx * 2 + 2].unsqueeze(0), tgt), dim=0) + else: + output = torch.cat((src[-1, :, idx * 2:idx * 2 + 2].unsqueeze(0), tgt[:-1]), dim=0) + prev_pos = output + if has_mask: + device = mem.device + if self.src_mask is None or self.src_mask.size(0) != len(mem): + self.src_mask = self.generate_square_subsequent_mask(len(output), len(mem)).to(device) + if self.tgt_mask is None or self.tgt_mask.size(0) != len(mem): + self.tgt_mask = self.generate_square_subsequent_mask(len(output), len(output)).to(device) + else: + self.src_mask = None + self.tgt_mask = None + output = self.normalize_coord(output) + output = self.decoder_input_layer(output) + output = self.pos_encoder(output) + output = self.transformer_decoder( + tgt=output, + memory=mem, + tgt_mask=self.tgt_mask, + memory_mask=self.src_mask + ) + output = self.linear_mapping(output) + output = self.normalize_layer(torch.clamp(output, min=-50, max=50)) + # output: [direction, distance] both in [0, 1] + output = self.restore_coord(output) + # output = nn.Tanh()(output) + # output = torch.stack((500 * output[:, :, 0], 500 * output[:, :, 1]), dim=-1) + prev_pos + # output = self.cal_pos(output, src, tgt, role) + + return output \ No newline at end of file diff --git a/case/secret_tundra-case1.json b/case/secret_tundra-case1.json new file mode 100644 index 0000000..4318162 --- /dev/null +++ b/case/secret_tundra-case1.json @@ -0,0 +1 @@ +{ "match_id": 6832287527, "tags": [ [], [], [ "Fog" ], [ "Fog" ], [ "Fog" ], [ "Anti-gank" ], [], [ "Anti-gank" ], [ "Attack" ], [ "Attack" ], [ "Anti-gank", "Defend" ], [ "Fog" ], [ "Fog" ], [ "Attack" ], [ "Anti-gank" ], [ "Attack" ], [], [], [ "Fog" ], [ "Attack" ] ], "contextSort": "highDiffFirst", "focusedTeam": 1, "focusedPlayer": 0, "frame": 6651, "trajTimeWindow": [ 0, 150 ], "contextLimit": [], "predictions": [ { "idx": 0, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12610.02145849829, 22122.602656819705 ], [ 12766.612496659385, 22143.57515194599 ], [ 12930.463205251133, 22212.635880521833 ], [ 13100.852640572737, 22290.090863855297 ], [ 13237.228870911165, 22409.085650483048 ], [ 13358.43132606363, 22546.539095845015 ], [ 13504.007605827219, 22663.794910104465 ], [ 13656.1331285112, 22730.873903817075 ], [ 13814.950180873162, 22777.153574261698 ], [ 13968.263482364537, 22873.5520396798 ] ], "probability": 0.03958355647565309, "attention": { "p00": { "health": 0.6704835247374894, "mana": 0.5635172469811269, "position": 0.18297208342343296, "level": 0.9942828715373186, "isAlive": 0.5120374352165562, "gold": 0.5801906479079058 }, "p01": { "health": 0.18252089301318297, "mana": 0.5538519881512005, "position": 0.2918331519564128, "level": 0.6280055529063506, "isAlive": 0.6768222186749888, "gold": 0.7325093401695835 }, "p02": { "health": 0.8616342294654318, "mana": 0.6658352377811159, "position": 0.41300035983020256, "level": 0.3432639335188514, "isAlive": 0.6941166594260664, "gold": 0.17957673019789855 }, "p03": { "health": 0.21101148906299683, "mana": 0.6984519554277311, "position": 0.27085670904484815, "level": 0.8055943519001934, "isAlive": 0.6799740965930015, "gold": 0.598663803851323 }, "p04": { "health": 0.45663014226990617, "mana": 0.5865429022822701, "position": 0.8265739909114793, "level": 0.15154425961065043, "isAlive": 0.04051160768595685, "gold": 0.3169934142843085 }, "p10": { "health": 0.21138816205558753, "mana": 0.9571107114879038, "position": 0.20519578066780486, "level": 0.8912779268142279, "isAlive": 0.10314557458815687, "gold": 0.6653747950241091 }, "p11": { "health": 0.7566341690310445, "mana": 0.22247520976964918, "position": 0.7978452693079028, "level": 0.30779298860633664, "isAlive": 0.7085796853373842, "gold": 0.943919544126778 }, "p12": { "health": 0.7105511357790686, "mana": 0.7223228200602705, "position": 0.5156474751674383, "level": 0.8688799005570578, "isAlive": 0.6675459074539574, "gold": 0.07884781092755966 }, "p13": { "health": 0.19781894070737382, "mana": 0.630442444205326, "position": 0.18137630712233932, "level": 0.4974826501297833, "isAlive": 0.5084142475733395, "gold": 0.7291814423364706 }, "p14": { "health": 0.4316127705151154, "mana": 0.5585631551950518, "position": 0.7840559669789837, "level": 0.9906829278176907, "isAlive": 0.30751508268410155, "gold": 0.5005961894119109 }, "t0": { "towerTop1": 0.3736431082819027, "towerTop2": 0.5592120498801847, "towerTop3": 0.9175828183089219, "towerMid1": 0.8664774560010486, "towerMid2": 0.22584415176099437, "towerMid3": 0.8787028812571791, "towerBot1": 0.6269783348025888, "towerBot2": 0.6648661989156694, "towerBot3": 0.8625604912627691, "towerBase1": 0.10906308007518728, "towerBase2": 0.8958617609098145, "creepTop": 0.7477623530750852, "creepMid": 0.316609745479723, "creepBot": 0.17752787613798793 }, "t1": { "towerTop1": 0.6846070081182347, "towerTop2": 0.12453730302874644, "towerTop3": 0.43209944583488946, "towerMid1": 0.636733153135463, "towerMid2": 0.19115555167280007, "towerMid3": 0.6629190716671034, "towerBot1": 0.35738012294961075, "towerBot2": 0.38942081549839447, "towerBot3": 0.6200093806049094, "towerBase1": 0.8583784283677325, "towerBase2": 0.6072249256347535, "creepTop": 0.5816664491532044, "creepMid": 0.0025062632035419696, "creepBot": 0.6336540237448103 }, "g": { "gameTime": 0.19148893076337137, "isNight": 0.47302972253690023, "roshanHP": 0.30885124535023256 } } }, { "idx": 1, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12601.889833650357, 22145.932677693843 ], [ 12764.29342474454, 22127.870225460072 ], [ 12948.784031364507, 22149.221003146784 ], [ 13134.221133215657, 22157.89960290715 ], [ 13312.90423863072, 22111.07725428514 ], [ 13494.907394399257, 22113.149616985742 ], [ 13662.12617335864, 22110.36667853965 ], [ 13823.559451365414, 22102.57701960127 ], [ 14003.88148693294, 22068.04845181792 ], [ 14165.407085224873, 22019.581469794102 ] ], "probability": 0.02538414987173654, "attention": { "p00": { "health": 0.6924320568538718, "mana": 0.5059984659372494, "position": 0.9734666096030966, "level": 0.928398797797295, "isAlive": 0.9331406329235337, "gold": 0.4794192882018229 }, "p01": { "health": 0.38236987112715926, "mana": 0.6411553320850332, "position": 0.9332901440434684, "level": 0.7062062015885682, "isAlive": 0.7475446339136578, "gold": 0.6242403482770766 }, "p02": { "health": 0.3890811517119348, "mana": 0.8886039402261163, "position": 0.9246123341481092, "level": 0.18686486786202372, "isAlive": 0.7686398064851201, "gold": 0.5392561411121135 }, "p03": { "health": 0.8962320888648836, "mana": 0.3518960724871678, "position": 0.6740142199974364, "level": 0.49947280123324034, "isAlive": 0.6541790311047067, "gold": 0.4173389312125044 }, "p04": { "health": 0.5831499513729592, "mana": 0.5922562368242332, "position": 0.602905004753393, "level": 0.7384629488867953, "isAlive": 0.284632138678079, "gold": 0.760705139218133 }, "p10": { "health": 0.4063482146705044, "mana": 0.7035468959367024, "position": 0.2939637412138363, "level": 0.38400252027907955, "isAlive": 0.26397721962870224, "gold": 0.0009138741916907556 }, "p11": { "health": 0.014785677382361584, "mana": 0.44119601836635414, "position": 0.9439217460424407, "level": 0.58087361878007, "isAlive": 0.18353716094225847, "gold": 0.5313658948574438 }, "p12": { "health": 0.09271176962730565, "mana": 0.5310164803169137, "position": 0.7715630930873087, "level": 0.6247337896406162, "isAlive": 0.2230649457510534, "gold": 0.41604206546194833 }, "p13": { "health": 0.1427480536812622, "mana": 0.9387713365030024, "position": 0.7642376532258901, "level": 0.47359434272793544, "isAlive": 0.1883438279319234, "gold": 0.36369515399783214 }, "p14": { "health": 0.865499340341606, "mana": 0.3167942546193714, "position": 0.47329580301015617, "level": 0.6796332028695904, "isAlive": 0.6293093514679546, "gold": 0.5691036718614733 }, "t0": { "towerTop1": 0.9614033432229843, "towerTop2": 0.2850549441485484, "towerTop3": 0.9454669118524219, "towerMid1": 0.28633558179415086, "towerMid2": 0.5461328255629969, "towerMid3": 0.6534408804829053, "towerBot1": 0.5004218428276317, "towerBot2": 0.20702696761128592, "towerBot3": 0.29449951625445414, "towerBase1": 0.9362890199617278, "towerBase2": 0.8764489016217851, "creepTop": 0.17027224808525543, "creepMid": 0.789520264527533, "creepBot": 0.5521951228514466 }, "t1": { "towerTop1": 0.7510758075005293, "towerTop2": 0.6542607445293152, "towerTop3": 0.41892004572818164, "towerMid1": 0.6675422669642559, "towerMid2": 0.49466189717370956, "towerMid3": 0.28694768119595926, "towerBot1": 0.8529684457670781, "towerBot2": 0.041424226808504994, "towerBot3": 0.8679395983777352, "towerBase1": 0.24925338262430752, "towerBase2": 0.11849546417145085, "creepTop": 0.41616932072909973, "creepMid": 0.03192347146203556, "creepBot": 0.7643558536853055 }, "g": { "gameTime": 0.4878532548380514, "isNight": 0.39703474702297203, "roshanHP": 0.800146200167946 } } }, { "idx": 2, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12245.35010689844, 22151.772274255087 ], [ 12075.504503804053, 22169.25601611797 ], [ 11900.013904247033, 22224.08345181106 ], [ 11731.6739838614, 22265.404196518994 ], [ 11547.417752945788, 22312.27624127692 ], [ 11370.057310439468, 22374.52926430245 ], [ 11215.237137065275, 22468.302748230333 ], [ 11064.495529324227, 22535.82103599959 ], [ 10884.365806532536, 22594.726755271087 ], [ 10733.96637769679, 22707.49965372479 ] ], "probability": 0.05266573759002395, "attention": { "p00": { "health": 0.1592145496459484, "mana": 0.28341008437341036, "position": 0.015992378089171755, "level": 0.08333646535189085, "isAlive": 0.13446960631689062, "gold": 0.1674336308707018 }, "p01": { "health": 0.14026249326651752, "mana": 0.09935757056653136, "position": 0.14441406017817504, "level": 0, "isAlive": 0.1569327231065365, "gold": 0.07237608058259479 }, "p02": { "health": 0.17352566043286743, "mana": 0.2847123543087784, "position": 0.32012832344732245, "level": 0.5468891834991735, "isAlive": 0.17429107663320872, "gold": 0.13670954607825136 }, "p03": { "health": 0.4698598029656767, "mana": 0.3445125527603615, "position": 0.697547938396138, "level": 0.5727638280577207, "isAlive": 0.22540504486510005, "gold": 0.16605870081081872 }, "p04": { "health": 0.5101249219133797, "mana": 0.5721342731951164, "position": 0.9113491767334219, "level": 0.33076721085035393, "isAlive": 0.08746795564689683, "gold": 0.14367821957839672 }, "p10": { "health": 0.6457408958959167, "mana": 0.7325857584425947, "position": 0.5432368867223234, "level": 0.3521969314561054, "isAlive": 0.3136436391909181, "gold": 0.11794154705248883 }, "p11": { "health": 0.38291697925744683, "mana": 0.3218778765876989, "position": 0.33157228542145156, "level": 0.14253479411078962, "isAlive": 0.15737820637122657, "gold": 0.11515087763032945 }, "p12": { "health": 0.5305336859987467, "mana": 0.679032632636752, "position": 0.6618937224630504, "level": 0.3819082664692697, "isAlive": 0.14728477674797852, "gold": 0.3497193597300514 }, "p13": { "health": 0.22184122315074395, "mana": 0.053584473621620984, "position": 0.19851698498468182, "level": 0.1326321973141332, "isAlive": 0, "gold": 0.14716188709127026 }, "p14": { "health": 0.43585789520333185, "mana": 0.3246062674896076, "position": 0.20747652681653223, "level": 0.2832431041389999, "isAlive": 0.12372872615052855, "gold": 0.09681920417510134 }, "t0": { "towerTop1": 0.47852054805683825, "towerTop2": 0, "towerTop3": 0, "towerMid1": 0.2705881393882265, "towerMid2": 0.05701781065139955, "towerMid3": 0.14374823087715696, "towerBot1": 0.07273689337408261, "towerBot2": 0.04267591925896773, "towerBot3": 0.0288435423565033, "towerBase1": 0.026184916258903228, "towerBase2": 0.05151928465247914, "creepTop": 0.6712940754213512, "creepMid": 0.4311902343927882, "creepBot": 0.03143181415551347 }, "t1": { "towerTop1": 0.2304417184042591, "towerTop2": 0.06194612670629114, "towerTop3": 0.0116526055114783, "towerMid1": 0.5368445890562427, "towerMid2": 0.013216860926482901, "towerMid3": 0.11971248408170779, "towerBot1": 0.0585035309674573, "towerBot2": 0.040230991474110606, "towerBot3": 0.04629271913342535, "towerBase1": 0, "towerBase2": 0, "creepTop": 0.734251583272711, "creepMid": 0.7307975321996699, "creepBot": 0.1433975510572637 }, "g": { "gameTime": 0.1832496668420132, "isNight": 0.6139071720231897, "roshanHP": 0 } } }, { "idx": 3, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12254.788129344744, 22080.93910599854 ], [ 12095.091428337824, 21986.05529489702 ], [ 11929.647175346157, 21920.006461589157 ], [ 11747.036274487391, 21903.69662803279 ], [ 11573.500478822763, 21882.298975087484 ], [ 11392.944968863187, 21900.01893156932 ], [ 11236.308579750548, 21957.06005609718 ], [ 11082.06721182537, 21989.135383227796 ], [ 10906.649687189865, 21968.96837804261 ], [ 10743.643836077967, 21903.4799781875 ] ], "probability": 0.061487562458724686, "attention": { "p00": { "health": 0.20786731139695858, "mana": 0.11637361078260414, "position": 0.06323182204332412, "level": 0.06116828132791192, "isAlive": 0.11570766840938923, "gold": 0.15017959621743618 }, "p01": { "health": 0.16490947393979688, "mana": 0.27653550876307254, "position": 0.16301734321413588, "level": 0, "isAlive": 0.17128540696236044, "gold": 0.20889281722205438 }, "p02": { "health": 0.21478208692006595, "mana": 0.23593102468507288, "position": 0.2626973438253537, "level": 0.6037211208301436, "isAlive": 0.16437926170266362, "gold": 0.19301749660380804 }, "p03": { "health": 0.4367703589812426, "mana": 0.2578084366779002, "position": 0.7666644345522617, "level": 0.5373557016908919, "isAlive": 0.2079588347968245, "gold": 0.11837707044934728 }, "p04": { "health": 0.38763178241426244, "mana": 0.5967384567573709, "position": 0.8927668289747083, "level": 0.23786461135993492, "isAlive": 0.13679841393467723, "gold": 0.16978277866126154 }, "p10": { "health": 0.5599312940764023, "mana": 0.7769320612582409, "position": 0.637795327481551, "level": 0.36513272281991277, "isAlive": 0.30921703053273936, "gold": 0.1517499324765639 }, "p11": { "health": 0.3332635843972203, "mana": 0.3650356569489764, "position": 0.387021651427065, "level": 0.06656942293489443, "isAlive": 0.14452898165646186, "gold": 0.11352143090460685 }, "p12": { "health": 0.5159599359732113, "mana": 0.6521898660466843, "position": 0.6760145275350179, "level": 0.47314687786869036, "isAlive": 0.2567246385604557, "gold": 0.371262286788482 }, "p13": { "health": 0.15987809377751724, "mana": 0.06439920672333811, "position": 0.12091207049205294, "level": 0, "isAlive": 0, "gold": 0.12496287872801651 }, "p14": { "health": 0.40322218997121173, "mana": 0.23865086710228725, "position": 0.21089728729354076, "level": 0.2175454229550921, "isAlive": 0.14834176959471307, "gold": 0.15998224506232112 }, "t0": { "towerTop1": 0.5142125869392652, "towerTop2": 0.0033611998121436046, "towerTop3": 0.008662610949999906, "towerMid1": 0.2843888073340858, "towerMid2": 0.08522043845753632, "towerMid3": 0.05018452343551344, "towerBot1": 0.10486619790645063, "towerBot2": 0, "towerBot3": 0, "towerBase1": 0.0007632882427664328, "towerBase2": 0, "creepTop": 0.6057830819454479, "creepMid": 0.4521862045435433, "creepBot": 0.136014944666076 }, "t1": { "towerTop1": 0.39484070381419345, "towerTop2": 0, "towerTop3": 0, "towerMid1": 0.5737640118997781, "towerMid2": 0.07163324193143618, "towerMid3": 0.043961135020774936, "towerBot1": 0.044547754688580354, "towerBot2": 0.00873409142794207, "towerBot3": 0.043190644361603796, "towerBase1": 0, "towerBase2": 0, "creepTop": 0.7855688679206813, "creepMid": 0.6649012800721211, "creepBot": 0.014483089502530036 }, "g": { "gameTime": 0.32789263421532316, "isNight": 0.5863053062700503, "roshanHP": 0.09562353323236564 } } }, { "idx": 4, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12233.235912441582, 22127.469772313227 ], [ 12057.726621554335, 22145.682995465297 ], [ 11883.589063613876, 22215.086318896992 ], [ 11732.578992819776, 22288.477047583154 ], [ 11608.641397978594, 22403.59928777063 ], [ 11499.556115732215, 22527.440819088344 ], [ 11409.112240363806, 22662.60688338391 ], [ 11284.336157213133, 22771.79120299003 ], [ 11185.588996899354, 22906.793903453196 ], [ 11104.706409400784, 23074.826004029994 ] ], "probability": 0.033204655699721114, "attention": { "p00": { "health": 0.16883183682894953, "mana": 0.19289963736031418, "position": 0.13891942054162804, "level": 0.1141541510212467, "isAlive": 0.23822915789232685, "gold": 0.1207519844749058 }, "p01": { "health": 0.16930583908194125, "mana": 0.14677683173884237, "position": 0.01907916170010239, "level": 0.12642399946015445, "isAlive": 0.20136977874737916, "gold": 0.010148352065206381 }, "p02": { "health": 0.1412776764590873, "mana": 0.23199172751080704, "position": 0.2649208179054894, "level": 0.5921959075011877, "isAlive": 0.20629658854840982, "gold": 0.07972541030218205 }, "p03": { "health": 0.5367390511524108, "mana": 0.2476854773318744, "position": 0.8606952459875844, "level": 0.6640256539679367, "isAlive": 0.25083400355587837, "gold": 0.1963944036751475 }, "p04": { "health": 0.4065872209609633, "mana": 0.5898882025836464, "position": 0.8865283610271139, "level": 0.3493681925810423, "isAlive": 0.175348210045368, "gold": 0.19177971082161324 }, "p10": { "health": 0.5994469302652716, "mana": 0.7184082655201346, "position": 0.5813982555018113, "level": 0.4014667719557463, "isAlive": 0.30557588399121083, "gold": 0.07282548076755885 }, "p11": { "health": 0.2900784201308497, "mana": 0.25347596696649743, "position": 0.3332118137998515, "level": 0.07151738175879625, "isAlive": 0.19923927535671934, "gold": 0.1412104250357638 }, "p12": { "health": 0.608397258301802, "mana": 0.7430833881855778, "position": 0.8063765484664646, "level": 0.4094413475188593, "isAlive": 0.13341967087512613, "gold": 0.36036111969604684 }, "p13": { "health": 0.19898324205395188, "mana": 0, "position": 0.17164323738416, "level": 0.07988103219742618, "isAlive": 0, "gold": 0 }, "p14": { "health": 0.3606549608283841, "mana": 0.1646276457776274, "position": 0.15107581389686897, "level": 0.18213440926184274, "isAlive": 0.06543957436178824, "gold": 0.1588173183321 }, "t0": { "towerTop1": 0.5477889136471472, "towerTop2": 0.08745981631411641, "towerTop3": 0.11692280144189778, "towerMid1": 0.27124334151687385, "towerMid2": 0.06385580938277459, "towerMid3": 0.10039665016549044, "towerBot1": 0.03398917345015314, "towerBot2": 0.012320387007118388, "towerBot3": 0, "towerBase1": 0.06210430923846606, "towerBase2": 0, "creepTop": 0.6911509001589431, "creepMid": 0.3956374072938721, "creepBot": 0.0374206840034142 }, "t1": { "towerTop1": 0.3044557349618633, "towerTop2": 0.057381462671269354, "towerTop3": 0.11602061074189712, "towerMid1": 0.5483327247174354, "towerMid2": 0.014321907280099445, "towerMid3": 0.1483339249806379, "towerBot1": 0.05469656725163595, "towerBot2": 0.10120193430908783, "towerBot3": 0.03219484337926552, "towerBase1": 0, "towerBase2": 0.03997463608392174, "creepTop": 0.8058372686021262, "creepMid": 0.6564134705257083, "creepBot": 0.08638846124638326 }, "g": { "gameTime": 0.31332462966738794, "isNight": 0.5808499244964078, "roshanHP": 0.04438332916973481 } } }, { "idx": 5, "trajectory": [ [ 12423.031, 22117.906 ], [ 12576.643799094607, 21982.2799154805 ], [ 12756.870859314005, 21846.93781001919 ], [ 12868.718169596283, 21674.436697667887 ], [ 12993.351329499423, 21488.649467705956 ], [ 13048.306873586569, 21266.63578302877 ], [ 13100.159224067955, 21067.621446774552 ], [ 13129.662000893351, 20866.250689463675 ], [ 13110.096956314823, 20655.986313724534 ], [ 13037.421271103967, 20462.305583476904 ], [ 13026.207813078285, 20256.5512237038 ] ], "probability": 0.08422753638434632, "attention": { "p00": { "health": 0.1359599488104406, "mana": 0.1893211372160789, "position": 0.0833855836536717, "level": 0.0478346213137512, "isAlive": 0.1159423365127697, "gold": 0.120038737914476 }, "p01": { "health": 0.1116796194652963, "mana": 0.1137559082644609, "position": 0.10727096907439115, "level": 0.0827043513312448, "isAlive": 0.1285682702116492, "gold": 0.12101002629969634 }, "p02": { "health": 0.6816351996990558, "mana": 0.21093666560208765, "position": 0.8462035029852035, "level": 0.36170079034956365, "isAlive": 0.586287917815713, "gold": 0.08711455054610129 }, "p03": { "health": 0.3622992652103896, "mana": 0.1706325865164902, "position": 0.3125230495577322, "level": 0.104731131635401, "isAlive": 0.278804030045552, "gold": 0.3239406994510089 }, "p04": { "health": 0.1518096046609228, "mana": 0.1543887810173658, "position": 0.34749692021643, "level": 0.4537861777029158, "isAlive": 0.11760402009337656, "gold": 0.0507381344763072 }, "p10": { "health": 0.5267311212565872, "mana": 0.7715887608349917, "position": 0.6447347823344292, "level": 0.3589794063849389, "isAlive": 0.24736416571735803, "gold": 0.1722399886071507 }, "p11": { "health": 0.5618914693071813, "mana": 0.2881316677775427, "position": 0.6724113995942038, "level": 0.1343124688056117, "isAlive": 0.4988765078403495, "gold": 0.13130297665742083 }, "p12": { "health": 0.7886807868140081, "mana": 0.1637502677738932, "position": 0.4516330333580985, "level": 0.342173581415851, "isAlive": 0.28278726444023145, "gold": 0.3098069966563946 }, "p13": { "health": 0.1877448086703104, "mana": 0.0127036412344506, "position": 0.14521722267152978, "level": 0.17177176953746612, "isAlive": 0.0407337682602224, "gold": 0.0254787631528496 }, "p14": { "health": 0.7446664489958748, "mana": 0.33530316768106233, "position": 0.8275076157626913, "level": 0.4152728626085763, "isAlive": 0.2344123969493684, "gold": 0.04877807242975947 }, "t0": { "towerTop1": 0.4910535443051372, "towerTop2": 0.02190227254039994, "towerTop3": 0.02451221773852437, "towerMid1": 0.2820490556124544, "towerMid2": 0.08837801771883393, "towerMid3": 0.0879493687980876, "towerBot1": 0.0136267554796872, "towerBot2": 0.05224850742769087, "towerBot3": 0.02400249366437204, "towerBase1": 0.0108872548473528, "towerBase2": 0.07871525770939428, "creepTop": 0.7813216308763907, "creepMid": 0.6819485857678512, "creepBot": 0.04705725945584724 }, "t1": { "towerTop1": 0.28100418957018036, "towerTop2": 0.07813247439999867, "towerTop3": 0.07502084705878542, "towerMid1": 0.5726998411974527, "towerMid2": 0.010562220732304173, "towerMid3": 0.0537573695757708, "towerBot1": 0.0650060846603196, "towerBot2": 0.0230166744165905, "towerBot3": 0.09819427905434117, "towerBase1": 0.018679225614997685, "towerBase2": 0.0213455988989778, "creepTop": 0.8212459706972776, "creepMid": 0.643553658424069, "creepBot": 0.0495522547354122 }, "g": { "gameTime": 0.80893942270323, "isNight": 0.3982297591596623, "roshanHP": 0.0458134467654837 } } }, { "idx": 6, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12587.577157982203, 22128.645597805968 ], [ 12757.499375192338, 22125.696030662653 ], [ 12941.421512071922, 22172.430164235502 ], [ 13127.270652490144, 22182.660384709983 ], [ 13280.474951997225, 22245.20384293507 ], [ 13451.452831616525, 22301.845083602908 ], [ 13607.351972925137, 22327.275514646157 ], [ 13758.131460143297, 22394.07562612227 ], [ 13889.425045601283, 22517.26850772622 ], [ 14044.514473254496, 22629.617628169024 ] ], "probability": 0.0150450553131366, "attention": { "p00": { "health": 0.7387383822037101, "mana": 0.5993635441340639, "position": 0.3378570610944409, "level": 0.07056771079613622, "isAlive": 0.2518254537625735, "gold": 0.05382768792438353 }, "p01": { "health": 0.21764260252404166, "mana": 0.7816390927940275, "position": 0.7799163125581892, "level": 0.004794127562152362, "isAlive": 0.6324520592946492, "gold": 0.4509022029748393 }, "p02": { "health": 0.3249762662308, "mana": 0.8800585347930143, "position": 0.29343721357476915, "level": 0.600638771236994, "isAlive": 0.4025716436213127, "gold": 0.8577337438938899 }, "p03": { "health": 0.7195708773489096, "mana": 0.4222374035567613, "position": 0.13908922286873304, "level": 0.19658905168596696, "isAlive": 0.49948661685382323, "gold": 0.7355970078456344 }, "p04": { "health": 0.10284779679583789, "mana": 0.12266513397212098, "position": 0.7731452886330021, "level": 0.6003556564585821, "isAlive": 0.7857092122869895, "gold": 0.42276444876682473 }, "p10": { "health": 0.8247457101182161, "mana": 0.31393229182368243, "position": 0.8263647933266627, "level": 0.6241104733572669, "isAlive": 0.2804504686549516, "gold": 0.3173198146789471 }, "p11": { "health": 0.12786665106898454, "mana": 0.011392766338043092, "position": 0.24247897987237077, "level": 0.04814162136361788, "isAlive": 0.35449485876629905, "gold": 0.19931714496754527 }, "p12": { "health": 0.7640039521859991, "mana": 0.6465200581908397, "position": 0.5731152243098108, "level": 0.046958831693008074, "isAlive": 0.4129233318522776, "gold": 0.15433803303069626 }, "p13": { "health": 0.17840437759180872, "mana": 0.044932723820077625, "position": 0.2282245290837317, "level": 0.6454224087467957, "isAlive": 0.7696023407960135, "gold": 0.6975602892592865 }, "p14": { "health": 0.22207383034043748, "mana": 0.9025817426250369, "position": 0.0647618543627575, "level": 0.8174573364071502, "isAlive": 0.8767588295231497, "gold": 0.2611576858553539 }, "t0": { "towerTop1": 0.4706159656969242, "towerTop2": 0.22909557820716553, "towerTop3": 0.5399297219205383, "towerMid1": 0.22793919349481162, "towerMid2": 0.858765890695625, "towerMid3": 0.8136942618513765, "towerBot1": 0.062251450715644197, "towerBot2": 0.383132903700945, "towerBot3": 0.27116419773753186, "towerBase1": 0.24539471868383123, "towerBase2": 0.19270709665631447, "creepTop": 0.33695344336936617, "creepMid": 0.5352812922207475, "creepBot": 0.48373432672231065 }, "t1": { "towerTop1": 0.2978199417187184, "towerTop2": 0.8282215464526392, "towerTop3": 0.9680040403999206, "towerMid1": 0.9522319949823588, "towerMid2": 0.8015098394810674, "towerMid3": 0.5213009992605915, "towerBot1": 0.024668253050153854, "towerBot2": 0.33342287990696073, "towerBot3": 0.9570554075947364, "towerBase1": 0.542182822985585, "towerBase2": 0.9879571810144501, "creepTop": 0.1483019980689224, "creepMid": 0.9082195989842976, "creepBot": 0.8394185433031569 }, "g": { "gameTime": 0.28682440101780826, "isNight": 0.1846220124468545, "roshanHP": 0.6035794004168296 } } }, { "idx": 7, "trajectory": [ [ 12423.031, 22117.906 ], [ 12551.543246097215, 21915.670276001947 ], [ 12667.068205038813, 21705.777857410016 ], [ 12719.097851074737, 21474.76556894854 ], [ 12773.714309268447, 21227.33557649594 ], [ 12744.158460963969, 20969.18629248631 ], [ 12686.295600356243, 20689.90438151874 ], [ 12656.261479482077, 20428.8772762832 ], [ 12677.790134291803, 20182.51440677442 ], [ 12660.782872760268, 19910.92361657953 ], [ 12671.02168179526, 19646.324559681256 ] ], "probability": 0.043043770260701025, "attention": { "p00": { "health": 0.1278575496243127, "mana": 0.13358842324721332, "position": 0.0977964626137625, "level": 0.0768114973330479, "isAlive": 0.1063178792341321, "gold": 0.1169936615376085 }, "p01": { "health": 0.1001611145168719, "mana": 0.1251794943414743, "position": 0.14344060599871075, "level": 0.046194798789896296, "isAlive": 0.115590414298007, "gold": 0.1218013870547148 }, "p02": { "health": 0.6027048260894561, "mana": 0.2687903948459307, "position": 0.8660295024489872, "level": 0.31352541279933344, "isAlive": 0.5623063942494997, "gold": 0.0804080479264715 }, "p03": { "health": 0.302402816332727, "mana": 0.1930800165339009, "position": 0.343639332379636, "level": 0.1028950992320715, "isAlive": 0.2812016207347911, "gold": 0.33343095965976577 }, "p04": { "health": 0.11451401711998677, "mana": 0.14995723590982285, "position": 0.3551603100166647, "level": 0.4487773488310419, "isAlive": 0.17399292910251574, "gold": 0.07316966664144756 }, "p10": { "health": 0.5493814763885909, "mana": 0.7485576617271295, "position": 0.6312127986035699, "level": 0.3586232835316446, "isAlive": 0.2810647021513399, "gold": 0.1741637665676384 }, "p11": { "health": 0.5324718403095938, "mana": 0.23211971408240695, "position": 0.6899527924193127, "level": 0.10639692141623658, "isAlive": 0.4847453048063972, "gold": 0.1416260109814742 }, "p12": { "health": 0.7852136355052048, "mana": 0.151369300072729, "position": 0.44244290818487464, "level": 0.3332375197757292, "isAlive": 0.2560208878163424, "gold": 0.3682489658647256 }, "p13": { "health": 0.1889160129633162, "mana": 0.0941116560704773, "position": 0.1212114685215353, "level": 0.1104719562079761, "isAlive": 0.0648355848260974, "gold": 0.07296985019370994 }, "p14": { "health": 0.7040142908858744, "mana": 0.30964019639932494, "position": 0.862246281549308, "level": 0.4583631158819643, "isAlive": 0.2415444685474839, "gold": 0.014013921414263386 }, "t0": { "towerTop1": 0.4200179380239151, "towerTop2": 0.0526748653252085, "towerTop3": 0.0804316896372322, "towerMid1": 0.2896707862796159, "towerMid2": 0.07649918765000444, "towerMid3": 0.0186068535485604, "towerBot1": 0.0639659192386669, "towerBot2": 0.05576185112055154, "towerBot3": 0.0180997566934207, "towerBase1": 0.005555811234948, "towerBase2": 0.0573802624621558, "creepTop": 0.7772471153231818, "creepMid": 0.6461646904229633, "creepBot": 0.0984623256561699 }, "t1": { "towerTop1": 0.2793845731450506, "towerTop2": 0.02240444461411353, "towerTop3": 0.0995439651603091, "towerMid1": 0.5323955010519046, "towerMid2": 0.01273618310846953, "towerMid3": 0.0087523739843351, "towerBot1": 0.09534653718092131, "towerBot2": 0.01910229411896305, "towerBot3": 0.0459760987140506, "towerBase1": 0.0220743455557479, "towerBase2": 0.002160266627918, "creepTop": 0.8514502255627852, "creepMid": 0.6066560819437978, "creepBot": 0.0668697866111036 }, "g": { "gameTime": 0.8666469881377038, "isNight": 0.3096509914895487, "roshanHP": 0.033281015176416 } } }, { "idx": 8, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12294.69015303862, 21976.998723601013 ], [ 12139.121231199262, 21901.44477905596 ], [ 11978.715050999725, 21867.493493388825 ], [ 11807.935024661187, 21782.372064628602 ], [ 11646.687719162233, 21706.747946093343 ], [ 11470.579270692551, 21695.847379554296 ], [ 11284.381705419184, 21654.650639592404 ], [ 11106.792749174096, 21637.618858236037 ], [ 10944.713698657972, 21565.959460773007 ], [ 10773.34205071443, 21512.01374923657 ] ], "probability": 0.10050956649430627, "attention": { "p00": { "health": 0.1706509597093353, "mana": 0.22017992845919127, "position": 0.13902056875184357, "level": 0.1944605382425097, "isAlive": 0.07791725442397847, "gold": 0.25098452700160595 }, "p01": { "health": 0.17106228202122695, "mana": 0.06503515900207277, "position": 0.17428460125235226, "level": 0.10909354458853576, "isAlive": 0.14452143720925884, "gold": 0.13301017708305715 }, "p02": { "health": 0.15321393718172058, "mana": 0.27287949327192207, "position": 0.24257688782414008, "level": 0.5678256801731146, "isAlive": 0.08231979809141285, "gold": 0.1511751081664102 }, "p03": { "health": 0.8017255401604458, "mana": 0.5336086912362583, "position": 0.9160747976882752, "level": 0.7316754096842537, "isAlive": 0.14462382574266205, "gold": 0.176789039639183 }, "p04": { "health": 0.46965661243057366, "mana": 0.36365393896406345, "position": 0.5939061927242227, "level": 0.4084685532673365, "isAlive": 0.259307372789491, "gold": 0.13222616864749417 }, "p10": { "health": 0.5008015427538227, "mana": 0.7257379778233148, "position": 0.6379055554712173, "level": 0.4017273897970859, "isAlive": 0.2864542666480359, "gold": 0.10230317955939919 }, "p11": { "health": 0.41131693240343586, "mana": 0.33136671511867916, "position": 0.3612094462787809, "level": 0.1667656239151614, "isAlive": 0.15211554019752813, "gold": 0.11539465819603639 }, "p12": { "health": 0.7144753156811684, "mana": 0.7384343141312356, "position": 0.7601179397092194, "level": 0.5021745630239576, "isAlive": 0.1736562017112859, "gold": 0.24031151600523526 }, "p13": { "health": 0.1624590041264293, "mana": 0, "position": 0.14915104676054458, "level": 0.07782140383530883, "isAlive": 0.0934288258254305, "gold": 0.14639035158924812 }, "p14": { "health": 0.40509985162915063, "mana": 0.19610079082993997, "position": 0.28502517042387465, "level": 0.2222250861717321, "isAlive": 0.12033140399273466, "gold": 0.2044451944584489 }, "t0": { "towerTop1": 0.4741157239965817, "towerTop2": 0.08097886147482146, "towerTop3": 0, "towerMid1": 0.24364845050141207, "towerMid2": 0.12960231476790757, "towerMid3": 0, "towerBot1": 0.17363020991538114, "towerBot2": 0.08808659818978672, "towerBot3": 0, "towerBase1": 0.1589798712254834, "towerBase2": 0.054232250022666205, "creepTop": 0.7589572469247634, "creepMid": 0.4385559550890871, "creepBot": 0.07431857074232076 }, "t1": { "towerTop1": 0.22835284406535705, "towerTop2": 0, "towerTop3": 0, "towerMid1": 0.4929712717751904, "towerMid2": 0, "towerMid3": 0.12386143614552826, "towerBot1": 0.13679932364891895, "towerBot2": 0.005187502386146278, "towerBot3": 0, "towerBase1": 0.06228605368885885, "towerBase2": 0.011493359502332427, "creepTop": 0.7648227370592824, "creepMid": 0.5970168693527156, "creepBot": 0.05545329757399065 }, "g": { "gameTime": 0.3434955341127276, "isNight": 0.6080733964412731, "roshanHP": 0.06098651223952732 } } }, { "idx": 9, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12286.587571561797, 22016.89264600054 ], [ 12136.117356020126, 21931.105739526374 ], [ 12032.189971676702, 21806.884047568015 ], [ 11946.689647321627, 21641.107908070288 ], [ 11862.987208318422, 21475.95314314664 ], [ 11787.856106922083, 21335.74485830831 ], [ 11720.067801146934, 21165.251709031898 ], [ 11703.183086665365, 20995.34226153592 ], [ 11744.122409334032, 20835.239568850724 ], [ 11748.37282475237, 20661.823930090846 ] ], "probability": 0.06418450268453144, "attention": { "p00": { "health": 0.293493370555702, "mana": 0.2465049721596654, "position": 0.0427358371211695, "level": 0.09345552512987834, "isAlive": 0.08653562270978288, "gold": 0.11781910451930235 }, "p01": { "health": 0.12708405561912414, "mana": 0.041710330170517465, "position": 0.05347729348672797, "level": 0.049127795041519644, "isAlive": 0.07654211540192132, "gold": 0.14557198824026754 }, "p02": { "health": 0.2616778449977664, "mana": 0.16451611606189115, "position": 0.27062496563264454, "level": 0.6193249881934377, "isAlive": 0.0980139564272827, "gold": 0.16699881108469594 }, "p03": { "health": 0.8175470946616158, "mana": 0.5736019537518967, "position": 0.9020765088312883, "level": 0.7417316165544445, "isAlive": 0.15200521317378476, "gold": 0.06469948198876461 }, "p04": { "health": 0.5536108335705744, "mana": 0.32891513582217347, "position": 0.6569282726890254, "level": 0.40511430838117984, "isAlive": 0.14171575188745064, "gold": 0.18099655388332694 }, "p10": { "health": 0.5073889557662341, "mana": 0.6265182641372908, "position": 0.6800580230971218, "level": 0.313134398658515, "isAlive": 0.25855901011483073, "gold": 0.21094108315950225 }, "p11": { "health": 0.315134935559901, "mana": 0.3834447579536867, "position": 0.4312969705264176, "level": 0.24092435977511972, "isAlive": 0.25509703904968734, "gold": 0.09650110825271993 }, "p12": { "health": 0.585529475803234, "mana": 0.7357789028392167, "position": 0.7995609561593634, "level": 0.46855568120288493, "isAlive": 0.23755236849237982, "gold": 0.22613386985681563 }, "p13": { "health": 0.22667395985419078, "mana": 0, "position": 0.09351198474061273, "level": 0.12838027664247326, "isAlive": 0.0885109680612748, "gold": 0.16370621912850208 }, "p14": { "health": 0.3450769080036154, "mana": 0.22788799826949116, "position": 0.19016983530281958, "level": 0.2153013681429322, "isAlive": 0.1691304224395316, "gold": 0.2070191244470019 }, "t0": { "towerTop1": 0.4009500534160468, "towerTop2": 0.061335798813950654, "towerTop3": 0.03292918006518657, "towerMid1": 0.217463835497896, "towerMid2": 0.01640444021433808, "towerMid3": 0.02897618640440249, "towerBot1": 0.14997022599583748, "towerBot2": 0.08318750410396879, "towerBot3": 0, "towerBase1": 0.05743596011942184, "towerBase2": 0.012853941924917086, "creepTop": 0.6907004082792018, "creepMid": 0.48226836005451384, "creepBot": 0.09676828930362393 }, "t1": { "towerTop1": 0.24628073145152246, "towerTop2": 0.049031177978414664, "towerTop3": 0, "towerMid1": 0.5139144378896018, "towerMid2": 0.04394648278642303, "towerMid3": 0.01674155107833908, "towerBot1": 0.10113581692267488, "towerBot2": 0.03971105861369116, "towerBot3": 0.08835102559737767, "towerBase1": 0, "towerBase2": 0, "creepTop": 0.7996006667503198, "creepMid": 0.6621338457869572, "creepBot": 0.0027411309333114087 }, "g": { "gameTime": 0.33233238188004754, "isNight": 0.5549179085495903, "roshanHP": 0.08666464782374926 } } }, { "idx": 10, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12584.99726849107, 22034.20992005378 ], [ 12739.638333661062, 21967.670113179778 ], [ 12926.331170307874, 21939.6081754968 ], [ 13091.069316375972, 21961.493255352678 ], [ 13259.370602783576, 21962.481986309394 ], [ 13420.232124773682, 21979.783010900508 ], [ 13566.381615178389, 22043.731511821512 ], [ 13741.440388790512, 22098.980390561366 ], [ 13898.354355913425, 22149.34518460757 ], [ 14070.488979243573, 22136.87601431208 ] ], "probability": 0.04218607314510983, "attention": { "p00": { "health": 0.08019920061813292, "mana": 0.2104180729981036, "position": 0.1713097973672542, "level": 0.049925186493871676, "isAlive": 0.1558848571355269, "gold": 0.15196766153750682 }, "p01": { "health": 0.13025305791859132, "mana": 0.038031102381396426, "position": 0.07887638850077949, "level": 0.0999350662336664, "isAlive": 0.11084887669633509, "gold": 0.13151214125435653 }, "p02": { "health": 0.6940518727948259, "mana": 0.20290928500588631, "position": 0.8721371870051899, "level": 0.35480323122259927, "isAlive": 0.6362353257951141, "gold": 0.037222218315609526 }, "p03": { "health": 0.4148888743843907, "mana": 0.20487982290147838, "position": 0.7880292511980993, "level": 0.8246158611417427, "isAlive": 0.34668462247668413, "gold": 0.340036272853518 }, "p04": { "health": 0.14079802412720366, "mana": 0.8281554900288199, "position": 0.7667675294906433, "level": 0.4235020848800852, "isAlive": 0.15309792933468971, "gold": 0 }, "p10": { "health": 0.44894565272820275, "mana": 0.7553015344174029, "position": 0.6404507873138272, "level": 0.4536732020112252, "isAlive": 0.2677856133893186, "gold": 0.111226359740739 }, "p11": { "health": 0.5554946640073188, "mana": 0.26433159876258794, "position": 0.7770435533734878, "level": 0.06650403925662193, "isAlive": 0.513470178297253, "gold": 0.17190190434130087 }, "p12": { "health": 0.6870924614603784, "mana": 0.18782236511381847, "position": 0.3789108052479686, "level": 0.332627250159786, "isAlive": 0.2998988756836891, "gold": 0.34531110436508927 }, "p13": { "health": 0.0844427572871002, "mana": 0.012246433215799893, "position": 0.12388678914960918, "level": 0.11485296301846673, "isAlive": 0.13006180569220938, "gold": 0.1460583071546369 }, "p14": { "health": 0.7151780911884028, "mana": 0.32107558356878463, "position": 0.9565272195407347, "level": 0.47005827068953787, "isAlive": 0.3179616599424918, "gold": 0 }, "t0": { "towerTop1": 0.5432102852681877, "towerTop2": 0.000590979398982526, "towerTop3": 0, "towerMid1": 0.1868229007882408, "towerMid2": 0.05207768963530146, "towerMid3": 0.035134054696526536, "towerBot1": 0, "towerBot2": 0, "towerBot3": 0, "towerBase1": 0.06959104436472367, "towerBase2": 0.10076694931619798, "creepTop": 0.7488088633183174, "creepMid": 0.6259985870585774, "creepBot": 0.06292188124724325 }, "t1": { "towerTop1": 0.37856482048874984, "towerTop2": 0.1730551576703559, "towerTop3": 0.11690103106182306, "towerMid1": 0.7359900409610481, "towerMid2": 0, "towerMid3": 0.11644798336155023, "towerBot1": 0.0022675297127523544, "towerBot2": 0, "towerBot3": 0, "towerBase1": 0.17587197154772247, "towerBase2": 0.013947450250643284, "creepTop": 0.6846210581372585, "creepMid": 0.6725636798255893, "creepBot": 0.1827119814861855 }, "g": { "gameTime": 0.4988743699544615, "isNight": 0.6700530623785068, "roshanHP": 0.04942277445181556 } } }, { "idx": 11, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12247.90619878664, 22127.33675706884 ], [ 12078.546724417756, 22165.550080289395 ], [ 11924.927025758572, 22233.00617594663 ], [ 11759.541157175454, 22259.566584338394 ], [ 11569.419443736868, 22258.310328346248 ], [ 11400.637658824913, 22242.89370815587 ], [ 11216.54034445398, 22294.054821687514 ], [ 11029.986005956243, 22327.57292208332 ], [ 10852.957388505707, 22376.82652592874 ], [ 10719.894823625937, 22475.711862644523 ] ], "probability": 0.06077818197905083, "attention": { "p00": { "health": 0.09231144607736641, "mana": 0.3020717907489705, "position": 0, "level": 0.061987425286165194, "isAlive": 0.13219329530909088, "gold": 0.16164003007993372 }, "p01": { "health": 0.1891036815618083, "mana": 0.12694826493896127, "position": 0.057429471414289446, "level": 0.06709827705718302, "isAlive": 0.20067061356225765, "gold": 0.06827999997025455 }, "p02": { "health": 0.2097082091844871, "mana": 0.2341528489087612, "position": 0.24090955793113777, "level": 0.6088739329864556, "isAlive": 0.24139926747085544, "gold": 0.1029470591142934 }, "p03": { "health": 0.535958021668706, "mana": 0.2853365821000072, "position": 0.813725668167749, "level": 0.5794137478779868, "isAlive": 0.21694175836979673, "gold": 0.11527528701742229 }, "p04": { "health": 0.5427505723571578, "mana": 0.558350961668778, "position": 0.8485956934492767, "level": 0.3060595287025075, "isAlive": 0.21805780631185287, "gold": 0.12434359070587396 }, "p10": { "health": 0.5550533258054191, "mana": 0.7351231905401117, "position": 0.45278880042816555, "level": 0.3709853185077062, "isAlive": 0.3468535727828293, "gold": 0.18143025448118302 }, "p11": { "health": 0.2654956499836301, "mana": 0.35765625839973153, "position": 0.4226955143027009, "level": 0.23301021579937617, "isAlive": 0.2679975108748974, "gold": 0.21763796459581247 }, "p12": { "health": 0.6416572596783301, "mana": 0.6894668462822124, "position": 0.7147920040879974, "level": 0.5027553661365973, "isAlive": 0.2337822813074099, "gold": 0.38360602330676524 }, "p13": { "health": 0.14801860298915223, "mana": 0.095142530475099, "position": 0.07441036343437346, "level": 0.0018605207052871248, "isAlive": 0.06067089696769051, "gold": 0.08013828542019018 }, "p14": { "health": 0.43571696641670354, "mana": 0.24180864573503513, "position": 0.19576701609140756, "level": 0.25215627213566666, "isAlive": 0.1970020112799863, "gold": 0.16208181032583027 }, "t0": { "towerTop1": 0.4955424060797908, "towerTop2": 0, "towerTop3": 0.06931376562379898, "towerMid1": 0.31718943639101754, "towerMid2": 0.06727820571353213, "towerMid3": 0.01743009031520904, "towerBot1": 0.09084693189898384, "towerBot2": 0.05961419943491808, "towerBot3": 0, "towerBase1": 0.08719880374145664, "towerBase2": 0, "creepTop": 0.6468766393269809, "creepMid": 0.36835953932122706, "creepBot": 0.11581876312253973 }, "t1": { "towerTop1": 0.3272563732939979, "towerTop2": 0, "towerTop3": 0.12016534171300584, "towerMid1": 0.587046914398458, "towerMid2": 0.12174982551487007, "towerMid3": 0.059615429505291764, "towerBot1": 0.15641236166960978, "towerBot2": 0.05410091868254927, "towerBot3": 0.03888040758848225, "towerBase1": 0.030820089500046985, "towerBase2": 0, "creepTop": 0.6817661668130295, "creepMid": 0.5882668835175096, "creepBot": 0.17761811098253935 }, "g": { "gameTime": 0.280560282747186, "isNight": 0.6471230788142269, "roshanHP": 0.033555129527529454 } } }, { "idx": 12, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12262.703174548371, 22160.472561385668 ], [ 12076.20727117338, 22189.08746804775 ], [ 11918.153680172723, 22278.93911156946 ], [ 11763.017570526541, 22359.27808576453 ], [ 11620.052810027184, 22451.24854176262 ], [ 11505.722981450948, 22605.896607693554 ], [ 11425.593115506186, 22750.592557609063 ], [ 11362.916114564063, 22911.1192569004 ], [ 11289.33570755582, 23053.06745368365 ], [ 11227.822715813832, 23233.35323528589 ] ], "probability": 0.052366626157332244, "attention": { "p00": { "health": 0.18228151336600457, "mana": 0.1957470306936244, "position": 0.08488065684601126, "level": 0.06744527295229875, "isAlive": 0.07511421432984489, "gold": 0.1651733208273115 }, "p01": { "health": 0.1293550219575215, "mana": 0.12765831448622603, "position": 0.18682492374357948, "level": 0.14152587066727038, "isAlive": 0.13470766245037988, "gold": 0.035948911103876297 }, "p02": { "health": 0.23539631215388274, "mana": 0.223320567769959, "position": 0.2395876849521759, "level": 0.6609935472008673, "isAlive": 0.055432455696809074, "gold": 0.1837271507376807 }, "p03": { "health": 0.42093967040291774, "mana": 0.31779483430741073, "position": 0.818690887390473, "level": 0.7120091758869309, "isAlive": 0.15787814280021206, "gold": 0.13249813828000287 }, "p04": { "health": 0.3540833506394393, "mana": 0.5304770633678271, "position": 0.8954907103249491, "level": 0.36982075667444614, "isAlive": 0.08867864286610817, "gold": 0.1255082438833603 }, "p10": { "health": 0.5556070374976417, "mana": 0.7286112281279957, "position": 0.6008894054172994, "level": 0.36951145490025605, "isAlive": 0.24022551143316836, "gold": 0.12537594528487153 }, "p11": { "health": 0.2928296441606703, "mana": 0.41553085029606657, "position": 0.32601645812051544, "level": 0.16661124406913463, "isAlive": 0.2341201577605975, "gold": 0.18097404759754013 }, "p12": { "health": 0.5615711679941059, "mana": 0.6702041986696052, "position": 0.722866346087403, "level": 0.38449723800091695, "isAlive": 0.15507849507717697, "gold": 0.3649082476708555 }, "p13": { "health": 0.1107198814965892, "mana": 0.14429328995668395, "position": 0.13448721474213093, "level": 0.14726925670556487, "isAlive": 0.047657028024333525, "gold": 0.12007650331526226 }, "p14": { "health": 0.38729864066852, "mana": 0.25602965032138114, "position": 0.20015785954311338, "level": 0.2034159057017692, "isAlive": 0.19611107302224687, "gold": 0.1671400638413748 }, "t0": { "towerTop1": 0.49238851150094715, "towerTop2": 0.027855050641473058, "towerTop3": 0.15058340184395486, "towerMid1": 0.22407193719400534, "towerMid2": 0.08583533751155101, "towerMid3": 0.06277787997820405, "towerBot1": 0.16901936536306442, "towerBot2": 0.15044152945446435, "towerBot3": 0, "towerBase1": 0.11149397888955939, "towerBase2": 0.03013581082330299, "creepTop": 0.6648576664677661, "creepMid": 0.41835249486058795, "creepBot": 0.09003043938723976 }, "t1": { "towerTop1": 0.3717912154493724, "towerTop2": 0.01299090971097773, "towerTop3": 0.019068048979411084, "towerMid1": 0.6684854248973128, "towerMid2": 0.035658771610373244, "towerMid3": 0.005594496236320232, "towerBot1": 0.12699671491223818, "towerBot2": 0.03249748354634747, "towerBot3": 0.07971368499528919, "towerBase1": 0, "towerBase2": 0, "creepTop": 0.7879655189717225, "creepMid": 0.5784011968121004, "creepBot": 0.024160084263670945 }, "g": { "gameTime": 0.36817497344128014, "isNight": 0.5859902649018124, "roshanHP": 0.009658004600992975 } } }, { "idx": 13, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12300.626547535234, 21978.06941676178 ], [ 12207.267371652148, 21845.024463419977 ], [ 12164.161886069669, 21661.248337806526 ], [ 12170.1407300906, 21476.709335736385 ], [ 12186.643747402088, 21301.011448157013 ], [ 12209.988252687772, 21123.52757539197 ], [ 12285.550744540797, 20963.80118739408 ], [ 12390.397169581795, 20819.19605026091 ], [ 12521.280781884987, 20694.978664082177 ], [ 12685.610288049736, 20629.81015252701 ] ], "probability": 0.04834989063528593, "attention": { "p00": { "health": 0.14239621349137177, "mana": 0.21495785319286048, "position": 0.036659714865096685, "level": 0.11583637727743125, "isAlive": 0.14910116667074236, "gold": 0.1784197854098735 }, "p01": { "health": 0.17171101539334893, "mana": 0.08960441064991544, "position": 0.1492146273950467, "level": 0.10310826532445297, "isAlive": 0.07708307611873255, "gold": 0.08683209869520811 }, "p02": { "health": 0.23881906898093955, "mana": 0.17289595217368955, "position": 0.18932972402448053, "level": 0.5633946463841292, "isAlive": 0.10370166657573697, "gold": 0.14513629638040032 }, "p03": { "health": 0.7960579167464024, "mana": 0.553437351734079, "position": 0.8731948718847555, "level": 0.7287374122793527, "isAlive": 0.1681031740470489, "gold": 0.14625429694190037 }, "p04": { "health": 0.5561976816734674, "mana": 0.22714888858550245, "position": 0.5576837144996739, "level": 0.36134767430329473, "isAlive": 0.17886943302190597, "gold": 0.1428494991462294 }, "p10": { "health": 0.5989053300075162, "mana": 0.6975548476814938, "position": 0.5880033352329623, "level": 0.42655563505789457, "isAlive": 0.22900667316398887, "gold": 0.11419437199244492 }, "p11": { "health": 0.32876264346105916, "mana": 0.31901251990311685, "position": 0.3149084894816113, "level": 0.13360546349531302, "isAlive": 0.1345283954523499, "gold": 0.1208440640170795 }, "p12": { "health": 0.6337931194914299, "mana": 0.7197068240964939, "position": 0.8124956407840201, "level": 0.5497886053342336, "isAlive": 0.2554608742251775, "gold": 0.2527980525442023 }, "p13": { "health": 0.1444112042955516, "mana": 0.05459452179415208, "position": 0.08287963592523975, "level": 0.006168771745171137, "isAlive": 0.03613259254408235, "gold": 0.059140415326730436 }, "p14": { "health": 0.40521967612933646, "mana": 0.22087722890673275, "position": 0.17301408220839865, "level": 0.17328093195746036, "isAlive": 0.03783994798034851, "gold": 0.1375370518903998 }, "t0": { "towerTop1": 0.4733013630298036, "towerTop2": 0.06626894741844083, "towerTop3": 0.14918077238307226, "towerMid1": 0.1802039051029799, "towerMid2": 0.0020563362226551085, "towerMid3": 0.043778039635485495, "towerBot1": 0.08101714405904403, "towerBot2": 0.08320383361181932, "towerBot3": 0.09243972781063514, "towerBase1": 0.16384319672550865, "towerBase2": 0.0018005421349442472, "creepTop": 0.6130375758104336, "creepMid": 0.5252274378222067, "creepBot": 0.05412010771826473 }, "t1": { "towerTop1": 0.2906512014353643, "towerTop2": 0.024826928194622407, "towerTop3": 0.02707462723271592, "towerMid1": 0.5538921286915807, "towerMid2": 0, "towerMid3": 0.19323954868763238, "towerBot1": 0.1287984918847299, "towerBot2": 0, "towerBot3": 0.11062449633123303, "towerBase1": 0.06096742734602626, "towerBase2": 0.06022586323152371, "creepTop": 0.8573459454536402, "creepMid": 0.6299028516583736, "creepBot": 0.16993594262364048 }, "g": { "gameTime": 0.28197548440828507, "isNight": 0.6200529916431348, "roshanHP": 0.025241001226198653 } } }, { "idx": 14, "trajectory": [ [ 12423.031, 22117.906 ], [ 12555.1966199155, 21936.161259567158 ], [ 12643.05343818289, 21706.145345473742 ], [ 12787.135251166937, 21551.131232251326 ], [ 12980.478482251507, 21421.585006481415 ], [ 13174.458670076096, 21295.31193401905 ], [ 13298.683602798055, 21116.591509685793 ], [ 13357.24742062765, 20915.323395835225 ], [ 13407.10427904374, 20692.280266405563 ], [ 13528.936756111982, 20510.677039919156 ], [ 13706.199561211808, 20388.868307406417 ] ], "probability": 0.038043779610465, "attention": { "p00": { "health": 0.152688222985537, "mana": 0.1676537222398363, "position": 0.0739703762719949, "level": 0.0913167895560752, "isAlive": 0.173919828096983, "gold": 0.1404218226458767 }, "p01": { "health": 0.18088330537011496, "mana": 0.1297913729062577, "position": 0.1200569711883461, "level": 0.0630266769699678, "isAlive": 0.172976027114453, "gold": 0.1228211727430424 }, "p02": { "health": 0.6456403444162733, "mana": 0.2221172745125356, "position": 0.8969732354958937, "level": 0.3512298308676127, "isAlive": 0.5969676665269807, "gold": 0.04399839595822606 }, "p03": { "health": 0.3559088829752062, "mana": 0.179847433552975, "position": 0.3434115664497202, "level": 0.17100191739391196, "isAlive": 0.2977125966688117, "gold": 0.3931929615387986 }, "p04": { "health": 0.1737000985584962, "mana": 0.14688398386790916, "position": 0.7192678263051776, "level": 0.4614812756071907, "isAlive": 0.175194417899449, "gold": 0.0251297003404212 }, "p10": { "health": 0.5993036684293049, "mana": 0.771744511301454, "position": 0.665374628700688, "level": 0.3928694891188214, "isAlive": 0.2844027002732294, "gold": 0.1556855340114878 }, "p11": { "health": 0.5916793486077031, "mana": 0.278861357169312, "position": 0.7499277310043821, "level": 0.1747770126268399, "isAlive": 0.4116570905581678, "gold": 0.1665728272950804 }, "p12": { "health": 0.7073947805135715, "mana": 0.13396024622224326, "position": 0.4993776847808207, "level": 0.31146707263701606, "isAlive": 0.2383319080267039, "gold": 0.3238784105682796 }, "p13": { "health": 0.1190598999993027, "mana": 0.04375759802628552, "position": 0.19262596619651715, "level": 0.12404348925350486, "isAlive": 0.0647268842178905, "gold": 0.0835685781028354 }, "p14": { "health": 0.7291925233875546, "mana": 0.30388986939299073, "position": 0.8801280318021301, "level": 0.4431548853571047, "isAlive": 0.24218058314345542, "gold": 0.0614558885722106 }, "t0": { "towerTop1": 0.4420728985423949, "towerTop2": 0.0632482687761651, "towerTop3": 0.08068880459455934, "towerMid1": 0.2760706804277948, "towerMid2": 0.071135912599626, "towerMid3": 0.07999704825649676, "towerBot1": 0.058443057138069854, "towerBot2": 0.0351566867361846, "towerBot3": 0.0023796429017396, "towerBase1": 0.0456541871329051, "towerBase2": 0.0065008581549629, "creepTop": 0.7812083797586235, "creepMid": 0.6285490108813891, "creepBot": 0.0749903225245082 }, "t1": { "towerTop1": 0.2046125004566762, "towerTop2": 0.08717435197833012, "towerTop3": 0.0776411214493719, "towerMid1": 0.554653153715601, "towerMid2": 0.00831903951888253, "towerMid3": 0.047174391284496, "towerBot1": 0.08023511934662761, "towerBot2": 0.0989257616761416, "towerBot3": 0.02878159142919827, "towerBase1": 0.026787971909926434, "towerBase2": 0.03446019188642504, "creepTop": 0.8114022991768779, "creepMid": 0.6493227506058835, "creepBot": 0.0680306299759871 }, "g": { "gameTime": 0.9290270544874428, "isNight": 0.3759699238698057, "roshanHP": 0.0178598832063933 } } }, { "idx": 15, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12257.749256647476, 22036.21222945416 ], [ 12080.793408397054, 21992.945841547153 ], [ 11894.254512122186, 21949.185478813044 ], [ 11715.253384385143, 21919.834198703484 ], [ 11565.110235671136, 21870.72872391883 ], [ 11389.93326602554, 21864.68386389327 ], [ 11229.346052493227, 21880.89935555073 ], [ 11041.808486813281, 21869.06310224425 ], [ 10871.602461730678, 21793.872873697594 ], [ 10740.624032346715, 21685.86650860551 ] ], "probability": 0.030267602892550257, "attention": { "p00": { "health": 0.2278496659039505, "mana": 0.19844287382821718, "position": 0.11250985341882444, "level": 0.07117567522816921, "isAlive": 0.007388628099755712, "gold": 0.15975527140663207 }, "p01": { "health": 0.06728263721647662, "mana": 0.13736274373379004, "position": 0.22830231270203463, "level": 0.024342905865362592, "isAlive": 0.021460017883080648, "gold": 0.10273287794895644 }, "p02": { "health": 0.23508323144228205, "mana": 0.30862297754797835, "position": 0.2420488377014381, "level": 0.6652409701177237, "isAlive": 0.09358917123012088, "gold": 0.17948120466176837 }, "p03": { "health": 0.7954145861250544, "mana": 0.5374602035046805, "position": 0.9355895810584295, "level": 0.8312945891340521, "isAlive": 0.16252463423292055, "gold": 0.15619504010496113 }, "p04": { "health": 0.5406332741405124, "mana": 0.33892312049514123, "position": 0.7078108440407438, "level": 0.4175347252672635, "isAlive": 0.22041404185983288, "gold": 0.09257795530880542 }, "p10": { "health": 0.4917956598008653, "mana": 0.7103374495995898, "position": 0.5204230600331328, "level": 0.41130276406617344, "isAlive": 0.22092441174526475, "gold": 0.21398548179868587 }, "p11": { "health": 0.2415994165285705, "mana": 0.36805483525266197, "position": 0.4164039552148329, "level": 0.2160405895915838, "isAlive": 0.28554700852064174, "gold": 0.15541917142648623 }, "p12": { "health": 0.7317962728827855, "mana": 0.804714351605718, "position": 0.8127718395301734, "level": 0.5107311506920339, "isAlive": 0.09467419241112855, "gold": 0.31726742525998547 }, "p13": { "health": 0.20257453706447945, "mana": 0.14942787223359336, "position": 0.17355468625590625, "level": 0.026528494709397585, "isAlive": 0.04348210984328776, "gold": 0.013967209879060541 }, "p14": { "health": 0.3738111156793655, "mana": 0.14484840258252568, "position": 0.16999392121806842, "level": 0.13907456865898735, "isAlive": 0.13989892068787652, "gold": 0.2420753167316998 }, "t0": { "towerTop1": 0.482132351740956, "towerTop2": 0.07698418432376092, "towerTop3": 0, "towerMid1": 0.2615830683743048, "towerMid2": 0.022983941521340256, "towerMid3": 0.013434542667778694, "towerBot1": 0.10227073051450986, "towerBot2": 0.11952520299200067, "towerBot3": 0, "towerBase1": 0.10463006689187139, "towerBase2": 0.0014503780805286658, "creepTop": 0.7512871421684854, "creepMid": 0.4650894742123915, "creepBot": 0.0654763745161372 }, "t1": { "towerTop1": 0.3604142596725424, "towerTop2": 0.10504076772060839, "towerTop3": 0, "towerMid1": 0.573025134505086, "towerMid2": 0.09865886391333901, "towerMid3": 0.07465851015849947, "towerBot1": 0.10408549161999599, "towerBot2": 0.017808016265110513, "towerBot3": 0.053962941157036426, "towerBase1": 0.0017839517578640898, "towerBase2": 0.07414456618545104, "creepTop": 0.8406973627599382, "creepMid": 0.5924702527794081, "creepBot": 0.05806724852577749 }, "g": { "gameTime": 0.3195219379097829, "isNight": 0.6137834176728302, "roshanHP": 0.010240630482891755 } } }, { "idx": 16, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12556.721252955696, 22226.528087094302 ], [ 12659.870073262922, 22380.27888900538 ], [ 12749.170000231052, 22543.765439512827 ], [ 12805.082539845007, 22697.146520673683 ], [ 12818.595531773686, 22858.732613391607 ], [ 12863.984571240086, 23035.824837586584 ], [ 12926.155904160432, 23181.056723075933 ], [ 13044.58872973761, 23319.52767777721 ], [ 13157.090124394581, 23433.584597642654 ], [ 13232.465101342046, 23590.224721249404 ] ], "probability": 0.026982221824027756, "attention": { "p00": { "health": 0.06435122022385764, "mana": 0.4768819977919958, "position": 0.7722694500678557, "level": 0.22434658128096863, "isAlive": 0.054220311919691344, "gold": 0.010880137922347544 }, "p01": { "health": 0.37991469279673673, "mana": 0.046123076356861636, "position": 0.8831549326913144, "level": 0.17438182490324006, "isAlive": 0.2748340687481745, "gold": 0.024340610949103336 }, "p02": { "health": 0.5206870758760729, "mana": 0.9837935532727136, "position": 0.2801146773417462, "level": 0.424483396474578, "isAlive": 0.2715623893789185, "gold": 0.5740103404344732 }, "p03": { "health": 0.02053318275745797, "mana": 0.42931973670113854, "position": 0.6121995393757795, "level": 0.11488269838165821, "isAlive": 0.4614577255920209, "gold": 0.004607846216766687 }, "p04": { "health": 0.12856785575789376, "mana": 0.7630430322950681, "position": 0.18616721597172936, "level": 0.5969540421790693, "isAlive": 0.853506854281322, "gold": 0.17415659792312632 }, "p10": { "health": 0.9780027925534496, "mana": 0.9011138902950504, "position": 0.22254492530504733, "level": 0.4844817678990856, "isAlive": 0.5426420933017577, "gold": 0.5729235877644019 }, "p11": { "health": 0.6223450336028999, "mana": 0.6286482424876443, "position": 0.7553564304871567, "level": 0.12868610082975906, "isAlive": 0.31820377002286726, "gold": 0.5625120259440943 }, "p12": { "health": 0.8479305619341491, "mana": 0.5630805311795724, "position": 0.7417427249402864, "level": 0.21163047659807876, "isAlive": 0.5367310059532584, "gold": 0.8384041087331822 }, "p13": { "health": 0.09151724384549853, "mana": 0.054946996208707466, "position": 0.9138083386132387, "level": 0.5506170294449462, "isAlive": 0.8715586336405672, "gold": 0.8511163639079871 }, "p14": { "health": 0.4064814368968963, "mana": 0.4201985422979573, "position": 0.35669211708072646, "level": 0.3570019508014157, "isAlive": 0.032463673727509024, "gold": 0.7385415948135015 }, "t0": { "towerTop1": 0.0019859922412601705, "towerTop2": 0.4196550655141622, "towerTop3": 0.3911174609326564, "towerMid1": 0.604170511426056, "towerMid2": 0.6131461125295812, "towerMid3": 0.24297418837933948, "towerBot1": 0.5673871280245331, "towerBot2": 0.195172026064673, "towerBot3": 0.7025928587753694, "towerBase1": 0.0023793590667446907, "towerBase2": 0.11732467138503355, "creepTop": 0.01249934622357185, "creepMid": 0.5282496931040781, "creepBot": 0.25586924214542695 }, "t1": { "towerTop1": 0.5041921848120645, "towerTop2": 0.22732232442482747, "towerTop3": 0.8407755401535424, "towerMid1": 0.6026700098377178, "towerMid2": 0.44276955545679764, "towerMid3": 0.3264933491676092, "towerBot1": 0.1712335390505928, "towerBot2": 0.5178900942349478, "towerBot3": 0.639713293184403, "towerBase1": 0.49610571205433596, "towerBase2": 0.9342108014617927, "creepTop": 0.7615163712076505, "creepMid": 0.40823844324139147, "creepBot": 0.9352602088430295 }, "g": { "gameTime": 0.09568341056286256, "isNight": 0.36225887186997885, "roshanHP": 0.7398362130180651 } } }, { "idx": 17, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12595.732282904986, 22162.124941592105 ], [ 12775.933815990955, 22155.100524288842 ], [ 12942.76133950772, 22204.860455901286 ], [ 13106.886655693694, 22228.86318760136 ], [ 13292.320874576677, 22197.46348524744 ], [ 13458.570021974785, 22194.230714935864 ], [ 13607.348076780852, 22131.903665556063 ], [ 13759.778852327308, 22042.008038281736 ], [ 13929.709906205866, 21998.82435656669 ], [ 14093.537523266672, 21947.25276749058 ] ], "probability": 0.003985432683624596, "attention": { "p00": { "health": 0.7559508841066935, "mana": 0.9244913042713003, "position": 0.8920627578348088, "level": 0.8375130736131533, "isAlive": 0.48502568065991336, "gold": 0.1571415078212588 }, "p01": { "health": 0.36163044372224484, "mana": 0.3307041078815094, "position": 0.15146770281037902, "level": 0.48706377245184185, "isAlive": 0.5677076331990416, "gold": 0.5980251380190515 }, "p02": { "health": 0.2002769983523296, "mana": 0.22064168106381454, "position": 0.32287268042533723, "level": 0.34952082654892913, "isAlive": 0.36496988995956414, "gold": 0.6535163686353624 }, "p03": { "health": 0.4359850388695603, "mana": 0.26738173358020134, "position": 0.20024491505458197, "level": 0.7791169510536509, "isAlive": 0.4469906717330647, "gold": 0.13977522720571622 }, "p04": { "health": 0.40948894314687245, "mana": 0.6633047921686244, "position": 0.9664743200131587, "level": 0.10825112698327843, "isAlive": 0.44278009120713824, "gold": 0.00677995910552931 }, "p10": { "health": 0.940021693828659, "mana": 0.9242215293937146, "position": 0.17732968244308345, "level": 0.6922196675033965, "isAlive": 0.2631083573422417, "gold": 0.4043949348420832 }, "p11": { "health": 0.8763158044518307, "mana": 0.7540254922783303, "position": 0.1770433243424172, "level": 0.3941938321749179, "isAlive": 0.19301610850914996, "gold": 0.07908919535016179 }, "p12": { "health": 0.6559565548221791, "mana": 0.2944483027740248, "position": 0.3049790424399752, "level": 0.18647577371859447, "isAlive": 0.145022662618425, "gold": 0.902980915134437 }, "p13": { "health": 0.51577142160395, "mana": 0.010082171161841735, "position": 0.6522164495741922, "level": 0.669964559320821, "isAlive": 0.6294639912993003, "gold": 0.787444920838835 }, "p14": { "health": 0.48589707390514003, "mana": 0.9857244834676624, "position": 0.39603494356830504, "level": 0.2026022276387105, "isAlive": 0.031481310114411354, "gold": 0.1890498454287841 }, "t0": { "towerTop1": 0.5420131323262036, "towerTop2": 0.8941817722762317, "towerTop3": 0.6346721408511897, "towerMid1": 0.9332300016991231, "towerMid2": 0.844373295420223, "towerMid3": 0.5587081582190503, "towerBot1": 0.4342158101631639, "towerBot2": 0.2049912010943915, "towerBot3": 0.743930684927546, "towerBase1": 0.34103773407340254, "towerBase2": 0.6605356038283678, "creepTop": 0.08739720659143235, "creepMid": 0.3516691798610567, "creepBot": 0.992340528558012 }, "t1": { "towerTop1": 0.35797405227009027, "towerTop2": 0.14606255329659734, "towerTop3": 0.9208566646503351, "towerMid1": 0.08697710587397567, "towerMid2": 0.7063612086586413, "towerMid3": 0.2269230599190979, "towerBot1": 0.7390743735887402, "towerBot2": 0.8095560409620395, "towerBot3": 0.7347872024835194, "towerBase1": 0.13192359492237005, "towerBase2": 0.37980222557283216, "creepTop": 0.7550602563215436, "creepMid": 0.5493890255848204, "creepBot": 0.1867248276875506 }, "g": { "gameTime": 0.7156941985560723, "isNight": 0.6832456840565073, "roshanHP": 0.7425485438601085 } } }, { "idx": 18, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12263.01168560451, 22179.07301297231 ], [ 12119.470220865729, 22266.35974253743 ], [ 12011.800795094723, 22389.08384151499 ], [ 11914.020500544566, 22541.26299833877 ], [ 11821.667424207792, 22696.43545118931 ], [ 11756.793611530406, 22860.183583334383 ], [ 11625.456322614793, 22995.659305553978 ], [ 11552.848447638462, 23146.54815779316 ], [ 11430.69346087758, 23263.362064248086 ], [ 11342.890164510865, 23409.615658830484 ] ], "probability": 0.07224004133563902, "attention": { "p00": { "health": 0.16011154425391644, "mana": 0.24700361421305586, "position": 0.053028224638604474, "level": 0.13209265593882774, "isAlive": 0.1431493174518623, "gold": 0.200715452992488 }, "p01": { "health": 0.11883983924918248, "mana": 0.15764142918619728, "position": 0.1347376242916917, "level": 0.044839828330059894, "isAlive": 0.12553579822758404, "gold": 0.11190319183311943 }, "p02": { "health": 0.24908016092656485, "mana": 0.22407586715949543, "position": 0.2630991096157972, "level": 0.6150352753493618, "isAlive": 0.12007933465933014, "gold": 0.16930913043581036 }, "p03": { "health": 0.49369672041774987, "mana": 0.30998441035326074, "position": 0.7995375016719686, "level": 0.6456622628541459, "isAlive": 0.26822666559701613, "gold": 0.17030619982760833 }, "p04": { "health": 0.41071345491675865, "mana": 0.48715971089487503, "position": 0.907865100087834, "level": 0.30759603745512265, "isAlive": 0.09471862423388287, "gold": 0.14841719197213343 }, "p10": { "health": 0.5799478200895536, "mana": 0.7699883447372149, "position": 0.6081387147697364, "level": 0.30698711359113817, "isAlive": 0.27567175577632536, "gold": 0.1539798794855046 }, "p11": { "health": 0.2838291655317512, "mana": 0.3991465981717416, "position": 0.35793222612771614, "level": 0.08410143294155503, "isAlive": 0.14544196496826822, "gold": 0.14880099970479752 }, "p12": { "health": 0.5524077061688322, "mana": 0.7797310151167565, "position": 0.6645500544598204, "level": 0.4225245055786248, "isAlive": 0.16341805580769292, "gold": 0.3526180721499223 }, "p13": { "health": 0.1555359987334638, "mana": 0.09851557924627488, "position": 0.03403661421912804, "level": 0.05348373566301336, "isAlive": 0.12580039417517458, "gold": 0.11000480561039055 }, "p14": { "health": 0.37122993320058895, "mana": 0.3175030323879465, "position": 0.1420851398579586, "level": 0.2175659024237464, "isAlive": 0.2289913472043814, "gold": 0.17361620714659326 }, "t0": { "towerTop1": 0.4961336600773199, "towerTop2": 0, "towerTop3": 0.062056284748658055, "towerMid1": 0.31251230693806886, "towerMid2": 0.00875882527389571, "towerMid3": 0.06305758458515338, "towerBot1": 0.005393496615337487, "towerBot2": 0.09332863837988119, "towerBot3": 0.07696123956382965, "towerBase1": 0.02020101624042383, "towerBase2": 0.026420366702158048, "creepTop": 0.8337481739135011, "creepMid": 0.49036050592702235, "creepBot": 0.056056883465337076 }, "t1": { "towerTop1": 0.30314072589706953, "towerTop2": 0.04222722658031747, "towerTop3": 0.1725606832359815, "towerMid1": 0.6356730746715232, "towerMid2": 0, "towerMid3": 0, "towerBot1": 0.14629851761545037, "towerBot2": 0.12804877904997303, "towerBot3": 0.10413739709713175, "towerBase1": 0.017115972673693858, "towerBase2": 0.04804530285277133, "creepTop": 0.7397717794562766, "creepMid": 0.62699520779633, "creepBot": 0.035978704864755295 }, "g": { "gameTime": 0.31101719864284233, "isNight": 0.5649992430847709, "roshanHP": 0 } } }, { "idx": 19, "trajectory": [ [ 12423.031, 22117.906, 16512 ], [ 12325.590125654897, 21968.69648163222 ], [ 12192.752895072894, 21838.946127233838 ], [ 12060.46258639133, 21729.514741939613 ], [ 11966.76993085358, 21586.18036878334 ], [ 11865.592166453473, 21427.127532070277 ], [ 11794.43246290927, 21284.990179813667 ], [ 11666.4474739281, 21151.598026415493 ], [ 11597.039869447117, 21004.265054717576 ], [ 11564.741747166843, 20820.877157656076 ], [ 11506.454158067549, 20655.838633545525 ] ], "probability": 0.10546405650403348, "attention": { "p00": { "health": 0.18862126146869906, "mana": 0.19644947944671226, "position": 0.12948539657579042, "level": 0.09650111739551583, "isAlive": 0.17332443674857748, "gold": 0.20575574326066332 }, "p01": { "health": 0.13182462740176779, "mana": 0.112221093396351, "position": 0.14103660488763228, "level": 0.10843370140307197, "isAlive": 0.11181847777352848, "gold": 0.011888115720209202 }, "p02": { "health": 0.19645149575684503, "mana": 0.29670178964328026, "position": 0.2290739853987456, "level": 0.5332255263367369, "isAlive": 0.14088305892029432, "gold": 0.19400443117222205 }, "p03": { "health": 0.8216922523486696, "mana": 0.6122243703905743, "position": 0.9421047495285176, "level": 0.7455297366749839, "isAlive": 0.1492684224326154, "gold": 0.1564121146854025 }, "p04": { "health": 0.44433352295858103, "mana": 0.30249318771168365, "position": 0.503140059222443, "level": 0.47773576769140713, "isAlive": 0.19077230469211998, "gold": 0.21517736791276115 }, "p10": { "health": 0.46924331047446555, "mana": 0.738601961016918, "position": 0.5234132103417394, "level": 0.3952280526207683, "isAlive": 0.10262060702277598, "gold": 0.23623274216384352 }, "p11": { "health": 0.3987381314591219, "mana": 0.29196328956847484, "position": 0.3737350337423319, "level": 0.09902207878531018, "isAlive": 0.28014259104049605, "gold": 0.14216485622107555 }, "p12": { "health": 0.7447774456979156, "mana": 0.8007642770175782, "position": 0.7583621770274652, "level": 0.48047218893512283, "isAlive": 0.2518627092675644, "gold": 0.2408902333010745 }, "p13": { "health": 0.2842372026008865, "mana": 0.20343236361396477, "position": 0.16127781802697058, "level": 0.09939833909639952, "isAlive": 0.025307568097971717, "gold": 0.07434664838616681 }, "p14": { "health": 0.37497203524700684, "mana": 0.27097455003351006, "position": 0.16724792522575027, "level": 0.1362167675884765, "isAlive": 0.1160250587725464, "gold": 0.13577354127138339 }, "t0": { "towerTop1": 0.40432629577372226, "towerTop2": 0.09670360491727241, "towerTop3": 0.11745147703368757, "towerMid1": 0.26588037208960397, "towerMid2": 0.10724333558886202, "towerMid3": 0.013180457910987861, "towerBot1": 0.15837835312842158, "towerBot2": 0.015252497019230205, "towerBot3": 0.027225002412031015, "towerBase1": 0.0983376651769369, "towerBase2": 0.06354914026920229, "creepTop": 0.7276601850167798, "creepMid": 0.4770571344252157, "creepBot": 0.12033701219869536 }, "t1": { "towerTop1": 0.19840218880440247, "towerTop2": 0, "towerTop3": 0.01756670850609538, "towerMid1": 0.5936478231051144, "towerMid2": 0.07449064115705248, "towerMid3": 0.11465077425946411, "towerBot1": 0.07803059439924567, "towerBot2": 0, "towerBot3": 0.05227606310550519, "towerBase1": 0.07072854653444022, "towerBase2": 0, "creepTop": 0.8495990148003628, "creepMid": 0.654349522237993, "creepBot": 0.11508171445911304 }, "g": { "gameTime": 0.3242248133775889, "isNight": 0.5962840865516479, "roshanHP": 0.02353547426786041 } } } ], "predictionGroups": [ [ 18, 3, 11, 2, 12, 4 ], [ 19, 8, 13, 15, 9 ], [ 5, 7, 14, 10 ], [ 6, 0 ], [ 1, 16, 17 ] ], "selectedPredictors": [ 5, 7, 14 ], "comparedPredictors": [ 18, 3, 11, 2, 12, 4 ], "predictionProjection": [ [ 0.41992261707578227, 0.24635318136683268, 1 ], [ 0.7811800876032037, 0.11618605860844154, 1 ], [ 0.8511040689997533, 0.8312508049223163, 1 ], [ 0.7131399469211791, 0.5873263391789779, 1 ], [ 0.812407635338785, 0.7468555302935221, 1 ], [ 0.2064831429153183, 0.3032674033138903, 1 ], [ 0.5791196392603073, 0.22987565609159627, 1 ], [ 0.1763659917382141, 0.2654983603949973, 1 ], [ 0.4629053228281001, 0.6969528736479195, 1 ], [ 0.30039526845889264, 0.7182973662758472, 1 ], [ 0.17501534405150973, 0.4300373784181138, 1 ], [ 0.767728481819593, 0.7072535468550749, 1 ], [ 0.740977872717789, 0.6300994640417776, 1 ], [ 0.3517068447955676, 0.7832403770884735, 1 ], [ 0.01369289322586139, 0.5175893953727817, 1 ], [ 0.23742196958555187, 0.8521017097946777, 1 ], [ 0.9540947740781002, 0.2208297828705551, 1 ], [ 0.7801311174636695, 0.2719212546362193, 1 ], [ 0.6742929074371568, 0.7353780745738331, 1 ], [ 0.25392832347752176, 0.8159889024967045, 1 ] ], "instancesData": { "stages": [ { "groups": [ [ 7, 14, 5, 10 ], [ 17, 16, 1 ], [ 6, 0 ], [ 4, 18, 3, 11, 12, 2 ], [ 15, 19, 9, 8, 13 ] ], "instances": 159 }, { "groups": [ [ 7, 14, 5 ], [ 10, 17, 16, 1 ], [ 6, 0, 4 ], [ 18, 3, 11, 12, 2 ], [ 15 ], [ 19, 9 ], [ 8, 13 ] ], "instances": 132 }, { "groups": [ [ 7, 14, 5 ], [ 17, 18, 3 ], [ 10, 16, 1, 8 ], [ 6, 0, 4 ], [ 15, 19 ], [ 11, 9 ], [ 12, 2 ], [ 13 ] ], "instances": 100 }, { "groups": [ [ 7, 14, 5, 19 ], [ 17, 18, 3 ], [ 16, 1, 8 ], [ 6, 0, 4 ], [ 11, 9 ], [ 10, 12, 2 ], [ 15, 13 ] ], "instances": 84 }, { "groups": [ [ 7, 14, 5, 19 ], [ 17, 18, 3 ], [ 16 ], [ 1, 8 ], [ 6, 4 ], [ 11, 9 ], [ 10 ], [ 0, 12, 2, 15, 13 ] ], "instances": 40 }, { "groups": [ [ 7, 14, 5, 19 ], [ 17, 18, 3, 10 ], [ 16 ], [ 1, 8 ], [ 6, 4 ], [ 9 ], [ 11 ], [ 0, 12, 13 ], [ 2, 15 ] ], "instances": 34 }, { "groups": [ [ 7, 14, 5, 19 ], [ 17, 18, 3, 10, 16 ], [ 1 ], [ 8, 6, 4 ], [ 9 ], [ 11, 0, 12, 13 ], [ 2 ], [ 15 ] ], "instances": 17 } ], "numPredictors": 20, "totalInstances": 566 }, "mapStyle": "grey" } \ No newline at end of file diff --git a/case/secret_tundra-case2.json b/case/secret_tundra-case2.json new file mode 100644 index 0000000..ce8f20a --- /dev/null +++ b/case/secret_tundra-case2.json @@ -0,0 +1 @@ +{"match_id":6832287527,"tags":[["Defensive"],["Defensive"],["Sneak"],["Sneak"],["Sneak"],["Anti-gank"],["Defensive"],["Anti-gank"],["Offensive"],["Offensive"],["Anti-gank","Defensive"],["Sneak"],["Sneak"],["Offensive"],["Anti-gank"],["Offensive"],["Defensive"],["Defensive"],["Sneak"],["Offensive"]],"contextSort":"highDiffFirst","focusedTeam":1,"focusedPlayer":0,"frame":28620,"trajTimeWindow":[0,150],"contextLimit":[],"predictions":[{"idx":0,"trajectory":[[15305.344,14616.906,16512],[15146.084338596289,14721.118145647706],[15036.638418923705,14839.72052659637],[14951.977655534363,14980.752315732958],[14828.97802987265,15088.525783286985],[14706.6982561081,15199.300894262733],[14559.230533924943,15283.293186891424],[14382.554028953833,15318.199232741945],[14217.196071658693,15374.946635714625],[14088.65216830428,15479.933848884475],[13931.919686377056,15572.801200581602]],"probability":0.06571237425248641,"attention":{"p00":{"health":0.3439606009005459,"mana":0.25904699420589955,"position":0.28756290485722924,"level":0.33860157989407463,"isAlive":0.2036456530901502,"gold":0.17362625868888526},"p01":{"health":0.7762218636011984,"mana":0.4135305012978754,"position":0.41938402588777046,"level":0.20744307316929986,"isAlive":0.3894904685601138,"gold":0.1693375538513155},"p02":{"health":0.9313193190749887,"mana":0.7195323810798164,"position":0.9692991352393028,"level":0.3993706801063492,"isAlive":0.31667440039414485,"gold":0.20891641619818987},"p03":{"health":0.7619316225130223,"mana":0.6735753190939906,"position":0.9274590662748371,"level":0.4699155362346769,"isAlive":0.36549587409082457,"gold":0.3421358060854653},"p04":{"health":0.6660360092998081,"mana":0.5252203384985066,"position":0.973907418264806,"level":0.21577047507281333,"isAlive":0.354584973861718,"gold":0.19838562422028194},"p10":{"health":0.42434541452261654,"mana":0.404988871025106,"position":0.6204674356493353,"level":0.4558181396116931,"isAlive":0.26812310504098147,"gold":0.28426491093913525},"p11":{"health":0.6306816244744377,"mana":0.39227301162337613,"position":0.5171114552286925,"level":0.32431340823347143,"isAlive":0.3033354978743146,"gold":0.21542095898324232},"p12":{"health":0.36749288535070246,"mana":0.29560718233487415,"position":0.45080613516638934,"level":0.2775776720422292,"isAlive":0.2556350519724037,"gold":0.2979411191061431},"p13":{"health":0.7741711412551704,"mana":0.6479840849879489,"position":0.8946179840169958,"level":0.5272781193934377,"isAlive":0.29175887830823943,"gold":0.14091677962153998},"p14":{"health":0.40422366962733725,"mana":0.5323551098212368,"position":0.7322977452397328,"level":0.46143494427742004,"isAlive":0.268243469354113,"gold":0.22352641972042786},"t0":{"towerTop1":0.368627810122575,"towerTop2":0.5643038099322557,"towerTop3":0.29088173438413656,"towerMid1":0.4317943795907804,"towerMid2":0.3390275333348806,"towerMid3":0.36304596307425224,"towerBot1":0.21510191767333214,"towerBot2":0.2577405039036433,"towerBot3":0.3165668072537251,"towerBase1":0.17662679630905778,"towerBase2":0.16212515493215868,"creepTop":0.6105937857915397,"creepMid":0.6969494033357955,"creepBot":0.2014986977388351},"t1":{"towerTop1":0.29544848974796134,"towerTop2":0.19452848884549362,"towerTop3":0.2216124272214426,"towerMid1":0.30628012526724085,"towerMid2":0.4124613487328265,"towerMid3":0.30105355378199994,"towerBot1":0.319329657122847,"towerBot2":0.228834527107463,"towerBot3":0.19887654264339721,"towerBase1":0.10315500390734084,"towerBase2":0.13064229585367826,"creepTop":0.5837019704904614,"creepMid":0.8237152830126934,"creepBot":0.17352413859507995},"g":{"gameTime":0.5963160407574357,"isNight":0.8128272745109271,"roshanHP":0.2170129208969651}}},{"idx":1,"trajectory":[[15305.344,14616.906,16512],[15153.989571930573,14725.352666263154],[15018.96649285999,14832.398095011851],[14868.888066724357,14933.555970709236],[14712.438837679521,15013.993081008644],[14564.074036835895,15101.369883224106],[14459.112874922437,15228.868617327562],[14311.960561167556,15334.226123759567],[14186.64531870435,15431.945306676082],[14098.211445748393,15573.435598071948],[13979.893338662689,15689.008003039446]],"probability":0.06731205318332005,"attention":{"p00":{"health":0.3733090025906724,"mana":0.2694791965117737,"position":0.36618694713256317,"level":0.37848895666673604,"isAlive":0.2215683119175424,"gold":0.3981044620718025},"p01":{"health":0.8037783299037873,"mana":0.27692288536355647,"position":0.35490440178183535,"level":0.25559416926620293,"isAlive":0.27162882803176946,"gold":0.2658610065606422},"p02":{"health":0.8615000915247921,"mana":0.6681897719574299,"position":0.8429092922867669,"level":0.33070523270091623,"isAlive":0.2648417910936482,"gold":0.2608846858685941},"p03":{"health":0.7273822584582248,"mana":0.6630112758904931,"position":0.9835034052559751,"level":0.4375942763418875,"isAlive":0.2754716877817248,"gold":0.2800736717442671},"p04":{"health":0.6949691434319415,"mana":0.5097192523747397,"position":0.9154966461924864,"level":0.28434452580129754,"isAlive":0.2558989864319621,"gold":0.2206067267476115},"p10":{"health":0.3762344074188725,"mana":0.33910400797177814,"position":0.6998869889269637,"level":0.6220907123281378,"isAlive":0.22506802216222282,"gold":0.2124724111734407},"p11":{"health":0.5155797255726782,"mana":0.45062712899518076,"position":0.49085982381483034,"level":0.29322411083038813,"isAlive":0.1612136988589559,"gold":0.2997432147055943},"p12":{"health":0.439047158780606,"mana":0.45782032198545763,"position":0.4307599801278158,"level":0.2699330761196939,"isAlive":0.3549622487872968,"gold":0.2569134341213447},"p13":{"health":0.8281156629228282,"mana":0.6051275822359607,"position":0.871077777230929,"level":0.34870210089576703,"isAlive":0.22316749990051565,"gold":0.29611187221510304},"p14":{"health":0.41769069441367995,"mana":0.5206888079326335,"position":0.7115874752338162,"level":0.45311438974384183,"isAlive":0.1730062789139844,"gold":0.2199288848517266},"t0":{"towerTop1":0.319804826191848,"towerTop2":0.487322100177937,"towerTop3":0.41706818085978226,"towerMid1":0.42618762381589265,"towerMid2":0.37797515433784734,"towerMid3":0.3769787539082137,"towerBot1":0.19585008396220197,"towerBot2":0.24045464943131573,"towerBot3":0.3243760269217322,"towerBase1":0.03183240857018661,"towerBase2":0.1296504189877145,"creepTop":0.5739007800053249,"creepMid":0.7467425227010553,"creepBot":0.07793483251885142},"t1":{"towerTop1":0.25705237481186277,"towerTop2":0.20579653041733575,"towerTop3":0.25948648359033877,"towerMid1":0.3623364464638259,"towerMid2":0.35203722866632237,"towerMid3":0.4092887958278151,"towerBot1":0.22972638552280608,"towerBot2":0.22401486048437927,"towerBot3":0.17306818390782847,"towerBase1":0.20438907234871473,"towerBase2":0.15959656502485203,"creepTop":0.6361951443602354,"creepMid":0.7078622984461168,"creepBot":0.15130500156621385},"g":{"gameTime":0.5523437310435846,"isNight":0.8212360295704417,"roshanHP":0.2346772177176838}}},{"idx":2,"trajectory":[[15305.344,14616.906,16512],[15492.202860736426,14595.574891021786],[15660.299017592573,14561.315731854093],[15839.730834944221,14564.710981109936],[16011.153227209772,14544.631358562621],[16161.0629027042,14474.374594783609],[16296.672694460052,14362.191392150022],[16452.01577178828,14268.490338012523],[16588.955997548357,14182.45575579484],[16736.667540053917,14078.32023766664],[16846.847067624734,13936.639816261018]],"probability":0.08962488859992558,"attention":{"p00":{"health":0.2905845859091072,"mana":0.7660895093362616,"position":0.19691086632694632,"level":0.4023264127464976,"isAlive":0.6255546755609112,"gold":0.934146304612695},"p01":{"health":0.717143174535479,"mana":0.46788980786558576,"position":0.39564998685114583,"level":0.819479675628058,"isAlive":0.7552614760801384,"gold":0.9271059025913708},"p02":{"health":0.589593378306051,"mana":0.01606994850645216,"position":0.8217771354485026,"level":0.17654597546904394,"isAlive":0.4580463814474587,"gold":0.1165645446883039},"p03":{"health":0.8334778238992346,"mana":0.5191948769505721,"position":0.43761123795476453,"level":0.19248840476245177,"isAlive":0.9338942385842317,"gold":0.4854648867955724},"p04":{"health":0.46785329495047545,"mana":0.3404173090380447,"position":0.7220898383427263,"level":0.1543857792763943,"isAlive":0.03163964640217043,"gold":0.7475552040915141},"p10":{"health":0.263831371813285,"mana":0.7677538657413745,"position":0.6075042618991948,"level":0.6540112459980072,"isAlive":0.047674797876126274,"gold":0.23952324224249155},"p11":{"health":0.901604633622983,"mana":0.5464696066845378,"position":0.593771333420039,"level":0.08482849892482536,"isAlive":0.2717280128154489,"gold":0.760688849965026},"p12":{"health":0.611391557247978,"mana":0.15099864854595668,"position":0.8998014165459791,"level":0.8343260387932654,"isAlive":0.4111892406593678,"gold":0.8892606941283128},"p13":{"health":0.6528749679122334,"mana":0.44447627165452896,"position":0.7164057947481381,"level":0.02841863125194033,"isAlive":0.754799180247486,"gold":0.697060639064194},"p14":{"health":0.4477220782242135,"mana":0.688827924765266,"position":0.9706933771773396,"level":0.7833017710036765,"isAlive":0.7368037162343066,"gold":0.3383042973045638},"t0":{"towerTop1":0.42270762016928565,"towerTop2":0.12576926994366167,"towerTop3":0.6644586204150256,"towerMid1":0.12214321896156766,"towerMid2":0.6006902664007474,"towerMid3":0.08354117242263204,"towerBot1":0.9621448822102552,"towerBot2":0.4409623834349947,"towerBot3":0.5745856863023189,"towerBase1":0.6409136391748571,"towerBase2":0.2986428120514686,"creepTop":0.9199247378300324,"creepMid":0.6825161023495605,"creepBot":0.8900429787475581},"t1":{"towerTop1":0.5323698246684099,"towerTop2":0.6407164109795125,"towerTop3":0.7832717768778239,"towerMid1":0.7288888497746593,"towerMid2":0.01569526926443854,"towerMid3":0.9224210170786524,"towerBot1":0.9123853453005522,"towerBot2":0.6885227812215018,"towerBot3":0.8738611370288298,"towerBase1":0.8272452038658771,"towerBase2":0.27085510322250816,"creepTop":0.5942057438896928,"creepMid":0.5229380543245747,"creepBot":0.9091921993572605},"g":{"gameTime":0.7523756835218101,"isNight":0.3760756590000527,"roshanHP":0.42995689008279303}}},{"idx":3,"trajectory":[[15305.344,14616.906,16512],[15442.545371278002,14531.52219014144],[15584.855245457116,14449.15670014041],[15760.713708590602,14374.20074281363],[15935.578973027132,14353.54159810728],[16091.761640147084,14280.318593499649],[16227.911638141886,14195.26163006907],[16340.528630839774,14074.806143337708],[16417.503845868643,13912.689421930056],[16534.591975003852,13804.01447508565],[16682.946993332596,13706.143128685057]],"probability":0.0741202572061008,"attention":{"p00":{"health":0.28574567688674346,"mana":0.38486594422870213,"position":0.3248195017358231,"level":0.34499304459430347,"isAlive":0.7938649357758296,"gold":0.1990787543424486},"p01":{"health":0.21845660271504874,"mana":0.5755098565978858,"position":0.4405624663645069,"level":0.7183101570689414,"isAlive":0.11599336313202735,"gold":0.4129723030678867},"p02":{"health":0.4044776053684844,"mana":0.06572498328757059,"position":0.07542638671223378,"level":0.7621537151298889,"isAlive":0.5153452547936577,"gold":0.19243579436888747},"p03":{"health":0.40520046143089905,"mana":0.23576777178666197,"position":0.3528257616318804,"level":0.580049746887398,"isAlive":0.31857617182455944,"gold":0.5861602220664719},"p04":{"health":0.39494450511821255,"mana":0.39498121572348555,"position":0.7004972339302715,"level":0.8936785694800244,"isAlive":0.24928109734013137,"gold":0.15664481511577444},"p10":{"health":0.9978968988855075,"mana":0.7059387433545898,"position":0.9081720876049502,"level":0.7032862793697492,"isAlive":0.7643553762964339,"gold":0.15537778305718786},"p11":{"health":0.12821063619053175,"mana":0.44876354465411517,"position":0.2223915359332742,"level":0.009607944857660478,"isAlive":0.5870271534012681,"gold":0.6993962708969244},"p12":{"health":0.5881057622659751,"mana":0.14131462651240967,"position":0.6089134408442931,"level":0.5492835283829858,"isAlive":0.6569026407502663,"gold":0.40592852391615897},"p13":{"health":0.5091957215243681,"mana":0.5096081518422302,"position":0.0849824018989529,"level":0.37856220797037654,"isAlive":0.9475624001267025,"gold":0.444453464700886},"p14":{"health":0.924434713996118,"mana":0.9353730017695017,"position":0.021504643522461286,"level":0.3005693731968708,"isAlive":0.6936873164487458,"gold":0.07864855661558989},"t0":{"towerTop1":0.4220328078321771,"towerTop2":0.7087952854849378,"towerTop3":0.4752958569831509,"towerMid1":0.08479450336397387,"towerMid2":0.33244385989899095,"towerMid3":0.7484129663675991,"towerBot1":0.5306369575470187,"towerBot2":0.014039067478984979,"towerBot3":0.20627709501618985,"towerBase1":0.11056651691716635,"towerBase2":0.4930469201513279,"creepTop":0.7662013351576928,"creepMid":0.8816026876110954,"creepBot":0.5754390614358185},"t1":{"towerTop1":0.5740250200255677,"towerTop2":0.5543620047794724,"towerTop3":0.958881725732998,"towerMid1":0.5818161873070649,"towerMid2":0.14565733296579975,"towerMid3":0.7379798951227741,"towerBot1":0.5486449104905535,"towerBot2":0.6575548168223506,"towerBot3":0.44304732480284437,"towerBase1":0.8821803596382651,"towerBase2":0.1715694346179346,"creepTop":0.3973452670132167,"creepMid":0.08917168923700314,"creepBot":0.6599452326073412},"g":{"gameTime":0.634552372771438,"isNight":0.06060564751523789,"roshanHP":0.17782157680674815}}},{"idx":4,"trajectory":[[15305.344,14616.906,16512],[15304.807628603401,14635.852508629836],[15310.738797025268,14651.522815115506],[15312.56306379552,14669.360878921601],[15311.467446028475,14685.5934356611],[15307.357546309906,14701.53425263025],[15307.006094424085,14719.60751232043],[15305.834399946129,14737.714781107396],[15303.790593283671,14754.381411697554],[15307.354328448877,14770.72891389015],[15310.908031360394,14788.649760065831]],"probability":0.02468898783787012,"attention":{"p00":{"health":0.39846180763939354,"mana":0.11807206934764203,"position":0.11401050029890025,"level":0.4336625840854893,"isAlive":0.06355755144295361,"gold":0.1268460001902625},"p01":{"health":0.3057116859103144,"mana":0.4460692536273103,"position":0.09532799016951299,"level":0.14902142269514052,"isAlive":0.023440744846137784,"gold":0.27524750228801187},"p02":{"health":0.9027064626012435,"mana":0.8881187747370769,"position":0.2570731963561126,"level":0.03618582144443394,"isAlive":0.677329528104837,"gold":0.13767391773721482},"p03":{"health":0.2892091756098656,"mana":0.7369941602006642,"position":0.7739602553192801,"level":0.5014464831166578,"isAlive":0.47588732823972846,"gold":0.004358526582710631},"p04":{"health":0.7304334352737698,"mana":0.5440546500749106,"position":0.6241601325789947,"level":0.6236657552205591,"isAlive":0.46387189190173705,"gold":0.561077523413499},"p10":{"health":0.0779194732030204,"mana":0.5577670422387446,"position":0.9018953423247333,"level":0.3625304945266419,"isAlive":0.8041536174442814,"gold":0.757660903454213},"p11":{"health":0.7459119199174966,"mana":0.5587762519895672,"position":0.1994303478607431,"level":0.01486000409142152,"isAlive":0.3718934580473654,"gold":0.9499473198486936},"p12":{"health":0.0016868917537984363,"mana":0.20527050262501345,"position":0.8273264054035208,"level":0.8945381651996471,"isAlive":0.6639936711594292,"gold":0.7044271196618481},"p13":{"health":0.39644246959396945,"mana":0.5309130357374494,"position":0.8421532162528915,"level":0.0436605640348835,"isAlive":0.055475690652692755,"gold":0.9615619123034695},"p14":{"health":0.7414272156695245,"mana":0.6398935207233576,"position":0.5821306161100168,"level":0.7992577526214324,"isAlive":0.23139695946586047,"gold":0.4557231082777513},"t0":{"towerTop1":0.15653865389388133,"towerTop2":0.3027025543950381,"towerTop3":0.3458620598054849,"towerMid1":0.6084380812045798,"towerMid2":0.3048622073909113,"towerMid3":0.5096416881976917,"towerBot1":0.6255857256957671,"towerBot2":0.765059814576309,"towerBot3":0.08721142527038395,"towerBase1":0.5586401645758297,"towerBase2":0.3555201004845068,"creepTop":0.4563532624518727,"creepMid":0.1993130579775555,"creepBot":0.6112770865024342},"t1":{"towerTop1":0.3747368979168182,"towerTop2":0.40622480419619045,"towerTop3":0.3903217744021108,"towerMid1":0.261848363607025,"towerMid2":0.21555651342163484,"towerMid3":0.12891332299935288,"towerBot1":0.8696346024782147,"towerBot2":0.9918857823584861,"towerBot3":0.09515360453679444,"towerBase1":0.48614413388460953,"towerBase2":0.3237487332827258,"creepTop":0.2732300066285429,"creepMid":0.07206940901320391,"creepBot":0.421707300681045},"g":{"gameTime":0.3286552641745497,"isNight":0.7033540661666307,"roshanHP":0.5770613493368271}}},{"idx":5,"trajectory":[[15305.344,14616.906,16512],[15162.21112198727,14699.389839396672],[14997.638542977567,14754.111009555008],[14850.328188756763,14863.844156119985],[14712.442536559825,14945.060617597803],[14568.201872229465,15013.584487656643],[14427.943674583683,15118.632780366184],[14295.398609925078,15248.67513170063],[14164.843589663014,15372.62000461705],[14055.859695152321,15513.05699286148],[13968.149466121275,15647.782522401865]],"probability":0.006196354991325455,"attention":{"p00":{"health":0.35302479246800983,"mana":0.3131268310319508,"position":0.2960126301024835,"level":0.3969504525538222,"isAlive":0.2268034368709918,"gold":0.3319178818241333},"p01":{"health":0.7857652586114822,"mana":0.4272755958437905,"position":0.4646341421006528,"level":0.26623736581282825,"isAlive":0.24691989964393668,"gold":0.21752372900664946},"p02":{"health":0.7814490553035618,"mana":0.694512119056949,"position":0.9006834990746445,"level":0.34744703104383,"isAlive":0.3341684126802297,"gold":0.27182137499230724},"p03":{"health":0.8441336640124758,"mana":0.7196991267436992,"position":0.9522483307677133,"level":0.43823519477522616,"isAlive":0.34448476741152145,"gold":0.32847179814513994},"p04":{"health":0.5846815119792883,"mana":0.5034099940131682,"position":0.9201028257196164,"level":0.17260490029405978,"isAlive":0.19824673019408662,"gold":0.17765288517351174},"p10":{"health":0.47373243921167224,"mana":0.4079446773388695,"position":0.6281905845905078,"level":0.5209299910430261,"isAlive":0.19259351752656834,"gold":0.2260256056481214},"p11":{"health":0.48530481437952666,"mana":0.5170149492925596,"position":0.46655020744912634,"level":0.3511485840008104,"isAlive":0.2104859228451428,"gold":0.17692362517530066},"p12":{"health":0.4526135998172668,"mana":0.3227668293823857,"position":0.479718534965618,"level":0.3016196496798949,"isAlive":0.24336036998688226,"gold":0.23935457244298877},"p13":{"health":0.7652917313315118,"mana":0.6527844452965228,"position":0.8670834711196872,"level":0.5159626177090268,"isAlive":0.2522639663521219,"gold":0.23813634896191493},"p14":{"health":0.4433340573414177,"mana":0.583679594429354,"position":0.775928644690982,"level":0.501960380176305,"isAlive":0.20528905939746903,"gold":0.19630722417274288},"t0":{"towerTop1":0.46625915991662115,"towerTop2":0.4633969209945937,"towerTop3":0.314173724944426,"towerMid1":0.32298892557553,"towerMid2":0.39569942916554046,"towerMid3":0.39911017043842484,"towerBot1":0.15914533752740234,"towerBot2":0.2630871301375674,"towerBot3":0.2830325501054218,"towerBase1":0.08455002699880662,"towerBase2":0.21995609784027012,"creepTop":0.5224588141427747,"creepMid":0.7652624911577566,"creepBot":0.16899275452892248},"t1":{"towerTop1":0.3617575514817904,"towerTop2":0.2264871817395741,"towerTop3":0.20221505352805025,"towerMid1":0.42410261902205576,"towerMid2":0.4077439403570586,"towerMid3":0.26806308731370565,"towerBot1":0.24319083470759206,"towerBot2":0.22796549437884786,"towerBot3":0.18449482524999022,"towerBase1":0.14061285008323107,"towerBase2":0.06674055127604842,"creepTop":0.5629666262218054,"creepMid":0.6154417339189351,"creepBot":0.05600020623341109},"g":{"gameTime":0.6260596287531046,"isNight":0.7665965319571835,"roshanHP":0.24694480494597829}}},{"idx":6,"trajectory":[[15305.344,14616.906,16512],[15305.194335726173,14634.518817966735],[15304.357227940027,14652.959633532566],[15299.452765365011,14670.740811779157],[15288.89998780261,14686.244208409424],[15273.892472842099,14696.513177507792],[15263.415871775253,14712.090126489169],[15253.390255032906,14727.700823067631],[15240.404681529128,14738.740602567028],[15231.699682229386,14755.195021508245],[15219.083806672777,14769.259962926257]],"probability":0.08051325324747899,"attention":{"p00":{"health":0.5646558726406548,"mana":0.10273232801275278,"position":0.8601401257101471,"level":0.1627596541446794,"isAlive":0.4028977838980592,"gold":0.6784082516730647},"p01":{"health":0.388793308782559,"mana":0.1455503606292885,"position":0.5257297457776986,"level":0.3325704294110525,"isAlive":0.014451362203658391,"gold":0.06318548635414012},"p02":{"health":0.7407457447815469,"mana":0.012885294274911807,"position":0.12632401228514056,"level":0.13933408292679328,"isAlive":0.578359078592185,"gold":0.6214143227287574},"p03":{"health":0.4998755999504867,"mana":0.6169055090561966,"position":0.0494187989908772,"level":0.3370777103150433,"isAlive":0.1911718147394188,"gold":0.8505514557611309},"p04":{"health":0.7135928123669792,"mana":0.8537357699203982,"position":0.28269018442050053,"level":0.9953424241948916,"isAlive":0.08134670661064991,"gold":0.4231534629623013},"p10":{"health":0.06758778253737852,"mana":0.0810511604092703,"position":0.004112842025527197,"level":0.46270053263616795,"isAlive":0.18829182048748172,"gold":0.15623812141004723},"p11":{"health":0.0620281808910752,"mana":0.6099991445496462,"position":0.8303850029436011,"level":0.059433374342638956,"isAlive":0.6642664008216683,"gold":0.4002213195470812},"p12":{"health":0.20655572069705874,"mana":0.44114555533986843,"position":0.04535023103027802,"level":0.7929554812397157,"isAlive":0.08103201811503036,"gold":0.8301974585158403},"p13":{"health":0.3018971148122811,"mana":0.6372210061787593,"position":0.5785213086933574,"level":0.7160020472708781,"isAlive":0.8049052866922868,"gold":0.6665143761297252},"p14":{"health":0.6681399120242275,"mana":0.827783563294413,"position":0.047349030613109466,"level":0.8153369498014389,"isAlive":0.2930147161925001,"gold":0.5803699564012481},"t0":{"towerTop1":0.6114394977175859,"towerTop2":0.41703693700860667,"towerTop3":0.7867639358965235,"towerMid1":0.981439393283835,"towerMid2":0.2267211714583921,"towerMid3":0.07274029574638208,"towerBot1":0.49034182218454214,"towerBot2":0.16593185317950132,"towerBot3":0.637951011153534,"towerBase1":0.6446675267705579,"towerBase2":0.9274268053486265,"creepTop":0.10810212223356874,"creepMid":0.02982508456197719,"creepBot":0.4941430550792312},"t1":{"towerTop1":0.24951437430471746,"towerTop2":0.3023618515720934,"towerTop3":0.7116408071771352,"towerMid1":0.8571490199099283,"towerMid2":0.8423403179688347,"towerMid3":0.522078458880816,"towerBot1":0.7061481358310042,"towerBot2":0.8149198153960064,"towerBot3":0.46486032910193975,"towerBase1":0.9143970373738501,"towerBase2":0.7009635187695005,"creepTop":0.590081549438862,"creepMid":0.23741496760867098,"creepBot":0.8813803542486682},"g":{"gameTime":0.9441073149581913,"isNight":0.41827946669846505,"roshanHP":0.4028992641277831}}},{"idx":7,"trajectory":[[15305.344,14616.906,16512],[15371.12120435944,14441.185158785654],[15407.898172519148,14263.453678180003],[15398.380159672683,14091.857358259513],[15368.67484320386,13906.469032061792],[15310.523049664258,13745.360925038402],[15208.012920639536,13617.85323357068],[15106.876859802876,13480.365416315144],[14945.902337896669,13375.455724748288],[14847.74679213231,13233.400914433716],[14731.252535749794,13099.505753654088]],"probability":0.005074092826349824,"attention":{"p00":{"health":0.8110990927642729,"mana":0.2919865315780352,"position":0.27454161534212296,"level":0.5273857740150143,"isAlive":0.8087790078841295,"gold":0.05905154676430624},"p01":{"health":0.4348409992016524,"mana":0.8784633282001899,"position":0.5997923716076738,"level":0.004877265700267808,"isAlive":0.2857876800070551,"gold":0.5931960194381456},"p02":{"health":0.5484236863408247,"mana":0.8247644643361229,"position":0.048641362790135645,"level":0.5524311448613035,"isAlive":0.06992835596873825,"gold":0.01102693301815072},"p03":{"health":0.005235799768922966,"mana":0.742021410930815,"position":0.21712190435059275,"level":0.8423892696278579,"isAlive":0.8543280635618795,"gold":0.8971371542305464},"p04":{"health":0.24070394155774055,"mana":0.6542134877798078,"position":0.3481292445121378,"level":0.007783531957227474,"isAlive":0.10449829796684562,"gold":0.7920682215963755},"p10":{"health":0.9089666869094755,"mana":0.5295316753791013,"position":0.8601941703981444,"level":0.5000548193810803,"isAlive":0.8396239926943279,"gold":0.23989992383611436},"p11":{"health":0.7757761688021414,"mana":0.054006214312640966,"position":0.14669608267951006,"level":0.5651321080275507,"isAlive":0.04761084591475684,"gold":0.9505645557150797},"p12":{"health":0.5825502175329613,"mana":0.5952308169126532,"position":0.5543782118051344,"level":0.8776164004262763,"isAlive":0.9270506501557878,"gold":0.907599198457631},"p13":{"health":0.32404569908965786,"mana":0.4550338754516454,"position":0.2839229619831847,"level":0.689190453261987,"isAlive":0.8590248986885725,"gold":0.4121863851378198},"p14":{"health":0.7145161649802061,"mana":0.5943481062001374,"position":0.1973257196629985,"level":0.1976751798033607,"isAlive":0.49401301072525383,"gold":0.5823523821169947},"t0":{"towerTop1":0.6727689836848918,"towerTop2":0.5044270418832251,"towerTop3":0.5905739936124101,"towerMid1":0.6937003916166902,"towerMid2":0.30645911805467274,"towerMid3":0.0626947804976199,"towerBot1":0.056733160048622056,"towerBot2":0.9100218351633653,"towerBot3":0.34788820123936315,"towerBase1":0.40366886122237733,"towerBase2":0.47522667420264075,"creepTop":0.8652716593294212,"creepMid":0.08520516100240827,"creepBot":0.3216236354928268},"t1":{"towerTop1":0.7492941582248878,"towerTop2":0.7814607795043456,"towerTop3":0.7322455405522417,"towerMid1":0.5580801952888785,"towerMid2":0.7047612079205092,"towerMid3":0.7143133340437982,"towerBot1":0.3486395310437149,"towerBot2":0.5309383163524375,"towerBot3":0.6368811715531841,"towerBase1":0.542624316193334,"towerBase2":0.6452263057259444,"creepTop":0.3112104388120207,"creepMid":0.808224814189705,"creepBot":0.8394211938878064},"g":{"gameTime":0.8691537112523189,"isNight":0.13559200373624547,"roshanHP":0.8892636446112614}}},{"idx":8,"trajectory":[[15305.344,14616.906,16512],[15305.006068643606,14633.230902313579],[15310.791565013766,14650.982901745992],[15315.24698600719,14666.337754106724],[15315.756679491331,14685.289839379982],[15317.995298408763,14703.109251349126],[15319.136909725989,14721.726614868008],[15313.764133985256,14737.484459459263],[15314.373164433762,14754.748913651152],[15307.690041010852,14772.702651999682],[15303.02744941018,14788.000351592453]],"probability":0.0371045850856449,"attention":{"p00":{"health":0.5348676184327172,"mana":0.521490140035352,"position":0.05191999816640358,"level":0.16443598476386412,"isAlive":0.6168166076340202,"gold":0.13540859023796892},"p01":{"health":0.4353103415300148,"mana":0.8781458945383145,"position":0.6343947862508319,"level":0.8216915805603076,"isAlive":0.7566191784792422,"gold":0.22546782080440986},"p02":{"health":0.08915256655614368,"mana":0.676652492176794,"position":0.695608858936986,"level":0.25036768577844315,"isAlive":0.14523087029888915,"gold":0.30490293131866375},"p03":{"health":0.10411971133070064,"mana":0.061407131640879076,"position":0.19411279479138388,"level":0.32154183664817393,"isAlive":0.720041138915553,"gold":0.5954843100440756},"p04":{"health":0.8081325212349435,"mana":0.48383893691897195,"position":0.41670058647584596,"level":0.2205811930038455,"isAlive":0.7998580476700545,"gold":0.7755570645421086},"p10":{"health":0.40132551498543423,"mana":0.6923180897942884,"position":0.8845248473641438,"level":0.3701292339448641,"isAlive":0.7241319445021914,"gold":0.3154393096350425},"p11":{"health":0.35016890616095075,"mana":0.6181845176713061,"position":0.7259904734075731,"level":0.3337077437719871,"isAlive":0.10685360055853521,"gold":0.010663425808037141},"p12":{"health":0.892076848888326,"mana":0.884662511112297,"position":0.6179242842305392,"level":0.8174801678672479,"isAlive":0.7504236173950642,"gold":0.26073459716674896},"p13":{"health":0.7504335952551775,"mana":0.898585604813203,"position":0.77879069805771,"level":0.4827390139757819,"isAlive":0.9056433863342741,"gold":0.7166555129115599},"p14":{"health":0.4839117728680462,"mana":0.3344586608153248,"position":0.346594985001194,"level":0.05005681606035739,"isAlive":0.6347968536007698,"gold":0.6836558586633779},"t0":{"towerTop1":0.7050057323601666,"towerTop2":0.30436853625774263,"towerTop3":0.3576983967836487,"towerMid1":0.9292833784648962,"towerMid2":0.6202610920857543,"towerMid3":0.020024950638905414,"towerBot1":0.45381597773164106,"towerBot2":0.9246498818773474,"towerBot3":0.30926046879073765,"towerBase1":0.36823856730708293,"towerBase2":0.586293045184149,"creepTop":0.11256513954658454,"creepMid":0.36341140782791403,"creepBot":0.4829463038984765},"t1":{"towerTop1":0.904835910088146,"towerTop2":0.483607042487904,"towerTop3":0.12379712046752456,"towerMid1":0.18507949035469817,"towerMid2":0.9368522013644898,"towerMid3":0.9746911792061128,"towerBot1":0.2946633534735552,"towerBot2":0.8557583587115791,"towerBot3":0.6914725459535314,"towerBase1":0.35759570524888185,"towerBase2":0.9925564623397871,"creepTop":0.8619407979917801,"creepMid":0.5761819710909379,"creepBot":0.5664212546113532},"g":{"gameTime":0.539482180312594,"isNight":0.3580211702432159,"roshanHP":0.6240607865599781}}},{"idx":9,"trajectory":[[15305.344,14616.906,16512],[15314.346836878962,14807.522952914966],[15325.682116236609,14990.485464257506],[15360.453116257742,15162.337128378336],[15415.43102072841,15316.681672591658],[15454.498159510607,15502.138229460617],[15458.498974759957,15661.341087316317],[15313.753412519292,15732.188397763037],[15178.35093196323,15831.782150043215],[15045.288597779254,15952.394384221436],[14948.575098437417,16091.828819984317]],"probability":0.07933439933125942,"attention":{"p00":{"health":0.35611037753267705,"mana":0.3263467621390611,"position":0.31589807809271925,"level":0.5060629595322854,"isAlive":0.21033069815503463,"gold":0.3865367514404924},"p01":{"health":0.8118934752341569,"mana":0.26664020525370297,"position":0.3507857266310988,"level":0.15680855246411982,"isAlive":0.2761619787749092,"gold":0.25836302168124986},"p02":{"health":0.327554133626931,"mana":0.43169994746732343,"position":0.6330324351051002,"level":0.2193828754708487,"isAlive":0.2495854279625818,"gold":0.26187967876037266},"p03":{"health":0.42588264784390173,"mana":0.3902883314604154,"position":0.5587663479912508,"level":0.21519497888529882,"isAlive":0.26593629026058163,"gold":0.3562730033101356},"p04":{"health":0.5311486334924663,"mana":0.3483253751186863,"position":0.6289906906769301,"level":0.2856793814098349,"isAlive":0.28302245066780973,"gold":0.2332198323169423},"p10":{"health":0.626882634694638,"mana":0.31621048938140517,"position":0.5173972099161107,"level":0.466645164615363,"isAlive":0.18897676434895738,"gold":0.3634424092254869},"p11":{"health":0.6593413483936492,"mana":0.3595637369295534,"position":0.8652786250317157,"level":0.39118138295541877,"isAlive":0.28369068391179003,"gold":0.2471775647011517},"p12":{"health":0.5167086140534425,"mana":0.4325651305393599,"position":0.42238796948616314,"level":0.2950159307628699,"isAlive":0.327260894325433,"gold":0.33841941073939724},"p13":{"health":0.35437728721384604,"mana":0.3406924735809777,"position":0.6903769160675328,"level":0.15194923536510238,"isAlive":0.2935186643092781,"gold":0.24836255739336205},"p14":{"health":0.662917074163509,"mana":0.7377702727650898,"position":0.785006107799143,"level":0.393752219772131,"isAlive":0.0994958763213902,"gold":0.21377724785652183},"t0":{"towerTop1":0.37151287048087833,"towerTop2":0.47820673209677944,"towerTop3":0.3221410167252269,"towerMid1":0.43059746746186256,"towerMid2":0.48665071678274374,"towerMid3":0.3337158070109526,"towerBot1":0.10765922870661504,"towerBot2":0.14456216094389868,"towerBot3":0.21408134286528688,"towerBase1":0.17208543764455272,"towerBase2":0.048145992209100874,"creepTop":0.5288954789822694,"creepMid":0.7636946103084559,"creepBot":0.21406936969573276},"t1":{"towerTop1":0.3033265483912077,"towerTop2":0.1465597403080365,"towerTop3":0.2634468180057348,"towerMid1":0.5540062265902991,"towerMid2":0.38010108940697696,"towerMid3":0.2522055946270091,"towerBot1":0.1934051830890671,"towerBot2":0.2160520039978474,"towerBot3":0.23464962336203846,"towerBase1":0.14646905802428806,"towerBase2":0.018423949719557367,"creepTop":0.6047124184669409,"creepMid":0.6945936132280729,"creepBot":0.12750987113835682},"g":{"gameTime":0.825596503688415,"isNight":0.8901110245269935,"roshanHP":0.20884663121967922}}},{"idx":10,"trajectory":[[15305.344,14616.906,16512],[15194.796334753315,14771.901657279128],[15106.258114067574,14934.127532847242],[14987.923458428975,15039.652498767187],[14839.788418206183,15152.161934980702],[14732.544005944887,15292.399148148335],[14606.343312779336,15387.649748310589],[14487.209957034882,15514.056190872712],[14343.658865011454,15641.130800737608],[14230.862832126995,15793.703686169632],[14116.677220269134,15904.915267673845]],"probability":0.009827471048476815,"attention":{"p00":{"health":0.38529982915898703,"mana":0.2932515768072776,"position":0.2962538615928516,"level":0.3736593928987404,"isAlive":0.14766262492001042,"gold":0.30417710118093566},"p01":{"health":0.7970556285524678,"mana":0.34173898189183244,"position":0.29990318431555146,"level":0.2443377722843772,"isAlive":0.20018571518644157,"gold":0.2015662248030728},"p02":{"health":0.8334456947063638,"mana":0.5398441280708008,"position":0.8896644867156576,"level":0.4706097463400787,"isAlive":0.26090684923600926,"gold":0.18933037509001338},"p03":{"health":0.8115185852007668,"mana":0.6710656043875398,"position":0.9336596358479402,"level":0.319160864078381,"isAlive":0.2879559444344554,"gold":0.26304573569248424},"p04":{"health":0.6903497406480662,"mana":0.5127644704961116,"position":0.9297220278405389,"level":0.250529848744326,"isAlive":0.21569431152355986,"gold":0.23890791997844638},"p10":{"health":0.4149746029174917,"mana":0.2889552367786452,"position":0.5194686386771672,"level":0.40721187205119014,"isAlive":0.2507373498567764,"gold":0.2320050090750158},"p11":{"health":0.6355472169323496,"mana":0.3928191696002219,"position":0.5415118105584172,"level":0.44947827154764863,"isAlive":0.24176609854010817,"gold":0.29205277505311394},"p12":{"health":0.400467552790336,"mana":0.3054723901788667,"position":0.4438341252100366,"level":0.29406761012417515,"isAlive":0.2789552224449612,"gold":0.17709389520255503},"p13":{"health":0.8381345900383852,"mana":0.6379815492023396,"position":0.9138432451552008,"level":0.40461535807827637,"isAlive":0.33348059111917616,"gold":0.19532413446988767},"p14":{"health":0.43043568761389783,"mana":0.5677905712896936,"position":0.6888181929497713,"level":0.46400671854002573,"isAlive":0.28618180439943275,"gold":0.17815814056571816},"t0":{"towerTop1":0.37312017538742076,"towerTop2":0.49962815074608546,"towerTop3":0.3118690750554193,"towerMid1":0.34553002972398833,"towerMid2":0.4185053878893954,"towerMid3":0.4255270729047213,"towerBot1":0.17286658288333967,"towerBot2":0.19900264779152738,"towerBot3":0.16681369294120185,"towerBase1":0.1540869564349343,"towerBase2":0.2349321960530183,"creepTop":0.5846526751778564,"creepMid":0.7165045781910316,"creepBot":0.2462997483453953},"t1":{"towerTop1":0.28607852353824476,"towerTop2":0.3045427356185938,"towerTop3":0.2748472305551725,"towerMid1":0.31520728656642333,"towerMid2":0.45016116947805085,"towerMid3":0.38910849714416434,"towerBot1":0.28471774126175353,"towerBot2":0.11597218187277109,"towerBot3":0.1744875751822939,"towerBase1":0.21599542021660825,"towerBase2":0.09280227089889831,"creepTop":0.5618101688382693,"creepMid":0.5918668273400047,"creepBot":0.1667973113120807},"g":{"gameTime":0.48276347697821687,"isNight":0.701813545092323,"roshanHP":0.19250766410065429}}},{"idx":11,"trajectory":[[15305.344,14616.906,16512],[15310.034236557458,14632.250302935528],[15309.173256299338,14648.100890069943],[15305.104638366563,14663.483266311903],[15302.499498892612,14681.603013096203],[15295.205112358208,14695.75985669439],[15287.013482696155,14711.347250242936],[15275.245042281958,14722.132703934125],[15262.3562023108,14734.496382954707],[15249.35391639856,14746.107451697577],[15239.628936328916,14759.156437350677]],"probability":0.06093308365633329,"attention":{"p00":{"health":0.3802091788483022,"mana":0.3123626316879744,"position":0.1593140393584178,"level":0.09364681727173374,"isAlive":0.6021564775564927,"gold":0.7441966772825808},"p01":{"health":0.9021879147341765,"mana":0.7845178323921171,"position":0.6900735639708182,"level":0.20715836317487346,"isAlive":0.22996315336492046,"gold":0.6722278165426676},"p02":{"health":0.6836207182064769,"mana":0.718850818957903,"position":0.5187664720191771,"level":0.703909460906381,"isAlive":0.3218896848153223,"gold":0.7615446022842933},"p03":{"health":0.1810484820938678,"mana":0.9344414489357817,"position":0.41172404961321707,"level":0.11487598494921736,"isAlive":0.28857603572777246,"gold":0.9074837848755548},"p04":{"health":0.8734437814757079,"mana":0.37831000567927453,"position":0.0675077056705411,"level":0.9986267752082805,"isAlive":0.15019604964873134,"gold":0.5247451511693328},"p10":{"health":0.7259737374255846,"mana":0.2988303243417043,"position":0.5757292985141094,"level":0.6376569852578271,"isAlive":0.43684737362969295,"gold":0.6231978327882965},"p11":{"health":0.7494808805665552,"mana":0.06985702445337227,"position":0.6623345649300767,"level":0.48970589283616994,"isAlive":0.5363244254249031,"gold":0.6894092878438411},"p12":{"health":0.9837916804980793,"mana":0.09449760037431254,"position":0.7877335745172509,"level":0.5502546267907937,"isAlive":0.3138487437642137,"gold":0.2807709702282035},"p13":{"health":0.221186421417489,"mana":0.30128469428739435,"position":0.06177944886757114,"level":0.5641498093546569,"isAlive":0.5359928712901179,"gold":0.312595163175466},"p14":{"health":0.07780024914646066,"mana":0.6479230999009589,"position":0.5579666164162738,"level":0.09944536652979585,"isAlive":0.566395524592066,"gold":0.7447295445832713},"t0":{"towerTop1":0.9721714065999179,"towerTop2":0.4226584632230763,"towerTop3":0.5860368026581677,"towerMid1":0.17653081892917122,"towerMid2":0.6046102205009196,"towerMid3":0.19994396197125197,"towerBot1":0.9584913685537162,"towerBot2":0.22661876513299206,"towerBot3":0.271748607420093,"towerBase1":0.030005749425454198,"towerBase2":0.09939054926266722,"creepTop":0.1725609539774331,"creepMid":0.06971062930421268,"creepBot":0.3738583783875389},"t1":{"towerTop1":0.9550254019287425,"towerTop2":0.4044836518693127,"towerTop3":0.995719602839968,"towerMid1":0.816343222542552,"towerMid2":0.3357852082312398,"towerMid3":0.21471067703327806,"towerBot1":0.7086785838083052,"towerBot2":0.36541174263297593,"towerBot3":0.28428049392554433,"towerBase1":0.3064331241174114,"towerBase2":0.17384244976721352,"creepTop":0.6627270930236762,"creepMid":0.43357538237980475,"creepBot":0.9292435844913287},"g":{"gameTime":0.4212958704564036,"isNight":0.07468910010075303,"roshanHP":0.5523188065856703}}},{"idx":12,"trajectory":[[15305.344,14616.906,16512],[15313.590680823296,14632.438283570447],[15317.983143982423,14649.629676855395],[15321.137623540657,14666.87003655425],[15323.703042235446,14682.42197397179],[15324.990065531189,14701.596068379145],[15321.40335481812,14717.19237600424],[15316.568947470114,14735.2592245756],[15314.324859883496,14753.230009655888],[15316.780775686944,14771.046736840091],[15317.148144732404,14790.243926031351]],"probability":0.009921822195165356,"attention":{"p00":{"health":0.6568648726408644,"mana":0.3786315249603447,"position":0.402932725794529,"level":0.6033223409762163,"isAlive":0.4645440037018742,"gold":0.8814690174632203},"p01":{"health":0.2517869282728442,"mana":0.7230565663349144,"position":0.3460522452255237,"level":0.9767704138085773,"isAlive":0.7076007238324717,"gold":0.2816176447973764},"p02":{"health":0.34740181538364734,"mana":0.6212850287480065,"position":0.21257244866976333,"level":0.6773201245871567,"isAlive":0.9971393813815761,"gold":0.2681889638924826},"p03":{"health":0.9156645075832324,"mana":0.713674326733593,"position":0.03249816062864497,"level":0.3512298944569654,"isAlive":0.789154363675739,"gold":0.5375574950323954},"p04":{"health":0.5932708776015492,"mana":0.0021281386556262216,"position":0.8995150854702043,"level":0.9110432937590616,"isAlive":0.032426956665584594,"gold":0.8946809789606549},"p10":{"health":0.9814891917747324,"mana":0.4849420052161688,"position":0.5619742910967553,"level":0.65153495464281,"isAlive":0.592168539091726,"gold":0.19189441218125114},"p11":{"health":0.02824373624217058,"mana":0.6696526544606443,"position":0.028902729600056576,"level":0.1574154516061368,"isAlive":0.7582929046860023,"gold":0.45810432689402303},"p12":{"health":0.9230913347978875,"mana":0.10408276490581292,"position":0.6077073130151269,"level":0.4882470359045683,"isAlive":0.7758150258829828,"gold":0.5781641222508602},"p13":{"health":0.15863686521519682,"mana":0.007481424816316196,"position":0.869199510645436,"level":0.5309681112494191,"isAlive":0.4253713141867168,"gold":0.7689252122374419},"p14":{"health":0.1228863854344513,"mana":0.1353620258347168,"position":0.8014147211723894,"level":0.617608123265093,"isAlive":0.7811988092992093,"gold":0.8711949022867316},"t0":{"towerTop1":0.14257243113689855,"towerTop2":0.7192979172588441,"towerTop3":0.708804626556675,"towerMid1":0.6178235502420182,"towerMid2":0.9671602085187292,"towerMid3":0.0016788736981159236,"towerBot1":0.977970606483684,"towerBot2":0.44286972816527004,"towerBot3":0.43279062480855646,"towerBase1":0.0029440072035753495,"towerBase2":0.2256399801846125,"creepTop":0.9580123183608116,"creepMid":0.30829244965552705,"creepBot":0.8544275020678185},"t1":{"towerTop1":0.783709807770701,"towerTop2":0.08474541067539065,"towerTop3":0.6474406089074647,"towerMid1":0.6659848813195515,"towerMid2":0.5523273361141181,"towerMid3":0.9771555500308375,"towerBot1":0.6528552548883262,"towerBot2":0.5787274581963198,"towerBot3":0.21861021522177793,"towerBase1":0.35355391689271665,"towerBase2":0.4743946883928354,"creepTop":0.6074715519502889,"creepMid":0.38049358297366354,"creepBot":0.6768088748947583},"g":{"gameTime":0.008519030186022514,"isNight":0.2733610768768764,"roshanHP":0.6745479011364663}}},{"idx":13,"trajectory":[[15305.344,14616.906,16512],[15268.208281802887,14792.574454175301],[15203.498930676067,14952.356767226947],[15202.230525634246,15116.117799865637],[15200.615142212155,15289.71302182306],[15183.565215101826,15464.951741632181],[15142.74007733916,15643.58605758336],[15019.194250677649,15767.43678131583],[14901.389424697189,15919.309418573439],[14830.801186816609,16081.339252664771],[14757.550926255759,16251.713127325427]],"probability":0.0843792398863883,"attention":{"p00":{"health":0.25983966514783746,"mana":0.3107120194635162,"position":0.27740955592474964,"level":0.44437987228860315,"isAlive":0.2273969797904478,"gold":0.33882552164381435},"p01":{"health":0.7539298464027734,"mana":0.3467160465005035,"position":0.3617564407800708,"level":0.0905577559783425,"isAlive":0.2918255826510523,"gold":0.2295948807761608},"p02":{"health":0.5155799269850777,"mana":0.32405643299083375,"position":0.6001854267150843,"level":0.25281229658164284,"isAlive":0.3659556640736757,"gold":0.31198662575831876},"p03":{"health":0.4024866761323985,"mana":0.44543778353772256,"position":0.6008069200935133,"level":0.20648759910047623,"isAlive":0.22681689920673037,"gold":0.2848816265083381},"p04":{"health":0.47878716771837176,"mana":0.35844947356842716,"position":0.620329405041024,"level":0.27255762007361245,"isAlive":0.18802302055444742,"gold":0.2922740463863341},"p10":{"health":0.7299466373215154,"mana":0.2576735911910183,"position":0.6040622790121691,"level":0.39736412623030604,"isAlive":0.2181067283746622,"gold":0.19055747796423878},"p11":{"health":0.6443391108400707,"mana":0.38965367234114007,"position":0.7697181181739569,"level":0.3592542096181598,"isAlive":0.3237230199488438,"gold":0.28031219741431695},"p12":{"health":0.39585813977946127,"mana":0.318274132829159,"position":0.43518854845843263,"level":0.2868899023935261,"isAlive":0.4259102559886344,"gold":0.29881738505694266},"p13":{"health":0.437038895218658,"mana":0.32476732463825253,"position":0.5936471687435179,"level":0.26971080438623296,"isAlive":0.27029326133275317,"gold":0.251194142300687},"p14":{"health":0.5857383203510819,"mana":0.6948857487370041,"position":0.8118153582447509,"level":0.42328236928125873,"isAlive":0.2793545304438956,"gold":0.21053873716354865},"t0":{"towerTop1":0.3877327173977044,"towerTop2":0.4024283147997525,"towerTop3":0.3265775610137345,"towerMid1":0.4280343409490476,"towerMid2":0.31881294719398373,"towerMid3":0.3654066067358572,"towerBot1":0.20021622218213803,"towerBot2":0.221017240222125,"towerBot3":0.23399757920630554,"towerBase1":0.12021629688667416,"towerBase2":0.10926554131030694,"creepTop":0.42089156330659716,"creepMid":0.7078264207263889,"creepBot":0.21554120409586913},"t1":{"towerTop1":0.2992869016201544,"towerTop2":0.24444636087320332,"towerTop3":0.23235901305384815,"towerMid1":0.5508493231864189,"towerMid2":0.26327558523220146,"towerMid3":0.3295981726556241,"towerBot1":0.19496211992635168,"towerBot2":0.1750248241156907,"towerBot3":0.18599413672767623,"towerBase1":0.21081580060263314,"towerBase2":0.08368791780348178,"creepTop":0.5838466110284402,"creepMid":0.670576097104424,"creepBot":0.11373870571715969},"g":{"gameTime":0.8096322613131275,"isNight":0.8834282093613934,"roshanHP":0.15648586590904395}}},{"idx":14,"trajectory":[[15305.344,14616.906,16512],[15484.697185077543,14624.400032902282],[15671.417993305628,14624.396321866214],[15856.088253575563,14606.231500222615],[16028.000985484156,14625.085255934677],[16210.93434038973,14672.888113329704],[16364.033497029495,14740.47833083145],[16532.67229713141,14790.89120904009],[16694.308039794,14777.56960584955],[16884.461585390505,14780.203445033801],[17044.85778381571,14808.93167353634]],"probability":0.0875818117122206,"attention":{"p00":{"health":0.3790942398315553,"mana":0.9547704656859004,"position":0.6857531345054422,"level":0.02935175429939285,"isAlive":0.20104451266877255,"gold":0.9836830865360922},"p01":{"health":0.5076701982539498,"mana":0.39744032900955517,"position":0.9844179266559125,"level":0.41667770738303767,"isAlive":0.1775018851498935,"gold":0.3474104549757675},"p02":{"health":0.6275039712109292,"mana":0.16135833459934346,"position":0.393569818309339,"level":0.5175025211448712,"isAlive":0.8869301097990685,"gold":0.6631750830914991},"p03":{"health":0.03513422711448522,"mana":0.6914850549040041,"position":0.43655429325370565,"level":0.2902662253648305,"isAlive":0.0496388865755506,"gold":0.22579878531917763},"p04":{"health":0.6589387197909611,"mana":0.6548568265738852,"position":0.9328409292978193,"level":0.3322061432432484,"isAlive":0.34270190284369884,"gold":0.4622916527596761},"p10":{"health":0.2523657537402293,"mana":0.7514871315267633,"position":0.9260516678542083,"level":0.893972159093031,"isAlive":0.42977655330491826,"gold":0.20364126516342718},"p11":{"health":0.7376717212539725,"mana":0.5329988005305557,"position":0.9452413047290391,"level":0.591787211114313,"isAlive":0.4899431597019728,"gold":0.45012814978028204},"p12":{"health":0.7439003810999716,"mana":0.9487828312718465,"position":0.6283051875863257,"level":0.9364763188772678,"isAlive":0.6707290284406389,"gold":0.34914494930443585},"p13":{"health":0.38116569223552443,"mana":0.926931599968134,"position":0.25942323296971814,"level":0.746446736254438,"isAlive":0.5824754724231977,"gold":0.14773429178559483},"p14":{"health":0.32194076601828225,"mana":0.05199318889214677,"position":0.1288242935540207,"level":0.01133956728697072,"isAlive":0.06694712640982758,"gold":0.666604843635928},"t0":{"towerTop1":0.2242504967976655,"towerTop2":0.5169495454035888,"towerTop3":0.12874598698468742,"towerMid1":0.8274370988093098,"towerMid2":0.6730909791810744,"towerMid3":0.6693904198478868,"towerBot1":0.08916486136350388,"towerBot2":0.4052811685514184,"towerBot3":0.19531288886217268,"towerBase1":0.6312533562768023,"towerBase2":0.33007476101619915,"creepTop":0.49551111983553797,"creepMid":0.22128153686193697,"creepBot":0.74068367289405},"t1":{"towerTop1":0.1936100370525511,"towerTop2":0.3980741397926213,"towerTop3":0.7307287377359533,"towerMid1":0.0038325628309143767,"towerMid2":0.8754423092878136,"towerMid3":0.42796761421947127,"towerBot1":0.9152856356301675,"towerBot2":0.95318716198961,"towerBot3":0.8791782852602434,"towerBase1":0.6232994247789543,"towerBase2":0.7991363639399858,"creepTop":0.6544984525710487,"creepMid":0.7856435338160026,"creepBot":0.588651415024672},"g":{"gameTime":0.7326180805152982,"isNight":0.5492606915673817,"roshanHP":0.6103909813428774}}},{"idx":15,"trajectory":[[15305.344,14616.906,16512],[15171.098011487979,14754.727858286275],[15085.996775964068,14905.86747317075],[15060.18672474249,15090.071120209894],[15096.388303115118,15263.658143553277],[15182.852368851356,15406.657427725617],[15306.896164583892,15532.93167991356],[15452.126550157851,15623.785717352646],[15615.11591309211,15660.247180731434],[15795.655564675873,15659.311961418847],[15955.117132869062,15719.230764406228]],"probability":0.07641052708277996,"attention":{"p00":{"health":0.3270420410209819,"mana":0.28565995656704146,"position":0.3633552055824355,"level":0.40691114319866023,"isAlive":0.26789577584609126,"gold":0.33159979100073567},"p01":{"health":0.7654077866308323,"mana":0.3025426135530225,"position":0.3438911740374109,"level":0.1672224141989283,"isAlive":0.22192508265913422,"gold":0.2739696623336714},"p02":{"health":0.3202660419846405,"mana":0.43852124661700614,"position":0.5542804878728321,"level":0.21714854209247048,"isAlive":0.33891781305051394,"gold":0.17130444248883492},"p03":{"health":0.333972497129205,"mana":0.41194358414673776,"position":0.46934706705876467,"level":0.2441901180329917,"isAlive":0.2652064524985,"gold":0.2321997437616407},"p04":{"health":0.4156548363865571,"mana":0.43155222568980095,"position":0.5826150272856461,"level":0.13776690922312707,"isAlive":0.19196790095609453,"gold":0.1979549356074468},"p10":{"health":0.6416800831337001,"mana":0.40680711146741466,"position":0.4126179988298738,"level":0.4000911144373465,"isAlive":0.18388754270796812,"gold":0.22203723475306675},"p11":{"health":0.579554102972284,"mana":0.36198215689824764,"position":0.7471974569167754,"level":0.3816584604189382,"isAlive":0.22003713990314078,"gold":0.24437889306897204},"p12":{"health":0.5013381366328884,"mana":0.4020134850499623,"position":0.45930051530907456,"level":0.24757171217176155,"isAlive":0.3391237937247344,"gold":0.26792472405483375},"p13":{"health":0.3782245324890544,"mana":0.43416155589052535,"position":0.5103533621363344,"level":0.18464115993309752,"isAlive":0.2694904205827981,"gold":0.2948239226145229},"p14":{"health":0.6384741651870282,"mana":0.6607509382359841,"position":0.8023071206079802,"level":0.3556322439529603,"isAlive":0.23327651230739993,"gold":0.20359398271777346},"t0":{"towerTop1":0.44161926613385294,"towerTop2":0.48715997672670897,"towerTop3":0.3348950296913794,"towerMid1":0.3434239405872137,"towerMid2":0.3183817611816545,"towerMid3":0.3384348468591934,"towerBot1":0.24491087429255542,"towerBot2":0.24440080835412165,"towerBot3":0.23700303465227046,"towerBase1":0.10837067293675517,"towerBase2":0.22194049581811248,"creepTop":0.614202901557551,"creepMid":0.6571708534050936,"creepBot":0.24867430885443192},"t1":{"towerTop1":0.2502091659561785,"towerTop2":0.19990383867128464,"towerTop3":0.13000511179206112,"towerMid1":0.5596339477385485,"towerMid2":0.4619857513311966,"towerMid3":0.3373192269599187,"towerBot1":0.23841407876263415,"towerBot2":0.16487477613042,"towerBot3":0.20598892374846034,"towerBase1":0.1810012641026803,"towerBase2":0.12139016567160138,"creepTop":0.6041024060935917,"creepMid":0.7520678424518137,"creepBot":0.13591902335885236},"g":{"gameTime":0.7351473802349183,"isNight":0.833725578099489,"roshanHP":0.17764610424281047}}},{"idx":16,"trajectory":[[15305.344,14616.906,16512],[15157.169070049442,14520.882587142994],[15026.27428392037,14399.318164311917],[14943.381238179034,14236.993204383147],[14899.526629200634,14079.404242258082],[14850.82328253042,13919.997529799355],[14818.218863330387,13733.635294937885],[14775.662079605956,13572.25077344569],[14698.192237237343,13398.646886075236],[14683.966746736272,13231.105894915381],[14685.0432803911,13060.552461697393]],"probability":0.030329268725135493,"attention":{"p00":{"health":0.3440070478093449,"mana":0.595063389340635,"position":0.5090561220843812,"level":0.4494402897709793,"isAlive":0.8480780312181131,"gold":0.6198451764564212},"p01":{"health":0.258355977752599,"mana":0.6796197227492002,"position":0.7958065440791229,"level":0.9774146808588178,"isAlive":0.6346722338200455,"gold":0.28040935077112406},"p02":{"health":0.9834282539969756,"mana":0.20998388984437466,"position":0.9245704573999403,"level":0.3676164780869644,"isAlive":0.44034966923598184,"gold":0.7868170625481243},"p03":{"health":0.08850546902614398,"mana":0.8947369133994201,"position":0.2586192719492688,"level":0.0815709474702826,"isAlive":0.9888985957901768,"gold":0.3311819718246878},"p04":{"health":0.4481140204442189,"mana":0.7529407622048732,"position":0.20058399493791823,"level":0.16165457432277264,"isAlive":0.7462522560693323,"gold":0.9529306169631238},"p10":{"health":0.2774907406140259,"mana":0.7203805505130749,"position":0.3865164060002588,"level":0.8939645580300069,"isAlive":0.4557123121370348,"gold":0.836978468212942},"p11":{"health":0.1315138951304331,"mana":0.1590237465486679,"position":0.6839210568183876,"level":0.28526971426544323,"isAlive":0.3027990663364519,"gold":0.5482098501309702},"p12":{"health":0.23639035101079342,"mana":0.006329499792627091,"position":0.16813779904957626,"level":0.34254497156650343,"isAlive":0.09393699963242086,"gold":0.573522940130073},"p13":{"health":0.23974403343481243,"mana":0.49673340499435326,"position":0.4039483243236619,"level":0.3788627948341139,"isAlive":0.69613489025852,"gold":0.13197540631797144},"p14":{"health":0.8981717182652713,"mana":0.3658083200030551,"position":0.88912288650837,"level":0.6739810792247063,"isAlive":0.11123196195351825,"gold":0.7767288042893914},"t0":{"towerTop1":0.13431667946314607,"towerTop2":0.898921133329859,"towerTop3":0.668054036550225,"towerMid1":0.6312005429287639,"towerMid2":0.051057223001168994,"towerMid3":0.045394617647395163,"towerBot1":0.36087886753080545,"towerBot2":0.8538405361634107,"towerBot3":0.8410131119801765,"towerBase1":0.13758196629621877,"towerBase2":0.509154493985478,"creepTop":0.7629167298467996,"creepMid":0.7888898295022919,"creepBot":0.2942713806104835},"t1":{"towerTop1":0.8665832725707105,"towerTop2":0.9244163605253541,"towerTop3":0.67560537044946,"towerMid1":0.692891948484488,"towerMid2":0.7765625544596413,"towerMid3":0.8140631503408355,"towerBot1":0.4694997587277303,"towerBot2":0.4347079727413228,"towerBot3":0.10253751896342256,"towerBase1":0.7880882903536865,"towerBase2":0.3933479447402686,"creepTop":0.9510424574937562,"creepMid":0.6782466445077835,"creepBot":0.5741349217364022},"g":{"gameTime":0.5836078721265436,"isNight":0.7641118485499068,"roshanHP":0.12729697973763243}}},{"idx":17,"trajectory":[[15305.344,14616.906,16512],[15136.956566124562,14662.043396814597],[14951.868949050135,14647.441137481295],[14763.68899005315,14627.264623476764],[14585.522353368524,14585.455027345653],[14417.247181831208,14603.297293986714],[14260.37107589567,14684.765990664479],[14130.808373981536,14811.045827779802],[14010.354508926199,14956.02934696327],[13953.729274664993,15111.456230617492],[13931.74935318947,15287.664336357471]],"probability":0.026974619003867256,"attention":{"p00":{"health":0.3595274671299634,"mana":0.25797638609639173,"position":0.2807407386103985,"level":0.4586734234742584,"isAlive":0.1821383568765589,"gold":0.2515101920677091},"p01":{"health":0.8411312808496526,"mana":0.36058500963000645,"position":0.3458413557882863,"level":0.246963478592416,"isAlive":0.2218188369924156,"gold":0.15981908530204264},"p02":{"health":0.9086635663682346,"mana":0.6500740670656244,"position":0.8921870597348518,"level":0.39800499207105855,"isAlive":0.2963788991966688,"gold":0.25723880118652703},"p03":{"health":0.8358046890868902,"mana":0.7800452997322331,"position":0.9617051113004674,"level":0.4082302155499964,"isAlive":0.2913021552134908,"gold":0.3485646560527731},"p04":{"health":0.560654658394891,"mana":0.44834383251917076,"position":0.9890201832297052,"level":0.3209543080522614,"isAlive":0.18042642919284158,"gold":0.12220932699167623},"p10":{"health":0.2899107731018086,"mana":0.347186573123111,"position":0.5419910904316041,"level":0.5192044223689575,"isAlive":0.2775291584181255,"gold":0.2356847738005474},"p11":{"health":0.6065417578241388,"mana":0.33551129634603016,"position":0.5352045821045432,"level":0.2952540802007441,"isAlive":0.22223548568802376,"gold":0.3146157689098749},"p12":{"health":0.3390510026411503,"mana":0.320295957591914,"position":0.3669103732648794,"level":0.26724809160913005,"isAlive":0.3444785082093009,"gold":0.25741101013059836},"p13":{"health":0.8411925705341697,"mana":0.5014655206620917,"position":0.9359605516434694,"level":0.5276585747354903,"isAlive":0.19899178847334617,"gold":0.26334080388806824},"p14":{"health":0.4320387115733608,"mana":0.5791591782640965,"position":0.656401235230523,"level":0.46438861367711876,"isAlive":0.1523400687132687,"gold":0.197710931545155},"t0":{"towerTop1":0.414111805897185,"towerTop2":0.4497376043741563,"towerTop3":0.39920853493146097,"towerMid1":0.419977303345598,"towerMid2":0.42517554171866745,"towerMid3":0.3965265874146935,"towerBot1":0.20714895976303294,"towerBot2":0.25583653822125957,"towerBot3":0.2743540837672108,"towerBase1":0.15945212974373082,"towerBase2":0.1411925146379216,"creepTop":0.5131727268371653,"creepMid":0.7810950485918996,"creepBot":0.18073990357072997},"t1":{"towerTop1":0.33713084991714715,"towerTop2":0.22853138725926012,"towerTop3":0.189512977233383,"towerMid1":0.3299149015673551,"towerMid2":0.3822756802449446,"towerMid3":0.3395519301187792,"towerBot1":0.25485582048261085,"towerBot2":0.1798009074713554,"towerBot3":0.2809418637157889,"towerBase1":0.17297606772700394,"towerBase2":0.09718308929776462,"creepTop":0.5806212192182744,"creepMid":0.7473926539390923,"creepBot":0.14847882150955105},"g":{"gameTime":0.5844138768705289,"isNight":0.7884168938565267,"roshanHP":0.22043885114854392}}},{"idx":18,"trajectory":[[15305.344,14616.906,16512],[15281.240495609465,14782.851908283565],[15216.65493536916,14945.150512012331],[15103.383502940898,15070.048036628066],[15031.915736262234,15212.550208925362],[15007.173570049723,15371.717330636486],[15027.893313353981,15553.6059644707],[15070.637495218025,15732.260564696404],[15119.423532050385,15904.617311272148],[15200.182683480847,16073.809009717726],[15241.672299256446,16232.190536343925]],"probability":0.045583622667980744,"attention":{"p00":{"health":0.2656534915628879,"mana":0.261708411029047,"position":0.3025842067721852,"level":0.4104571969838155,"isAlive":0.138113220232901,"gold":0.23948870626657798},"p01":{"health":0.6941011845168511,"mana":0.36320463066431413,"position":0.44121375853054123,"level":0.16703048691746536,"isAlive":0.2765382811541204,"gold":0.21341878376028958},"p02":{"health":0.41536101305622775,"mana":0.35771315817439575,"position":0.5712441292575292,"level":0.2639993261258292,"isAlive":0.3807027407612372,"gold":0.2606050291100962},"p03":{"health":0.46134159690486093,"mana":0.4429925257659957,"position":0.6481675639313027,"level":0.2089232816332713,"isAlive":0.36210567843609354,"gold":0.22484054992787733},"p04":{"health":0.49014957713286283,"mana":0.4483879734578239,"position":0.6696393834349218,"level":0.3287209368680047,"isAlive":0.20265995853878782,"gold":0.16392779987168823},"p10":{"health":0.6451109074278227,"mana":0.3031728182047572,"position":0.544989687477201,"level":0.3248378157955526,"isAlive":0.2529732023701076,"gold":0.3090480207374914},"p11":{"health":0.6371657020510723,"mana":0.47071671519133346,"position":0.7638982257672067,"level":0.37526592757518296,"isAlive":0.22076035185656898,"gold":0.26602514741379696},"p12":{"health":0.42471369534657777,"mana":0.3626110853922054,"position":0.4902208504746906,"level":0.2690227477466788,"isAlive":0.37239376885495007,"gold":0.3139940746796089},"p13":{"health":0.3579149006559339,"mana":0.3363826508125638,"position":0.6162529977653902,"level":0.15670163251637986,"isAlive":0.24556067547152086,"gold":0.23163883897649865},"p14":{"health":0.6557646496915782,"mana":0.6927961916591067,"position":0.7489130924761697,"level":0.33675883618838387,"isAlive":0.290353068104088,"gold":0.21924776715390493},"t0":{"towerTop1":0.5026185938613559,"towerTop2":0.4113771727980869,"towerTop3":0.4476224601523261,"towerMid1":0.45223175842633506,"towerMid2":0.36103239565212225,"towerMid3":0.3849470030247165,"towerBot1":0.22278815385119408,"towerBot2":0.21848281378251577,"towerBot3":0.2574595274244205,"towerBase1":0.09159839345328068,"towerBase2":0.18215995318084902,"creepTop":0.4863713947043468,"creepMid":0.7955144682016251,"creepBot":0.1789977699740018},"t1":{"towerTop1":0.3070434200629349,"towerTop2":0.17123728099762417,"towerTop3":0.25966112478022507,"towerMid1":0.5163391131851822,"towerMid2":0.34524323965055015,"towerMid3":0.3103289729853208,"towerBot1":0.3009133556634109,"towerBot2":0.18400983212746194,"towerBot3":0.2515352234478131,"towerBase1":0.2061368455931175,"towerBase2":0.12924536967319428,"creepTop":0.5181453375170548,"creepMid":0.783601895858703,"creepBot":0.13020516439023289},"g":{"gameTime":0.6759134806996197,"isNight":0.8616854757505109,"roshanHP":0.18225670801505733}}},{"idx":19,"trajectory":[[15305.344,14616.906,16512],[15467.014967515921,14633.653872539096],[15625.394404528894,14652.244820506916],[15798.718085499404,14701.364452295715],[15980.621266530297,14686.614562748493],[16139.185822413412,14659.858442405128],[16312.84670397293,14661.908970587483],[16478.586217231037,14602.44948724784],[16625.44046726587,14524.40991107788],[16791.194812598278,14495.797196244228],[16958.465528038123,14496.47610405086]],"probability":0.03837728745989054,"attention":{"p00":{"health":0.5495399166133639,"mana":0.279262416468004,"position":0.27777857184812316,"level":0.941639655005085,"isAlive":0.8304099303328558,"gold":0.8085555934368929},"p01":{"health":0.9337301204798505,"mana":0.9831558290272346,"position":0.3168875303371599,"level":0.30863999094573336,"isAlive":0.49569337115326984,"gold":0.7069376134877594},"p02":{"health":0.3095013523252552,"mana":0.4337545134045715,"position":0.13912820987032237,"level":0.7102085320648157,"isAlive":0.9061636262191264,"gold":0.9560092942425584},"p03":{"health":0.6204108103032318,"mana":0.9972512224446068,"position":0.9836301330214692,"level":0.8360314157377058,"isAlive":0.8765469555155494,"gold":0.2782413295236652},"p04":{"health":0.09146853546118638,"mana":0.8780661451609204,"position":0.6386260189429951,"level":0.23232656382230332,"isAlive":0.9378470667066701,"gold":0.5709917960987176},"p10":{"health":0.18423644316514576,"mana":0.4121993577630365,"position":0.5704965389519985,"level":0.6221938602421855,"isAlive":0.686171726754556,"gold":0.8237107853606607},"p11":{"health":0.7275930378635955,"mana":0.42555260864182376,"position":0.7288405206776083,"level":0.02676580135853901,"isAlive":0.1921353998491373,"gold":0.7130679495136454},"p12":{"health":0.4528970664052425,"mana":0.26585060425756013,"position":0.07584978853487656,"level":0.7982849605475748,"isAlive":0.3896988318113135,"gold":0.6094542982069695},"p13":{"health":0.7797469163115143,"mana":0.6631511386297293,"position":0.4533878687220083,"level":0.30303079647725895,"isAlive":0.5769213038499186,"gold":0.7514537714839198},"p14":{"health":0.3996542941928052,"mana":0.9412665771787083,"position":0.6106853535980981,"level":0.9627456193947095,"isAlive":0.2360970183862081,"gold":0.16264723411222315},"t0":{"towerTop1":0.8537543785574404,"towerTop2":0.812509314703673,"towerTop3":0.1262083252659989,"towerMid1":0.8120431004460489,"towerMid2":0.2563541746006064,"towerMid3":0.490479702046684,"towerBot1":0.4470461971395574,"towerBot2":0.6590894047906193,"towerBot3":0.5044728945627253,"towerBase1":0.6486182881435101,"towerBase2":0.4232583397320049,"creepTop":0.06680045802341561,"creepMid":0.566903701233701,"creepBot":0.3015833855854295},"t1":{"towerTop1":0.33144218139389126,"towerTop2":0.21444799706643058,"towerTop3":0.27308239408010637,"towerMid1":0.9667236229681786,"towerMid2":0.731535200884907,"towerMid3":0.5720913253214985,"towerBot1":0.6352559977990495,"towerBot2":0.9652212663735231,"towerBot3":0.9561183743937451,"towerBase1":0.7245045577395888,"towerBase2":0.3935448122255256,"creepTop":0.3354802765609064,"creepMid":0.8648506310920689,"creepBot":0.5953549225482291},"g":{"gameTime":0.367710960646231,"isNight":0.9989377530025743,"roshanHP":0.04886098816197593}}}],"predictionGroups":[[9,15,18,13],[6,11,8,4,12],[2,14,3,19],[0,5,17,10,1],[7,16]],"selectedPredictors":[9,15,18,13],"comparedPredictors":[0,5,17,10,1],"predictionProjection":[[0.5113236573139297,0.3606271115173276,1],[0.586504138406687,0.3297775841885817,1],[0.07119346336663251,0.6196651560778915,1],[0.12236092397077544,0.47831195725219666,1],[0.43565831450851705,0.7842565831575781,1],[0.3371620386441053,0.32852412249928437,1],[0.5081274993704166,0.7053186432849697,1],[0.9180391883546495,0.339025136883514,1],[0.501612907527434,0.8932149243692508,1],[0.7146419228985038,0.6866287502200296,1],[0.45485858113157956,0.25077749794603404,1],[0.3461916685162195,0.6998657292013938,1],[0.3673878731376763,0.8273164988404993,1],[0.8845205626757245,0.6777208807462707,1],[0.19366531356936972,0.5943307719659253,1],[0.865145147189564,0.6687370448699312,1],[1.0014905238818632,0.38812857395480505,1],[0.4337197614481949,0.2948105166430762,1],[0.7696358622394369,0.6151604414267494,1],[0.00834916430571947,0.5243347530844094,1]],"instancesData":{"stages":[{"groups":[[11,8,12,4,6],[14,3,19,2],[13,18,15,9],[16,7],[17,0,1,5,10]],"instances":169},{"groups":[[11,8,12,4,14],[6,3,19,2],[13,18,15,9,16],[7,17,0],[1,5,10]],"instances":132},{"groups":[[11,8,12,4,14,3,19],[6,2,13],[18],[15,9,16],[7,17,0],[1,5,10]],"instances":121},{"groups":[[11,8,12,4,14,3,19],[6,2,13],[18],[16],[15,9,1],[7,17,0],[5,10]],"instances":90},{"groups":[[11,8,12,4,14,3,19,1],[6,2,13],[18,16,15,17],[9],[7,0],[5,10]],"instances":66},{"groups":[[11,8,12,4,14,3,19,1],[13],[6,2],[18,16,15,17],[7],[9,0,5,10]],"instances":21},{"groups":[[11,8,12,4,14,3,19,1,13,6],[16,15,17],[2,18,7],[9,0,5,10]],"instances":10}],"numPredictors":20,"totalInstances":609},"mapStyle":"grey"} \ No newline at end of file diff --git a/case/secret_tundra.json b/case/secret_tundra.json new file mode 100644 index 0000000..a53ec20 --- /dev/null +++ b/case/secret_tundra.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cbe849bbea9134b858508be02c0bb5c1ba44eeb7f4149b9142f058bc1d01a56 +size 121563972 diff --git a/frontend/.gitattributes b/frontend/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/frontend/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..6ff6e79 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +# Data +*.json +*.psd + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7a51acb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,33 @@ +# RoadSign for DOTA 2 + +## How to start it + +1. install `NodeJS` +2. install `yarn` +3. run `yarn install` command +4. run `yarn run dev` command +5. visit `http://127.0.0.1:5173` + +## Main techniques used + +- [React](https://react.dev/): UI framework +- [MobX](https://mobx.js.org/README.html): Data management +- [MUI](https://mui.com/material-ui/getting-started/): UI elements +- [Konva](https://konvajs.org/docs/react/Intro.html): Canvas drawing +- [I18N](https://react.i18next.com/): Change the language (we need an English version in our paper, and a Chinese version for user study) + +## Project structures + +- `src/components` is for some common and reusable UI elements +- `src/model` declares the data structure +- `src/store` declares the data center +- `src/utils` consists of some tool functions +- `src/views` contains the main UI designs + +## Example files + +Here are some example files with detailed comments, which demonstrate how the code works. + +- `src/store/store.js` demonstrates how we manage the data +- `src/store/App.jsx` demonstrates how we render the views based on data +- `src/model/D2Data.d.ts` demonstrates how we define data types diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e1b9dac --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + TLens for DOTA 2 + + +
+ + + diff --git a/package.json b/frontend/package.json similarity index 90% rename from package.json rename to frontend/package.json index 2687846..efa4255 100644 --- a/package.json +++ b/frontend/package.json @@ -15,6 +15,9 @@ "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@mui/icons-material": "^5.14.13", "@mui/material": "^5.14.13", + "bubblesets-js": "^2.3.2", + "d3": "^7.8.5", + "file-saver": "^2.0.5", "heatmap.js": "^2.0.5", "hull.js": "^1.0.4", "i18next": "^23.5.1", @@ -26,6 +29,7 @@ "react-dom": "^18.2.0", "react-i18next": "^13.2.2", "react-konva": "^18.2.10", + "react-range-slider-input": "^3.0.7", "setimmediate": "^1.0.5", "yieldable-json": "^2.0.1" }, diff --git a/frontend/public/icons/Beastmaster.webp b/frontend/public/icons/Beastmaster.webp new file mode 100644 index 0000000..a7cf2de Binary files /dev/null and b/frontend/public/icons/Beastmaster.webp differ diff --git a/frontend/public/icons/Crystal Maiden.webp b/frontend/public/icons/Crystal Maiden.webp new file mode 100644 index 0000000..907a677 Binary files /dev/null and b/frontend/public/icons/Crystal Maiden.webp differ diff --git a/frontend/public/icons/Dire.png b/frontend/public/icons/Dire.png new file mode 100644 index 0000000..9be2fd3 Binary files /dev/null and b/frontend/public/icons/Dire.png differ diff --git a/frontend/public/icons/Dota.png b/frontend/public/icons/Dota.png new file mode 100644 index 0000000..18e345c Binary files /dev/null and b/frontend/public/icons/Dota.png differ diff --git a/frontend/public/icons/Earthshaker.webp b/frontend/public/icons/Earthshaker.webp new file mode 100644 index 0000000..20c7873 Binary files /dev/null and b/frontend/public/icons/Earthshaker.webp differ diff --git a/frontend/public/icons/Ember Spirit.webp b/frontend/public/icons/Ember Spirit.webp new file mode 100644 index 0000000..cc8f301 Binary files /dev/null and b/frontend/public/icons/Ember Spirit.webp differ diff --git a/frontend/public/icons/Huskar.webp b/frontend/public/icons/Huskar.webp new file mode 100644 index 0000000..128a3d7 Binary files /dev/null and b/frontend/public/icons/Huskar.webp differ diff --git a/frontend/public/icons/Legion Commander.webp b/frontend/public/icons/Legion Commander.webp new file mode 100644 index 0000000..a185c1b Binary files /dev/null and b/frontend/public/icons/Legion Commander.webp differ diff --git a/frontend/public/icons/Leshrac.webp b/frontend/public/icons/Leshrac.webp new file mode 100644 index 0000000..d80a72a Binary files /dev/null and b/frontend/public/icons/Leshrac.webp differ diff --git a/frontend/public/icons/Lich.webp b/frontend/public/icons/Lich.webp new file mode 100644 index 0000000..72b0e74 Binary files /dev/null and b/frontend/public/icons/Lich.webp differ diff --git a/frontend/public/icons/Marci.webp b/frontend/public/icons/Marci.webp new file mode 100644 index 0000000..509aaf9 Binary files /dev/null and b/frontend/public/icons/Marci.webp differ diff --git a/frontend/public/icons/Medusa.webp b/frontend/public/icons/Medusa.webp new file mode 100644 index 0000000..2474517 Binary files /dev/null and b/frontend/public/icons/Medusa.webp differ diff --git a/frontend/public/icons/Mirana.webp b/frontend/public/icons/Mirana.webp new file mode 100644 index 0000000..180dc9e Binary files /dev/null and b/frontend/public/icons/Mirana.webp differ diff --git a/frontend/public/icons/Naga Siren.webp b/frontend/public/icons/Naga Siren.webp new file mode 100644 index 0000000..f213271 Binary files /dev/null and b/frontend/public/icons/Naga Siren.webp differ diff --git a/frontend/public/icons/Pangolier.webp b/frontend/public/icons/Pangolier.webp new file mode 100644 index 0000000..c21facf Binary files /dev/null and b/frontend/public/icons/Pangolier.webp differ diff --git a/frontend/public/icons/Phantom Assassin.webp b/frontend/public/icons/Phantom Assassin.webp new file mode 100644 index 0000000..dde2568 Binary files /dev/null and b/frontend/public/icons/Phantom Assassin.webp differ diff --git a/frontend/public/icons/Radiant.png b/frontend/public/icons/Radiant.png new file mode 100644 index 0000000..470bbdc Binary files /dev/null and b/frontend/public/icons/Radiant.png differ diff --git a/frontend/public/icons/Silencer.webp b/frontend/public/icons/Silencer.webp new file mode 100644 index 0000000..fc8aa73 Binary files /dev/null and b/frontend/public/icons/Silencer.webp differ diff --git a/frontend/public/icons/Tiny.webp b/frontend/public/icons/Tiny.webp new file mode 100644 index 0000000..109f3cd Binary files /dev/null and b/frontend/public/icons/Tiny.webp differ diff --git a/frontend/public/icons/Troll Warlord.webp b/frontend/public/icons/Troll Warlord.webp new file mode 100644 index 0000000..064ae7c Binary files /dev/null and b/frontend/public/icons/Troll Warlord.webp differ diff --git a/frontend/public/icons/Tusk.webp b/frontend/public/icons/Tusk.webp new file mode 100644 index 0000000..51bbb8d Binary files /dev/null and b/frontend/public/icons/Tusk.webp differ diff --git a/frontend/public/icons/Windrunner.webp b/frontend/public/icons/Windrunner.webp new file mode 100644 index 0000000..221f4cf Binary files /dev/null and b/frontend/public/icons/Windrunner.webp differ diff --git a/frontend/public/icons/alive.png b/frontend/public/icons/alive.png new file mode 100644 index 0000000..e866ad8 Binary files /dev/null and b/frontend/public/icons/alive.png differ diff --git a/frontend/public/icons/dead.png b/frontend/public/icons/dead.png new file mode 100644 index 0000000..e1986b4 Binary files /dev/null and b/frontend/public/icons/dead.png differ diff --git a/frontend/public/icons/gold.png b/frontend/public/icons/gold.png new file mode 100644 index 0000000..6822947 Binary files /dev/null and b/frontend/public/icons/gold.png differ diff --git a/frontend/public/icons/hero.webp b/frontend/public/icons/hero.webp new file mode 100644 index 0000000..8f4183f Binary files /dev/null and b/frontend/public/icons/hero.webp differ diff --git a/public/map.jpeg b/frontend/public/map.jpeg similarity index 100% rename from public/map.jpeg rename to frontend/public/map.jpeg diff --git a/frontend/public/map_grey.jpg b/frontend/public/map_grey.jpg new file mode 100644 index 0000000..fea7f2e Binary files /dev/null and b/frontend/public/map_grey.jpg differ diff --git a/frontend/public/map_no_color.jpg b/frontend/public/map_no_color.jpg new file mode 100644 index 0000000..cbc75d0 Binary files /dev/null and b/frontend/public/map_no_color.jpg differ diff --git a/frontend/public/minimap.jpg b/frontend/public/minimap.jpg new file mode 100644 index 0000000..ace23f2 Binary files /dev/null and b/frontend/public/minimap.jpg differ diff --git a/public/vite.svg b/frontend/public/vite.svg similarity index 100% rename from public/vite.svg rename to frontend/public/vite.svg diff --git a/src/App.jsx b/frontend/src/App.jsx similarity index 56% rename from src/App.jsx rename to frontend/src/App.jsx index 78d8ce7..365a9ae 100644 --- a/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,17 +2,20 @@ import View from "./components/View"; import TitleBar from "./views/TitleBar"; import {useLayout} from "./utils/layout"; import {styled} from "@mui/material/styles"; -import PlayerSelection from "./views/MapView/PlayerSelection"; -import {Button, createTheme, CssBaseline, Divider, ThemeProvider} from "@mui/material"; -import Timeline from "./views/MapView/Timeline.jsx"; -import MapRenderer from "./views/MapView/MapRenderer.jsx"; +import PlayerSelection from "./views/MapView/Map/PlayerSelection.jsx"; +import {createTheme, CssBaseline, Divider, IconButton, ThemeProvider, Tooltip, Typography} from "@mui/material"; +import Timeline from "./views/MapView/Timeline/Timeline.jsx"; +import {MapContextRenderer, MapRenderer} from "./views/MapView/index.jsx"; import {inject, observer} from "mobx-react"; import Waiting from "./components/Waiting.jsx"; import {useTranslation} from "react-i18next"; import StrategyView from "./views/StrategyView/index.jsx"; import ContextView from "./views/ContextView/index.jsx"; -import {defaultTheme} from "./utils/theme.js"; -import React from "react"; +import {defaultTheme, selectionColor} from "./utils/theme.js"; +import MapLegendTrigger from "./views/MapView/Legend/index.jsx"; +import {DisabledByDefault} from "@mui/icons-material"; +import {LassoIcon, ShiftIcon} from "./views/StrategyView/Projection/Icons.jsx"; +import ContextSortMenu from "./views/ContextView/SortMenu.jsx"; // React本质上就是用函数表达从数据到视图的映射,每一个不同的映射称为一个组件。 // 当数据发生变化时,React会自动处理视图的变化,并刷新组件的渲染。 @@ -37,35 +40,44 @@ function App({store}) { // 使用类似HTML的方式(即JSX的方式),形象地返回结果(和D3相比,更容易看懂了) // Root是下方定义的styled component,TitleBar/View/Waiting都是其他文件定义的组件 - return + return - , - + , + , ]}> - + }/> - {t('System.StrategyView.Predict')} - + <> + + {t('System.StrategyView.View')} + , + <> + + {t('System.StrategyView.Compare')} + , ]}> - + - Clear Context Limit - + + + , + + + + + , ]}> diff --git a/src/api/API.d.ts b/frontend/src/api/API.d.ts similarity index 66% rename from src/api/API.d.ts rename to frontend/src/api/API.d.ts index 5ba0056..4ae756a 100644 --- a/src/api/API.d.ts +++ b/frontend/src/api/API.d.ts @@ -1,4 +1,4 @@ -import {Prediction} from "../model/Strategy"; +import {Prediction, PredictorsStoryline} from "../model/Strategy"; export interface PredictionRequest { gameName: string, @@ -11,4 +11,6 @@ export interface PredictionRequest { export interface PredictionResponse { predictions: Prediction[] predGroups: number[][] + predProjection: [number, number][] + predInstances: PredictorsStoryline } \ No newline at end of file diff --git a/src/api/api.js b/frontend/src/api/api.js similarity index 86% rename from src/api/api.js rename to frontend/src/api/api.js index 05367d0..c8ab993 100644 --- a/src/api/api.js +++ b/frontend/src/api/api.js @@ -1,4 +1,4 @@ -const url = uri => `http://10.76.6.131:8787${uri}`; +const url = uri => `http://127.0.0.1:5000${uri}`; const api = { /** diff --git a/src/components/KonvaHeatmap.jsx b/frontend/src/components/KonvaHeatmap.jsx similarity index 100% rename from src/components/KonvaHeatmap.jsx rename to frontend/src/components/KonvaHeatmap.jsx diff --git a/src/components/KonvaImage.jsx b/frontend/src/components/KonvaImage.jsx similarity index 84% rename from src/components/KonvaImage.jsx rename to frontend/src/components/KonvaImage.jsx index 8a5a7fe..c19462f 100644 --- a/src/components/KonvaImage.jsx +++ b/frontend/src/components/KonvaImage.jsx @@ -4,9 +4,12 @@ import {Image} from 'react-konva'; function useImage(src, onLoad) { const image = useRef(new window.Image()); useEffect(() => { - image.current.src = src; image.current.addEventListener('load', onLoad); - }, [src]); + return () => image.current.removeEventListener('load', onLoad); + }, [onLoad]); + useEffect(() => { + image.current.src = src; + }, [src, onLoad]); return image.current; } diff --git a/src/components/View.jsx b/frontend/src/components/View.jsx similarity index 98% rename from src/components/View.jsx rename to frontend/src/components/View.jsx index 6c314c9..c82a84e 100644 --- a/src/components/View.jsx +++ b/frontend/src/components/View.jsx @@ -70,6 +70,8 @@ const Toolbar = styled('div')(({theme}) => ({ })); const Tool = styled('div')(({theme}) => ({ + display: 'flex', + alignItems: 'center', marginRight: theme.spacing(1), })); diff --git a/src/components/Waiting.jsx b/frontend/src/components/Waiting.jsx similarity index 100% rename from src/components/Waiting.jsx rename to frontend/src/components/Waiting.jsx diff --git a/src/i18n.js b/frontend/src/i18n.js similarity index 100% rename from src/i18n.js rename to frontend/src/i18n.js diff --git a/src/index.css b/frontend/src/index.css similarity index 88% rename from src/index.css rename to frontend/src/index.css index cc8f7bb..2e34680 100644 --- a/src/index.css +++ b/frontend/src/index.css @@ -6,3 +6,4 @@ html, body, #root { padding: 0; user-select: none; } +/* index.css */ diff --git a/src/main.jsx b/frontend/src/main.jsx similarity index 85% rename from src/main.jsx rename to frontend/src/main.jsx index 7841c44..5d1fd73 100644 --- a/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,12 +3,10 @@ import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' import {Provider} from "mobx-react"; -import Store from "./store/store.js"; +import {store} from "./store/store.js"; import 'setimmediate' import './i18n' -const store = new Store(); - ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/frontend/src/model/Context.d.ts b/frontend/src/model/Context.d.ts new file mode 100644 index 0000000..b5e60c2 --- /dev/null +++ b/frontend/src/model/Context.d.ts @@ -0,0 +1,7 @@ +export type ContextValue = number | string | boolean | number[] | undefined + +export interface Context { + [groupName: string]: { + [itemName: string]: ContextValue + } +} \ No newline at end of file diff --git a/src/model/D2Data.d.ts b/frontend/src/model/D2Data.d.ts similarity index 100% rename from src/model/D2Data.d.ts rename to frontend/src/model/D2Data.d.ts diff --git a/src/model/D2DataEvents.d.ts b/frontend/src/model/D2DataEvents.d.ts similarity index 100% rename from src/model/D2DataEvents.d.ts rename to frontend/src/model/D2DataEvents.d.ts diff --git a/src/model/Strategy.d.ts b/frontend/src/model/Strategy.d.ts similarity index 66% rename from src/model/Strategy.d.ts rename to frontend/src/model/Strategy.d.ts index 24560b7..b11c757 100644 --- a/src/model/Strategy.d.ts +++ b/frontend/src/model/Strategy.d.ts @@ -3,14 +3,17 @@ export type Trajectory = Position[] export type AttentionStruct = Record> export type SingleAttention = AttentionStruct + export interface AttentionRange { avg: number min: number max: number } + export type StratAttention = AttentionStruct export interface Prediction { + idx: number, trajectory: Trajectory probability: number attention: SingleAttention @@ -20,3 +23,16 @@ export interface Strategy { predictors: Prediction[] attention: StratAttention } + +export type StorylineGroup = number[] + +export interface StorylineStage { + groups: StorylineGroup[] + instances: number +} + +export interface PredictorsStoryline { + stages: StorylineStage[], + numPredictors: number, + totalInstances: number, +} diff --git a/frontend/src/model/System.d.ts b/frontend/src/model/System.d.ts new file mode 100644 index 0000000..82c74f9 --- /dev/null +++ b/frontend/src/model/System.d.ts @@ -0,0 +1,7 @@ +export interface MatrixCell { + probability: number + avgDirection: [number, number], + dirRange: number[], + // @ts-ignore + predictionIdxes: Set +} \ No newline at end of file diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js new file mode 100644 index 0000000..b9649af --- /dev/null +++ b/frontend/src/store/store.js @@ -0,0 +1,556 @@ +import {computed, makeAutoObservable, observable} from "mobx"; +import {genContext, playerColors, updateGameData} from "../utils/game.js"; +import {contextFactory, genProjection, genRandomStrategies, getStratAttention, shuffle} from "../utils/fakeData.js"; +import api from "../api/api.js"; +import {hashFileName} from "../utils/file.js"; +import {saveAs} from 'file-saver'; +import genStorylineData, {initStorylineData} from "../views/StrategyView/Storyline/useData.js"; +import discretize from "../utils/discretize.js"; +import newArr from "../utils/newArr.js"; +import {joinSet} from "../utils/set.js"; +import {rot} from "../utils/rot.js"; +import {createContext, useContext} from "react"; + +class Store { + constructor() { + // 此处是利用了MobX的监听机制,makeAutoObservable会按照以下规则,处理该类中定义的变量与函数 + // 1. 该类中的变量将被设置为observable,每当监听到变量改变,引用变量的视图将自动刷新 + // 3. 该类中的getter函数将被设置为computed数值缓存,即根据其他observable的变量计算出一个新的值;当依赖变量改变,缓存值将被重新计算 + // 2. 该类中的其他函数将被设置为action,只有action中修改变量会被监听,并触发computed值的重新计算 + + // 此处的gameData额外设置为observable.shallow有两个原因。 + // 1. observable会监听整个hierarchical的结构,observable.shallow只会监听根部引用的改变,提高效率 + // 2. gameData导入后是不变的,因此没必要监听其内部元素变化 + makeAutoObservable(this, { + gameData: observable.shallow, + predictionGroups: observable.shallow, + predictionProjection: observable.shallow, + instancesData: observable.shallow, + }); + + this.setDevMode(window.localStorage.getItem('dev') === 'true'); + } + + //region system + /** + * 系统状态,在执行一些费时的操作时,如果不希望用户在此期间和系统交互,可以设置waiting为true + * @type {boolean} + */ + waiting = false + setWaiting = state => this.waiting = state; + + devMode = false + setDevMode = dev => { + this.devMode = dev; + window.localStorage.setItem('dev', dev.toString()); + } + + mapStyle = 'colored' + setMapStyle = style => this.mapStyle = style; + + get mapImage() { + return { + 'colored': './map.jpeg', + 'sketch': './map_no_color.jpg', + 'grey': './map_grey.jpg' + }[this.mapStyle] || './map.jpeg'; + } + + workerTags = newArr(20, () => new Set()); + clusterTags = workers => computed(() => Object.fromEntries(workers.map(wId => [wId, Array.from(this.workerTags[wId])]))).get() + addTag = (idx, tag) => idx.forEach(i => this.workerTags[i].add(tag)); + removeTag = (idx, tag) => idx.forEach(i => this.workerTags[i].delete(tag)); + setTags = (idx, newTags, oldTags) => { + for (const tag of newTags) + if (!oldTags.has(tag)) this.addTag(idx, tag); + for (const tag of oldTags) + if (!newTags.has(tag)) this.removeTag(idx, tag); + } + initTags = (tags) => this.workerTags = tags.map(tag => new Set(tag)); + saveTags = () => this.workerTags.map(tagSet => Array.from(tagSet)); + + contextSort = 'default'; + setContextSort = cs => this.contextSort = cs; + autoDetermineContextSort = () => { + if (this.selectedPredictors.length === 0 && this.comparedPredictors.length === 0) this.setContextSort('default'); + else if (this.selectedPredictors.length !== 0 && this.comparedPredictors.length !== 0) this.setContextSort('highDiffFirst'); + else this.setContextSort('highAttFirst'); + } + + viewedTime = -1; + setViewedTime = t => this.viewedTime = t; + //endregion + + //region game context + /** + * Game gameData instance + * @type {import('src/model/D2Data.d.ts').D2Data | null} + */ + gameData = null; + gameName = ''; + /** + * Import gameData + * @param {string} filename + * @param {import('src/model/D2Data.d.ts').D2Data} gameData + */ + setData = (filename, gameData) => { + this.gameName = filename.substring(0, filename.length - 5); + this.gameData = updateGameData(gameData); + this.focusOnPlayer(-1, -1); + this.setFrame(0); + this.clearPredictions(); + this.clearContextLimit(); + } + + /** + * 从gameData中提取玩家名 + * @returns {string[][]} + */ + get playerNames() { + if (this.gameData === null) return [['', '', '', '', ''], ['', '', '', '', '']] + return [ + this.gameData.gameInfo.radiant.players.map(p => p.hero), + this.gameData.gameInfo.dire.players.map(p => p.hero), + ] + } + + /** + * 从gameData中提取总的数据帧数 + * @returns {number} + */ + get numFrames() { + if (this.gameData === null) return 0; + return this.gameData.gameRecords.length; + } + + /** + * 从gameData中提取第frame帧的玩家位置 + * @return {[number, number][][]} : the positions of 2*5 players + */ + get playerPositions() { + if (!this.gameData) return [ + newArr(5, () => [0, 0]), + newArr(5, () => [0, 0]) + ] + const curFrame = this.gameData.gameRecords[this.frame]; + return curFrame.heroStates.map(team => + team.map(player => + player.pos + ) + ) + } + + get focusedPlayerPosition() { + if (this.focusedTeam === -1 || this.focusedPlayer === -1) return [0, 0]; + return this.playerPositions[this.focusedTeam][this.focusedPlayer]; + } + + /** + * 从gameData中提取第frame帧的玩家存活状态 + * @return {boolean[][]}: life state of 2*5 players + */ + get playerLifeStates() { + if (!this.gameData) return [ + newArr(5, false), + newArr(5, false), + ] + const curFrame = this.gameData.gameRecords[this.frame]; + return curFrame.heroStates.map(team => + team.map(player => + player.life === 0 + ) + ) + } + + /** + * 当前选中的队伍 + * @type {-1 | 0 | 1} : -1 for none, 0 for team radiant, 1 for team dire + */ + focusedTeam = -1 + /** + * 当前选中的玩家 + * @type {-1 | 0 | 1 | 2 | 3 | 4} : -1 for none, 0~4 for five playerNames + */ + focusedPlayer = -1 + /** + * 选中一名玩家,预测他的行为 + * @param {-1 | 0 | 1} teamIndex + * @param {-1 | 0 | 1 | 2 | 3 | 4} playerIndex + */ + focusOnPlayer = (teamIndex, playerIndex) => { + if (teamIndex === this.focusedTeam && playerIndex === this.focusedPlayer) { + this.focusedTeam = -1; + this.focusedPlayer = -1; + } else { + this.focusedTeam = teamIndex; + this.focusedPlayer = playerIndex; + } + this.clearPredictions(); + this.clearContextLimit(); + } + + get curColor() { + if (this.focusedTeam === -1 || this.focusedPlayer === -1) return undefined; + return playerColors[this.focusedTeam][this.focusedPlayer]; + } + + /** + * 当前帧 + * @type {number} + */ + frame = 0; + trajTimeWindow = [0, 150]; + setFrame = f => { + this.frame = f; + this.clearPredictions(); + this.clearContextLimit(); + } + setTrajTimeWindow = w => { + if (this.frame + w[0] < 0 || this.frame + w[1] >= this.numFrames) return; + this.trajTimeWindow = w; + } + + /** + * 从gameData中提取第frame帧的游戏中时间(单位:秒) + * @return {number}:从-90~0为比赛正式开始前的准备时间,0~+∞是比赛正式进行的时间。 + */ + get curTime() { + if (!this.gameData) return 0; + const curFrame = this.gameData.gameRecords[this.frame]; + return curFrame.game_time; + } + + frameTime = f => computed(() => { + if (!this.gameData) return 0; + const frame = this.gameData.gameRecords[f]; + return frame.game_time; + }).get(); + + /** + * @return {import('src/model/Context.js').Context} + */ + get curContext() { + return genContext(this.gameData, this.frame); + } + + contextLimit = new Set() + hasContextLimit = (ctxGroup, ctxItem) => computed(() => + this.contextLimit.has(`${ctxGroup}|||${ctxItem}`) + ).get(); + addContextLimit = (ctxGroup, ctxItem) => this.contextLimit.add(`${ctxGroup}|||${ctxItem}`); + rmContextLimit = (ctxGroup, ctxItem) => this.contextLimit.delete(`${ctxGroup}|||${ctxItem}`); + clearContextLimit = () => this.contextLimit.clear(); + setContextLimit = cl => this.contextLimit = new Set(cl); + + //endregion + + get contextLimitDict() { + let labels = store.playerNames[0].concat(store.playerNames[1]); + labels.push('Dire'); + labels.push('Environment'); + labels.push('Radiant'); + let limitDict = labels.reduce((acc, label) => { + acc[label] = 0; + return acc; + }, {}); + + this.contextLimit.forEach((c)=> { + for (let i = 0; i < labels.length; i++) { + if (c.split("|||")[0] === labels[i]){ + limitDict[labels[i]] +=1; + } + } + }); + console.log(limitDict); + return limitDict; + } + + get selectedPlayerTrajectory() { + // Check if a player is selected + if (this.focusedPlayer === -1 || !this.gameData) { + return [] + } + let selectedPlayerTra = [] + // for (var i=1; i <= this.gameData.gameRecords.length-1; i++) { + for (var i = Math.max(1, this.frame - 450); i <= Math.min(this.gameData.gameRecords.length - 1, this.frame + 150); i++) { + var pos = this.gameData.gameRecords[i].heroStates.map(team => + team.map(player => + player.pos + ) + ); + var pos3d = pos[this.focusedTeam][this.focusedPlayer]; + let pos2d = [pos3d[0], pos3d[1]]; + selectedPlayerTra.push(pos2d); + } + return selectedPlayerTra; + } + + get selectedPlayerTrajectoryInTimeWindow() { + // Check if a player is selected + if (this.focusedPlayer === -1 || !this.gameData) return []; + + return this.gameData.gameRecords + .slice(Math.max(1, this.frame + this.trajTimeWindow[0]), this.frame + this.trajTimeWindow[1]) + .map(gr => gr.heroStates[this.focusedTeam][this.focusedPlayer].pos.slice(0, 2)); + } + + get allPlayerTrajectory() { + let allPlayerTra = Array.from({length: 2}, () => + Array.from({length: 5}, () => []) + ); + + const startFrame = Math.max(1, this.frame - 450); + const endFrame = Math.min(this.gameData.gameRecords.length - 1, this.frame + 150); + + for (let i = startFrame; i <= endFrame; i++) { + const frameData = this.gameData.gameRecords[i].heroStates; + for (let teamIdx = 0; teamIdx < 2; teamIdx++) { + for (let playerIdx = 0; playerIdx < 5; playerIdx++) { + const playerPos = frameData[teamIdx][playerIdx].pos; + var pos = [playerPos[0], playerPos[1]] + allPlayerTra[teamIdx][playerIdx].push(pos); + } + } + } + // console.log(allPlayerTra) + return allPlayerTra; + } + + //region prediction + fakePredict = () => { + console.warn('Failed to connect to the backend. Using fake data instead.'); + const startPos = this.playerPositions[this.focusedTeam][this.focusedPlayer]; + const strategies = genRandomStrategies(startPos, this.curContext); + let i = 0; + const idx = newArr(20, i => i); + shuffle(idx); + const predGroups = strategies.map(strat => strat.predictors.map(() => idx[i++])); + const predictions = []; + i = 0; + strategies.forEach(strat => strat.predictors.forEach(p => predictions[idx[i++]] = p)); + return { + predictions, + predGroups, + predProjection: genProjection(predictions, predGroups), + predInstances: genStorylineData(predGroups), + } + } + predict = () => { + this.setWaiting(true); + new Promise((resolve, reject) => { + if (this.devMode) reject(); + else api.predict({ + gameName: this.gameName, + teamId: this.focusedTeam, + playerId: this.focusedPlayer, + frame: this.frame, + contextLimit: this.contextLimit, + }).catch(reject).then(resolve); + }).catch(this.fakePredict) + .then(res => { + this.setPredictions(res.predictions); + this.setPredGroups(res.predGroups); + this.setPredProjection(res.predProjection); + this.setInstancesData(res.predInstances); + }).finally(() => this.setWaiting(false)); + } + + /** + * Predictions + * @type {import('src/model/Strategy.d.ts').Prediction[]} + */ + predictions = [] + setPredictions = pred => { + this.predictions = pred.map((p, idx) => ({idx, ...p})); + } + + get viewedPrediction() { + if (this.viewedPredictions.length === 1) return this.viewedPredictions[0]; + else return -1; + } + + viewPrediction = p => this.viewPredictions(p === -1 ? [] : [p]); + /** + * hovered predictions + * @type {number[]} + */ + viewedPredictions = []; + viewPredictions = ps => { + const uniPs = Array.from(new Set(ps)); + if (uniPs.length === this.viewedPredictions.length && uniPs.every(p => this.viewedPredictions.includes(p))) return; + this.viewedPredictions = ps; + } + /** + * prediction groups + * @type {number[][]} + */ + predictionGroups = [] + setPredGroups = predG => this.predictionGroups = predG; + /** + * @type {number[]} + */ + selectedPredictors = []; + /** + * @type {number[]} + */ + comparedPredictors = []; + selectPredictors = (ps, group) => { + if (ps.length) this.setMapStyle('grey'); + else this.setMapStyle('colored'); + if (group === 1) this.comparedPredictors = ps; + else this.selectedPredictors = ps; + + this.autoDetermineContextSort(); + } + /** + * + * @type {[number, number][]} + */ + predictionProjection = [] + setPredProjection = pp => this.predictionProjection = pp; + instancesData = initStorylineData(); + setInstancesData = id => this.instancesData = id; + + /** + * @return {import('src/model/Strategy.d.ts').Strategy} + */ + strategyFromPredictions = ps => computed(() => { + const predictors = ps.map(i => this.predictions[i]); + if (predictors.length === 0) return null; + return { + predictors, + attention: contextFactory(this.curContext, (g, i) => getStratAttention(predictors, g, i)) + }; + }).get(); + + get selectedPredictorsAsAStrategy() { + return this.strategyFromPredictions(this.selectedPredictors); + } + + get comparedPredictorsAsAStrategy() { + return this.strategyFromPredictions(this.comparedPredictors); + } + + get viewedPredictorsAsAStrategy() { + return this.strategyFromPredictions(this.viewedPredictions); + } + + clearPredictions = () => { + this.setPredictions([]); + this.viewPrediction(-1); + this.setPredGroups([]); + this.selectPredictors([], 0); + this.selectPredictors([], 1); + this.setPredProjection([]) + this.setInstancesData(initStorylineData()); + } + + /** + * + * @param {[number, number]} xRange + * @param {[number, number]} yRange + * @param {number} numGrid + * @param {number} timeStep + * @param {import('src/model/Strategy.js').Strategy} strategy + * @return {[import('src/model/System.js').MatrixCell[][], import('src/model/System.js').MatrixCell[][]]} + */ + trajStat = (xRange, yRange, numGrid, timeStep, strategy) => computed(() => { + const xData = newArr(numGrid, () => newArr(timeStep, () => ({ + probability: 0, + avgDirection: [0, 0], + dirRange: [], + predictionIdxes: new Set(), + }))); + const yData = newArr(numGrid, () => newArr(timeStep, () => ({ + probability: 0, + avgDirection: [0, 0], + dirRange: [], + predictionIdxes: new Set(), + }))); + if (strategy) + for (const {probability, trajectory, idx} of strategy.predictors) { + for (let i = 0; i < trajectory.length - 1; i++) { + const tPos = discretize(i, [0, trajectory.length - 2], timeStep); + const xPos = discretize(trajectory[i][0], xRange, numGrid); + const yPos = discretize(trajectory[i][1], yRange, numGrid); + const dx = trajectory[i + 1][0] - trajectory[i][0]; + const dy = trajectory[i + 1][1] - trajectory[i][1]; + if (xPos !== -1) { + xData[xPos][tPos].probability += probability; + xData[xPos][tPos].avgDirection[0] += dx * probability; + xData[xPos][tPos].avgDirection[1] += dy * probability; + xData[xPos][tPos].dirRange.push(rot([dx, dy])); + xData[xPos][tPos].predictionIdxes.add(idx); + } + if (yPos !== -1) { + yData[yPos][tPos].probability += probability; + yData[yPos][tPos].avgDirection[0] += dx * probability; + yData[yPos][tPos].avgDirection[1] += dy * probability; + yData[yPos][tPos].dirRange.push(rot([dx, dy])); + yData[yPos][tPos].predictionIdxes.add(idx); + } + } + } + return [xData, yData]; + }).get(); + + //endregion + + setCase(data) { + this.initTags(data.tags); + this.focusOnPlayer(data.focusedTeam, data.focusedPlayer); + if (this.focusedPlayer === -1) this.focusOnPlayer(data.focusedTeam, data.focusedPlayer); + this.setFrame(data.frame); + this.setTrajTimeWindow(data.trajTimeWindow); + this.setContextLimit(data.contextLimit); + this.setPredictions(data.predictions); + this.viewPredictions([]); + this.setPredGroups(data.predictionGroups); + this.selectPredictors(data.selectedPredictors, 0); + this.selectPredictors(data.comparedPredictors || [], 1); + this.setPredProjection(data.predictionProjection); + this.setInstancesData(data.instancesData); + this.setContextSort(data.contextSort || this.contextSort); + this.setMapStyle(data.mapStyle || ( + (data.selectedPredictors.length || data.comparedPredictors.length) + ? 'grey' + : 'colored' + )) + } + + saveCase() { + const data = JSON.stringify({ + match_id: this.gameData.gameInfo.match_id, + tags: this.saveTags(), + contextSort: this.contextSort, + focusedTeam: this.focusedTeam, + focusedPlayer: this.focusedPlayer, + frame: this.frame, + trajTimeWindow: this.trajTimeWindow, + contextLimit: Array.from(this.contextLimit), + predictions: this.predictions, + predictionGroups: this.predictionGroups, + selectedPredictors: this.selectedPredictors, + comparedPredictors: this.comparedPredictors, + predictionProjection: this.predictionProjection, + instancesData: this.instancesData, + mapStyle: this.mapStyle, + }) + saveAs(new File( + [data], + `${hashFileName(this.gameName, data)}.json`, + {type: "text/plain;charset=utf-8"} + )) + } +} + +export default Store; + +export const store = new Store(); + +const StoreContext = createContext(store); + +/** + * @return {Store} + */ +export const useStore = () => useContext(StoreContext); diff --git a/frontend/src/utils/discretize.js b/frontend/src/utils/discretize.js new file mode 100644 index 0000000..fdbceed --- /dev/null +++ b/frontend/src/utils/discretize.js @@ -0,0 +1,4 @@ +export default function discretize(d, range, levels) { + if (d < range[0] || d > range[1]) return -1; + return Math.min(levels - 1, Math.floor((d - range[0]) / (range[1] - range[0]) * levels)); +} \ No newline at end of file diff --git a/frontend/src/utils/encoding.js b/frontend/src/utils/encoding.js new file mode 100644 index 0000000..243addb --- /dev/null +++ b/frontend/src/utils/encoding.js @@ -0,0 +1,8 @@ +export function probOpacity(prob) { + if (prob >= 0.1) return 1; + else if (prob >= 0.07) return 0.8; + else if (prob >= 0.05) return 0.6; + else if (prob >= 0.02) return 0.4; + else if (prob > 0) return 0.2; + else return 0; +} \ No newline at end of file diff --git a/frontend/src/utils/fakeData.js b/frontend/src/utils/fakeData.js new file mode 100644 index 0000000..38d8649 --- /dev/null +++ b/frontend/src/utils/fakeData.js @@ -0,0 +1,100 @@ +import newArr from "./newArr.js"; + +const DIS = 175; + +export function randint(min, max) { + return Math.floor(Math.random() * (max + 1 - min)) + min; +} + +export function rand(min, max) { + return Math.random() * (max - min) + min; +} + +export function shuffle(arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = randint(0, i); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } +} + +export function getStratAttention(predictors, groupName, itemName) { + const values = predictors.map(p => p.attention[groupName][itemName]); + return { + avg: values.reduce((p, c) => p + c, 0) / values.length, min: Math.min(...values), max: Math.max(...values), + } +} + +export function contextFactory(context, factory) { + return Object.fromEntries(Object.keys(context).map(g => [g, Object.fromEntries(Object.keys(context[g]).map(i => [i, factory(g, i)]))])) +} + +/** + * @return {import('src/model/Strategy.d.ts').Prediction} + */ +function genRandomPrediction(startPos, context, startDir) { + const trajectory = [startPos]; + let lastPos = startPos, lastDir = startDir; + for (let i = 0; i < 10; i++) { + lastDir += rand(-Math.PI / 8, Math.PI / 8); + const dis = DIS * rand(0.9, 1.1); + const newPos = [lastPos[0] + dis * Math.cos(lastDir), lastPos[1] + dis * Math.sin(lastDir)]; + trajectory.push(newPos) + lastPos = newPos; + } + return { + trajectory, probability: rand(0, 1), attention: contextFactory(context, () => rand(0, 1)) + } +} + +/** + * @return {import('src/model/Strategy.d.ts').Strategy} + */ +export function genRandomStrategy(startPos, context, predNum) { + const randomDir = rand(0, 2 * Math.PI); + const predictors = newArr(predNum, 0) + .map(() => genRandomPrediction(startPos, context, randomDir)) + .sort((a, b) => b.probability - a.probability); + return { + predictors, attention: contextFactory(context, (g, i) => getStratAttention(predictors, g, i)), + } +} + +function stratProb(strat) { + return strat.predictors.reduce((p, c) => p + c.probability, 0); +} + +function randomSplit(sum, n) { + sum -= n; + const splits = newArr(n - 1, () => randint(0, sum)) + splits.push(0, sum) + splits.sort((a, b) => a - b); + const res = []; + for (let i = 1; i < splits.length; i++) res.push(splits[i] - splits[i - 1] + 1); + return res; +} + +export function genRandomStrategies(startPos, context, predCnt = 20) { + const stratCnt = randint(4, 6); + const predNums = randomSplit(predCnt, stratCnt); + const res = predNums.map(cnt => genRandomStrategy(startPos, context, cnt)) + const sumProb = res.reduce((p, c) => p + stratProb(c), 0); + res.forEach(s => s.predictors.forEach(p => p.probability /= sumProb)); + res.sort((a, b) => stratProb(b) - stratProb(a)); + return res; +} + +export function genProjection(predictions, groups) { + const pos = []; + const sectionRad = Math.PI * 2 / groups.length; + groups.forEach((g, i) => { + const rad = (i + rand(0.25, 0.75)) * sectionRad, + r = rand(0.25, 0.4); + const cx = 0.5 + Math.cos(rad) * r, cy = 0.5 + Math.sin(rad) * r; + g.forEach(i => pos[i] = [ + (Math.random() * 2 - 1) * 0.15 + cx, + (Math.random() * 2 - 1) * 0.15 + cy, + 1, + ]) + }) + return pos; +} \ No newline at end of file diff --git a/src/utils/file.js b/frontend/src/utils/file.js similarity index 73% rename from src/utils/file.js rename to frontend/src/utils/file.js index f57daae..7234ed5 100644 --- a/src/utils/file.js +++ b/frontend/src/utils/file.js @@ -26,3 +26,13 @@ export function readJSONFile(file) { fileReader.readAsText(file); }) } + +export function hashFileName(prefix, content) { + let hash = 0; + for (let i = 0; i < content.length; i++) { + const chr = content.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return `${prefix}-${hash}`; +} diff --git a/frontend/src/utils/game.js b/frontend/src/utils/game.js new file mode 100644 index 0000000..77e993c --- /dev/null +++ b/frontend/src/utils/game.js @@ -0,0 +1,163 @@ +export const teamNames = ['Radiant', 'Dire'] +export const teamShapes = ['rect', 'circle'] +export const playerColors = [ + ['#3476FF', '#67FFC0', '#C000C0', '#F3F00C', '#FF6C00'], + ['#FE87C3', '#A2B548', '#66D9F7', '#008422', '#A56A00'], +] +export const MIN_X = 8240, MAX_X = 24510; +export const MIN_Y = 8220, MAX_Y = 24450; +const x = v => (v - MIN_X) / (MAX_X - MIN_X); +const y = v => (MAX_Y - v) / (MAX_Y - MIN_Y); +const rx = v => v * (MAX_X - MIN_X) + MIN_X; +const ry = v => MAX_Y - v * (MAX_Y - MIN_Y); + +export const lanePositions = [ + [[9794, 12625], [9794, 22144], [20280, 22144]], + [[11518, 12008], [20903, 20387]], + [[12104, 10277], [22712, 10277], [22712, 19768]], +]; +export const laneLength = [ + [20005, 9519, 10486], + [12581, 12581], + [20099, 10608, 9491] +]; + +export function getWorldPosByLanePos(laneId, lanePos) { + const anchors = lanePositions[laneId], lengths = laneLength[laneId]; + if (lanePos === 0) return anchors[0]; + if (lanePos === 1) return anchors[anchors.length - 1]; + + let worldLanePos = lanePos * lengths[0]; + for (let i = 1; i < anchors.length; i++) + if (worldLanePos > lengths[i]) worldLanePos -= lengths[i]; + else { + let vx = anchors[i][0] - anchors[i - 1][0], vy = anchors[i][1] - anchors[i - 1][1]; + const scale = worldLanePos / lengths[i]; + vx *= scale; + vy *= scale; + return [vx + anchors[i - 1][0], vy + anchors[i - 1][1]]; + } + return [-1, -1]; +} + +export function formatTime(t) { + const sign = t < 0 ? '-' : ''; + t = Math.abs(t); + const h = Math.floor(t / 3600) + t -= h * 3600; + const m = Math.floor(t / 60) + t -= m * 60; + const s = Math.floor(t); + + const str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + if (h === 0) return `${sign}${str}`; + else return `${sign}${h}:${str}`; +} + +/** + * Project the positions in data onto the map + * @param {[number, number]} posInData + * @param {number} mapSize + * @param {boolean=False} reverse : project the position on the map to that in data + */ +export const mapProject = (posInData, mapSize, reverse = false) => { + if (reverse) + return [ + rx(posInData[0] / mapSize), + ry(posInData[1] / mapSize), + ] + else + return [ + x(posInData[0]) * mapSize, + y(posInData[1]) * mapSize, + ] +} + +/** + * Convert compressed game data into released form + * @param {import('src/model/D2Data.d.ts').D2Data} compressedGameData + * @return {import('src/model/D2Data.d.ts').D2Data} + */ +export const updateGameData = (compressedGameData) => { + const preRecord = { + tick: 0, + game_time: 0, + roshan_hp: 0, + is_night: false, + events: [], + heroStates: [[], []], + teamStates: [], + }; + const updateGR = (gr) => { + preRecord.tick = gr.tick; + preRecord.game_time = gr.game_time; + if (gr.hasOwnProperty('is_night')) preRecord.is_night = gr.is_night; + if (gr.hasOwnProperty('roshan_hp')) preRecord.roshan_hp = gr.roshan_hp; + preRecord.events = gr.events; + for (let i = 0; i < 2; i++) + for (let j = 0; j < 5; j++) + preRecord.heroStates[i][j] = Object.assign({}, preRecord.heroStates[i][j], gr.heroStates[i][j]); + for (let i = 0; i < 2; i++) + preRecord.teamStates[i] = Object.assign({}, preRecord.teamStates[i], gr.teamStates[i]); + } + return { + gameInfo: compressedGameData.gameInfo, + gameRecords: compressedGameData.gameRecords.map(gr => { + updateGR(gr); + return JSON.parse(JSON.stringify(preRecord)); + }) + }; +} + +export function mapDis(pos1, pos2) { + return Math.sqrt((pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) + (pos1[1] - pos2[1]) * (pos1[1] - pos2[1])) +} + +export function roshanMaxHP(gameTime) { + if (gameTime < 0 || !gameTime) return 0; + return Math.floor(gameTime / 60) * 130 + 6000; +} + +/** + * @param {import('src/model/D2Data.js').D2Data | null} gameData + * @param {number} frame + * @return {import('src/model/Context.js').Context} + */ +export function genContext(gameData, frame) { + const gameRecord = gameData ? gameData.gameRecords[frame] : null; + const ctx = {}; + [0, 1].forEach(tId => [0, 1, 2, 3, 4].forEach(pId => { + const playerRec = gameRecord ? gameRecord.heroStates[tId][pId] : {pos: []}; + const playerCtx = ctx[`p${tId}${pId}`] = {}; + playerCtx.health = [playerRec.hp, playerRec.mhp]; + playerCtx.mana = [playerRec.mp, playerRec.mmp]; + playerCtx.position = playerRec.pos.slice(0, 2); + playerCtx.level = playerRec.lvl; + playerCtx.isAlive = playerRec.life === 0; + playerCtx.gold = playerRec.gold; + })); + [0, 1].forEach(tId => { + const teamRec = gameRecord ? gameRecord.teamStates[tId] : {towers: [], creeps: []}; + const teamCtx = ctx[`t${tId}`] = {}; + teamCtx.towerTop1 = teamRec.towers[0]; + teamCtx.towerTop2 = teamRec.towers[1]; + teamCtx.towerTop3 = teamRec.towers[2]; + teamCtx.towerMid1 = teamRec.towers[3]; + teamCtx.towerMid2 = teamRec.towers[4]; + teamCtx.towerMid3 = teamRec.towers[5]; + teamCtx.towerBot1 = teamRec.towers[6]; + teamCtx.towerBot2 = teamRec.towers[7]; + teamCtx.towerBot3 = teamRec.towers[8]; + teamCtx.towerBase1 = teamRec.towers[9]; + teamCtx.towerBase2 = teamRec.towers[10]; + teamCtx.creepTop = teamRec.creeps[0]; + teamCtx.creepMid = teamRec.creeps[1]; + teamCtx.creepBot = teamRec.creeps[2]; + }) + ctx.g = { + gameTime: gameRecord?.game_time, + isNight: gameRecord?.is_night, + roshanHP: [gameRecord?.roshan_hp, roshanMaxHP(gameRecord?.game_time)], + } + return ctx; +} diff --git a/src/utils/layout.js b/frontend/src/utils/layout.js similarity index 83% rename from src/utils/layout.js rename to frontend/src/utils/layout.js index ecf6940..1508de7 100644 --- a/src/utils/layout.js +++ b/frontend/src/utils/layout.js @@ -1,7 +1,7 @@ import {useEffect, useState} from "react"; export const viewSize = { - timelineHeight: 100, + timelineHeight: 80, appTitleBarHeight: 40, viewTitleBarHeight: 40, spacing: 10, @@ -17,23 +17,23 @@ export const viewSize = { * }} */ export function useLayout() { - const [layout, setLayout] = useState([1920, 1080]); + const [layout, setLayout] = useState([window.innerWidth, window.innerHeight]); useEffect(() => { - const resize = () => { - if (window.innerWidth !== layout[0] || window.innerHeight !== layout[1]) - setLayout([window.innerWidth, window.innerHeight]); - } + const resize = () => setLayout(l => + l[0] === window.innerWidth && l[1] === window.innerHeight + ? l + : [window.innerWidth, window.innerHeight] + ) resize(); window.addEventListener('resize', resize); return () => window.removeEventListener('resize', resize); }, []); - const mapSize = layout[1] - viewSize.spacing * 5 - viewSize.appTitleBarHeight - viewSize.viewTitleBarHeight - viewSize.timelineHeight; - const strategyViewWidth = (layout[0] - mapSize - viewSize.spacing * 6) / 2.5; + const strategyViewWidth = (layout[0] - mapSize - viewSize.spacing * 6) / 2.5 + 100; return { mapSize, diff --git a/frontend/src/utils/newArr.js b/frontend/src/utils/newArr.js new file mode 100644 index 0000000..11bb8d4 --- /dev/null +++ b/frontend/src/utils/newArr.js @@ -0,0 +1,4 @@ +export default function newArr(len, facOrVal) { + if (typeof facOrVal === 'function') return new Array(len).fill(0).map((_, i) => facOrVal(i)); + else return new Array(len).fill(facOrVal); +} \ No newline at end of file diff --git a/frontend/src/utils/rot.js b/frontend/src/utils/rot.js new file mode 100644 index 0000000..911ac88 --- /dev/null +++ b/frontend/src/utils/rot.js @@ -0,0 +1 @@ +export const rot = vec => Math.atan(-vec[1] / vec[0]) / Math.PI * 180 - (vec[0] < 0 ? 180 : 0); \ No newline at end of file diff --git a/frontend/src/utils/set.js b/frontend/src/utils/set.js new file mode 100644 index 0000000..c343e8a --- /dev/null +++ b/frontend/src/utils/set.js @@ -0,0 +1,11 @@ +export function joinSet(sets) { + return sets.reduce((p, c) => new Set(Array.from(p).filter(v => c.has(v))), sets[0]) +} + +export function unionSet(sets) { + const res = new Set(); + for (const set of sets) + for (const val of set.values()) + res.add(val); + return res; +} \ No newline at end of file diff --git a/src/utils/theme.js b/frontend/src/utils/theme.js similarity index 73% rename from src/utils/theme.js rename to frontend/src/utils/theme.js index 96ca4fa..0fad6ad 100644 --- a/src/utils/theme.js +++ b/frontend/src/utils/theme.js @@ -5,11 +5,9 @@ export const defaultTheme = secondary => ({ primary: { main: '#271c1c', }, - ...(secondary && { - secondary: { - main: secondary, - } - }), + secondary: { + main: secondary || '#8000FF', + }, background: { default: '#f0f0f0', paper: '#ffffff', @@ -27,6 +25,26 @@ export const defaultTheme = secondary => ({ } } }, + MuiRadio: { + defaultProps: { + size: "small", + }, + }, + MuiSvgIcon: { + defaultProps: { + fontSize: "small", + }, + }, + MuiSlider: { + defaultProps: { + size: "small", + }, + }, + MuiCheckbox: { + defaultProps: { + size: "small", + }, + }, MuiToggleButtonGroup: { defaultProps: { size: 'small' @@ -64,3 +82,11 @@ export const defaultTheme = secondary => ({ } } }) + +export const selectionColor = [ + // '#2196f3', + '#8000FF', + '#D4AF37' + // '#f44336', +] + diff --git a/frontend/src/utils/useContextMenu.jsx b/frontend/src/utils/useContextMenu.jsx new file mode 100644 index 0000000..52aec23 --- /dev/null +++ b/frontend/src/utils/useContextMenu.jsx @@ -0,0 +1,37 @@ +import {useState} from "react"; +import {Popover} from "@mui/material"; + +export default function useContextMenu() { + const [contextMenu, setContextMenu] = useState(null); + const onContextMenu = e => { + e.preventDefault(); + setContextMenu( + contextMenu === null + ? { + mouseX: e.clientX + 2, + mouseY: e.clientY - 6, + } + : null, + ); + } + const onClose = () => setContextMenu(null); + const menuFactory = children => ( + + {children} + + ) + + return {menuFactory, onContextMenu, onClose} +} \ No newline at end of file diff --git a/src/utils/useHover.js b/frontend/src/utils/useHover.js similarity index 100% rename from src/utils/useHover.js rename to frontend/src/utils/useHover.js diff --git a/frontend/src/utils/useKeyPressed.js b/frontend/src/utils/useKeyPressed.js new file mode 100644 index 0000000..ff28308 --- /dev/null +++ b/frontend/src/utils/useKeyPressed.js @@ -0,0 +1,16 @@ +import {useCallback, useEffect, useState} from "react"; + +export default function useKeyPressed(key) { + const [pressed, setPressed] = useState(false); + const handleDown = useCallback((e) => (e.key === key) && setPressed(true), [key]); + const handleUp = useCallback((e) => (e.key === key) && setPressed(false), [key]); + useEffect(() => { + window.addEventListener('keydown', handleDown); + window.addEventListener('keyup', handleUp); + return () => { + window.removeEventListener('keydown', handleDown); + window.removeEventListener('keyup', handleUp); + } + }, [handleDown, handleUp]); + return pressed; +} \ No newline at end of file diff --git a/frontend/src/views/ContextView/ContextGroup.jsx b/frontend/src/views/ContextView/ContextGroup.jsx new file mode 100644 index 0000000..5bc0f45 --- /dev/null +++ b/frontend/src/views/ContextView/ContextGroup.jsx @@ -0,0 +1,113 @@ +import {styled} from "@mui/material/styles"; +import {Box, Button, Divider} from "@mui/material"; +import {useEffect, useRef, useState} from "react"; +import {inject, observer} from "mobx-react"; +import {useTranslation} from "react-i18next"; +import {KeyboardArrowDown} from "@mui/icons-material"; +import GroupAtt from "./GroupAtt.jsx"; +import ItemAtt from "./ItemAtt.jsx"; + +function ContextGroup({store, colorLabel, groupName, context, attention, compAtt}) { + //region content control + const [open, setOpen] = useState(false); + const [displayCnt, setDisplayCnt] = useState(0); + const toggle = () => { + setOpen(!open); + setDisplayCnt(open ? 0 : 3); + } + const expand = () => + setDisplayCnt(Math.min(displayCnt + 3, Object.keys(context).length)); + //endregion + + //region height control + const containerRef = useRef(); + const [height, setHeight] = useState(0); + useEffect(() => { + if (!containerRef.current) return; + const container = containerRef.current; + const lastChild = container.lastChild; + setHeight(lastChild.offsetTop + lastChild.clientHeight + parseInt(container.style.paddingBottom)); + }, [open, displayCnt]); + //endregion + + const {t} = useTranslation(); + const sortedContextKeys = Object.keys(context).sort((a, b) => attention[b]?.avg - attention[a]?.avg); + + const handleToggleContextLimit = key => { + if (store.hasContextLimit(groupName, key)) + store.rmContextLimit(groupName, key) + else + store.addContextLimit(groupName, key) + } + + return + attention[key]?.avg)} + compAtt={sortedContextKeys.slice(0, 3).map(key => attention[key]?.avg)} + /> + {open && } + {open && ( + +
Feature
+
Attention
+
+ )} + {sortedContextKeys + .slice(0, displayCnt) + .map(key => ( + handleToggleContextLimit(key)} + colorLabel={ + // show more + } +
+} + +const Container = styled('div')(({theme}) => ({ + position: 'relative', + border: `1px solid ${theme.palette.background.default}`, + borderRadius: theme.shape.borderRadius, + transition: 'height .3s ease', + display: 'block', + padding: theme.spacing(1), + textAlign: 'center', + width: '100%', + overflow: 'hidden', + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.background.default, + }, + '& ~ &': { + marginTop: theme.spacing(1), + } +})) + +export default inject('store')(observer(ContextGroup)); \ No newline at end of file diff --git a/frontend/src/views/ContextView/GroupAtt.jsx b/frontend/src/views/ContextView/GroupAtt.jsx new file mode 100644 index 0000000..8a4b0a0 --- /dev/null +++ b/frontend/src/views/ContextView/GroupAtt.jsx @@ -0,0 +1,316 @@ +import {inject, observer} from "mobx-react"; +import {styled} from "@mui/material/styles"; +import {Badge, Box, Typography} from "@mui/material"; +import Tooltip from '@mui/material/Tooltip'; +import {selectionColor} from "../../utils/theme"; +import {getHighestAtt, getHighestAttIndices} from "./contextSortFunctions"; + +function AttentionItem({store, colorLabel, onSelect, label, valueKey, value, attention, compAtt}) { + + let isComp = store.comparedPredictors.length !== 0; + let isSelect = store.selectedPredictors.length !== 0; + let selectWorker = store.selectedPredictors.length === 1; + let compWorker = store.comparedPredictors.length === 1; + let isInclude = false; + let selectAttention = []; + let compAttention = []; + let top3select = []; + let top3comp = []; + let top3sk = []; + let selectedKey = []; + let curLabel = ''; + + if (isSelect) { + selectAttention = getHighestAtt(store.selectedPredictorsAsAStrategy); + selectedKey = getHighestAttIndices(store.selectedPredictorsAsAStrategy); + + if (store.playerNames[0].includes(String(label)) || store.playerNames[1].includes(String(label))) { + isInclude = true; + for (let i = 0; i < store.playerNames.length; i++) { + for (let j = 0; j < store.playerNames[i].length; j++) { + if (store.playerNames[i][j] === String(label)) { + curLabel = 'p' + String(i) + String(j); + } + } + } + } else if (String(label) === "Radiant") { + isInclude = true; + curLabel = 't0'; + } else if (String(label) === "Dire") { + isInclude = true; + curLabel = 't1'; + } else if (String(label) === "Environment") { + isInclude = true; + curLabel = 'g'; + } + top3sk = selectedKey[curLabel]; + + top3select = selectAttention[curLabel]; + if (isComp && top3sk) { + compAttention = store.comparedPredictorsAsAStrategy.attention[curLabel]; + top3sk.forEach(k => { + top3comp.push(compAttention[k].avg); + }); + // console.log(top3comp); + // console.log(top3select); + } + } + + // console.log(store.contextLimit); + // console.log(colorLabel); + // console.log(String(store.contextSort).slice(-8, -5)); + // console.log(value); + // console.log(isInclude); + // console.log(store.contextLimitDict); + // console.log(label); + + + return
+ { + e.stopPropagation(); + onSelect() + } : undefined}> + + + {colorLabel} + + + + {label} + + + + {!isComp && isInclude && top3select && + + + + + + + } + + {String(store.contextSort).slice(-8, -5) === 'iff' && isComp && isInclude && top3comp && top3select && +
+ + + {top3select[0] >= top3comp[0] && <> + } + {!(top3select[0] >= top3comp[0]) && <> + + } + + + {top3select[1] >= top3comp[1] && <> + } + {!(top3select[1] >= top3comp[1]) && <> + + } + + + {top3select[2] >= top3comp[2] && <> + } + {!(top3select[2] >= top3comp[2]) && <> + + } +
} + + {String(store.contextSort).slice(-8, -5) === 'Att' && isComp && isInclude && top3comp && top3select && +
+ + + + + + + + + + + + + + + + +
+ } + +
+
+ +
+} + +export default inject('store')(observer(AttentionItem)); + +const Container = styled('div', { + shouldForwardProp: propName => !['selectable'].includes(propName) +})(({theme, selectable}) => ({ + position: 'relative', + display: 'flex', + width: '100%', + alignItems: 'center', + minHeight: 40, + borderRadius: theme.shape.borderRadius, + ...selectable && { + '&:hover': { + backgroundColor: theme.palette.background.paper + } + }, +})) + +const BarContainer = styled('div')(({theme}) => ({ + position: 'relative', + height: 15, + width: '100%', + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + display: 'flex', + flexWrap: 'nowrap', + overflow: 'hidden', + border: "1px solid grey" +})) + +const Bar2Container = styled('div')(({theme}) => ({ + position: 'relative', + display: 'flex', + height: 6, + width: '100%', + backgroundColor: "transparent", + borderRadius: theme.shape.borderRadius, + flexWrap: 'nowrap', + overflow: 'hidden', + zIndex: 5 +})) + +const Bar = styled('div')(({theme, barColor}) => ({ + flex: '0 0 auto', + height: '100%', + backgroundColor: barColor || theme.palette.secondary.main, + borderRight: `1px solid ${theme.palette.background.default}`, +})) + +const StyledBadge = styled(Badge)(({theme}) => ({ + '& .MuiBadge-badge': { + right: 0, + top: 0, + border: `2px solid ${theme.palette.background.paper}`, + padding: '0 4px', + }, +})) \ No newline at end of file diff --git a/frontend/src/views/ContextView/ItemAtt.jsx b/frontend/src/views/ContextView/ItemAtt.jsx new file mode 100644 index 0000000..84a11d7 --- /dev/null +++ b/frontend/src/views/ContextView/ItemAtt.jsx @@ -0,0 +1,279 @@ +import {inject, observer} from "mobx-react"; +import {styled} from "@mui/material/styles"; +import {Box, Slider as MuiSlider, Typography} from "@mui/material"; +import {selectionColor} from "../../utils/theme"; +import {getHighestAtt, getHighestAttIndices} from "./contextSortFunctions"; +import ValueDisplay from "./ValueDisplay.jsx"; + +function ItemAtt({store, colorLabel, onSelect, label, valueKey, value, attention, compAtt}) { + + let isComp = store.comparedPredictors.length !== 0; + let isSelect = store.selectedPredictors.length !== 0; + let selectWorker = store.selectedPredictors.length === 1; + let compWorker = store.comparedPredictors.length === 1; + let isInclude = false; + let selectAttention = []; + let compAttention = []; + let top3select = []; + let top3comp = []; + let top3sk = []; + let selectedKey = []; + let curLabel = ''; + + if (isSelect) { + selectAttention = getHighestAtt(store.selectedPredictorsAsAStrategy); + selectedKey = getHighestAttIndices(store.selectedPredictorsAsAStrategy); + + if (store.playerNames[0].includes(String(label)) || store.playerNames[1].includes(String(label))) { + isInclude = true; + for (let i = 0; i < store.playerNames.length; i++) { + for (let j = 0; j < store.playerNames[i].length; j++) { + if (store.playerNames[i][j] === String(label)) { + curLabel = 'p' + String(i) + String(j); + } + } + } + } else if (String(label) === "Radiant") { + isInclude = true; + curLabel = 't0'; + } else if (String(label) === "Dire") { + isInclude = true; + curLabel = 't1'; + } else if (String(label) === "Environment") { + isInclude = true; + curLabel = 'g'; + } + top3sk = selectedKey[curLabel]; + + top3select = selectAttention[curLabel]; + if (isComp && top3sk) { + compAttention = store.comparedPredictorsAsAStrategy.attention[curLabel]; + top3sk.forEach(k => { + top3comp.push(compAttention[k].avg); + }); + // console.log(top3comp); + // console.log(top3select); + } + } + + // console.log(store.contextLimit); + // console.log(colorLabel); + // console.log(String(store.contextSort).slice(-8, -5)); + // console.log(value); + // console.log(isInclude); + // console.log(store.contextLimitDict); + // console.log(label); + + + return
+ { + e.stopPropagation(); + onSelect() + } : undefined}> + + {colorLabel} + + + {label} + + + + + {!isComp && compWorker && compAtt && compAtt.avg && !compAtt?.length && + } + {!isComp && compWorker && compAtt && compAtt.avg && !compAtt?.length && + } + {!isComp && !compWorker && compAtt && compAtt.avg && !compAtt?.length && + v.toFixed(2)}/>} + + {!isComp && selectWorker && attention && attention.avg && !attention?.length && + } + {!isComp && selectWorker && attention && attention.avg && !attention?.length && + } + {!isComp && !selectWorker && attention && attention.avg && !attention?.length && + v.toFixed(2)}/>} + + {isComp && selectWorker && attention && attention.avg && !attention?.length && + } + {isComp && compWorker && attention && attention.avg && compAtt && compAtt.avg && !attention?.length && + } + {isComp && compWorker && attention && attention.avg && compAtt && compAtt.avg && selectWorker && !attention?.length && + } + {isComp && !selectWorker && compWorker && attention && attention.avg && + v.toFixed(2)} + sliderColor={selectionColor[0]}/>} + {isComp && !compWorker && selectWorker && compAtt && compAtt.avg && + v.toFixed(2)} + sliderColor={selectionColor[1]}/>} + {isComp && !compWorker && !selectWorker && attention && attention.avg && compAtt && compAtt.avg && (<> + v.toFixed(2)} + sliderColor={selectionColor[0]} + /> + v.toFixed(2)} + sliderColor={selectionColor[1]} + /> + )} + + +
+} + +export default inject('store')(observer(ItemAtt)); + +const Container = styled('div', { + shouldForwardProp: propName => !['selectable'].includes(propName) +})(({theme, selectable}) => ({ + position: 'relative', + display: 'flex', + width: '100%', + alignItems: 'center', + minHeight: 40, + borderRadius: theme.shape.borderRadius, + ...selectable && { + '&:hover': { + backgroundColor: theme.palette.background.paper + } + }, +})) + +const Slider = styled(MuiSlider)(({theme, sliderColor}) => ({ + color: sliderColor || theme.palette.primary.main, + zIndex: 2, + opacity: 0.8, + + '& .MuiSlider-track': { + height: 15, + border: 'none', + borderRadius: 5, + }, + '& .MuiSlider-thumb': { + width: 4, + height: 4, + shadow: `0 0 4px 4px white`, + }, + '& .MuiSlider-rail': { + height: 15, + backgroundColor: 'grey', + borderRadius: 5, + }, + '& .MuiSlider-mark': { + backgroundColor: sliderColor || theme.palette.background.paper, + height: 20, + width: 2, + }, + '& .MuiSlider-markLabel': { + color: sliderColor || theme.palette.background.main, // Change the text color of the value label here + } + +})); + +const ComparedRange = styled(MuiSlider)(({theme, sliderColor}) => ({ + color: sliderColor || theme.palette.primary.main, + zIndex: 2, + opacity: 0.8, + + '& .MuiSlider-track': { + height: 6, + border: 'none', + borderRadius: 5, + }, + '& .MuiSlider-thumb': { + width: 4, + height: 4, + shadow: `0 0 4px 4px white`, + }, + '& .MuiSlider-rail': { + backgroundColor: 'transparent', + borderRadius: 2, + }, + '& .MuiSlider-mark': { + backgroundColor: sliderColor || theme.palette.primary.main, + height: 20, + width: 2, + zIndex: 4, + }, + '& .MuiSlider-markLabel': { + top: '-20px', + color: sliderColor || theme.palette.background.paper, // Change the text color of the value label here + } + +})); + +const Anchor = styled('div')(({theme, anchorColor, mark}) => ({ + position: 'relative', + transition: 'left .3s ease', + transform: 'translateX(-50%)', + top: '12%', + zIndex: 2, + borderTop: `6px solid ${anchorColor || theme.palette.secondary.main}`, + borderBottom: `6px solid ${anchorColor || theme.palette.secondary.main}`, + borderLeft: `3px solid white`, + borderRight: `3px solid white`, + width: '10px', + height: '25px', + backgroundColor: anchorColor || theme.palette.secondary.main, +})); diff --git a/frontend/src/views/ContextView/SortMenu.jsx b/frontend/src/views/ContextView/SortMenu.jsx new file mode 100644 index 0000000..5b9a0b7 --- /dev/null +++ b/frontend/src/views/ContextView/SortMenu.jsx @@ -0,0 +1,41 @@ +import {forwardRef, Fragment, useState} from "react"; +import {IconButton, Menu, MenuItem} from "@mui/material"; +import {Sort} from "@mui/icons-material"; +import {useTranslation} from "react-i18next"; +import {useStore} from "../../store/store.js"; +import {contextSortFunctions} from "./contextSortFunctions.js"; +import {Observer} from "mobx-react"; + +const ContextSortMenu = forwardRef(function (props, ref) { + const store = useStore(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = event => setAnchorEl(event.currentTarget); + const handleClose = () => setAnchorEl(null); + + const {t} = useTranslation(); + + return + + + + + {() => ( + + {Object.keys(contextSortFunctions).map(key => ( + store.setContextSort(key)} + disabled={contextSortFunctions[key].disabled(store)} + selected={store.contextSort === key}> + {t(`System.ContextView.Sort.${key}`)} + + ))} + + )} + + +}) + +export default ContextSortMenu \ No newline at end of file diff --git a/frontend/src/views/ContextView/ValueDisplay.jsx b/frontend/src/views/ContextView/ValueDisplay.jsx new file mode 100644 index 0000000..430ec62 --- /dev/null +++ b/frontend/src/views/ContextView/ValueDisplay.jsx @@ -0,0 +1,184 @@ +import {AccessTime, DarkMode, LightMode} from "@mui/icons-material"; +import {formatTime, getWorldPosByLanePos, MAX_X, MAX_Y, MIN_X, MIN_Y} from "../../utils/game.js"; +import {keyframes, Typography} from "@mui/material"; +import {styled, useTheme} from "@mui/material/styles"; +import newArr from "../../utils/newArr.js"; + +function LevelBar({level}) { + const theme = useTheme(); + return
+ {newArr(5, r => +
+ {newArr(6, c => { + const l = r * 6 + c + 1; + const arrived = l <= level; + const color = [6, 11, 16].includes(l) ? theme.palette.secondary.main : theme.palette.primary.main; + return
+ + {l} + +
+ })} +
+ )} +
+} + +function MiniMap({pos}) { + const x = (pos[0] - MIN_X) / (MAX_X - MIN_X), + y = (MAX_Y - pos[1]) / (MAX_Y - MIN_Y); + return
+ + + + + +
+} + +const Wave = keyframes(` + 0%{transform: scale(1);opacity: 0.95;} + 25%{transform: scale(2);opacity: 0.75;} + 50%{transform: scale(3);opacity: 0.5;} + 75%{transform: scale(4);opacity: 0.25;} + 100%{transform: scale(5);opacity: 0.05;} +`) + +const BlinkPoint = styled('div')({ + position: 'absolute', + transform: 'translate(-50%,-50%)', + width: 5, + height: 5, + borderRadius: '50%', + backgroundColor: 'white', + border: '1px solid black', +}) + +const WavePoint = styled('div')({ + position: 'absolute', + top: 0.25, + left: 0.25, + width: 2.5, + height: 2.5, + borderRadius: '50%', + backgroundColor: 'red', + animationName: Wave, + animationDuration: '2s', + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', +}) + +function HealthBar({value, max, type = 'health'}) { + const theme = useTheme(); + return
+ + {value.toFixed(0)} / {max.toFixed(0)} + +
+ + {value.toFixed(0)} / {max.toFixed(0)} + +
+
+} + +function IconText({icon, text}) { + return
+
{icon}
+ {text} +
+} + +const config = { + health: ([v, mv]) => , + mana: ([v, mv]) => , + position: p => , + level: l => , + isAlive: i => : } text={i ? 'alive' : 'dead'}/>, + gold: g => } text={g}/>, + towerTop1: h => , + towerTop2: h => , + towerTop3: h => , + towerMid1: h => , + towerMid2: h => , + towerMid3: h => , + towerBot1: h => , + towerBot2: h => , + towerBot3: h => , + towerBase1: h => , + towerBase2: h => , + creepTop: r => , + creepMid: r => , + creepBot: r => , + gameTime: t => } text={formatTime(t)}/>, + isNight: i => : } + text={i ? 'daytime' : 'nighttime'}/>, + roshanHP: ([v, mv]) => , +} + +export default function ValueDisplay({valueKey, value}) { + if (!valueKey || !config[valueKey] || value === undefined) return null; + return config[valueKey](value); +} + +function Gold() { + return +} + +function Alive() { + return +} + +function Dead() { + return +} diff --git a/frontend/src/views/ContextView/contextSortFunctions.js b/frontend/src/views/ContextView/contextSortFunctions.js new file mode 100644 index 0000000..4890afd --- /dev/null +++ b/frontend/src/views/ContextView/contextSortFunctions.js @@ -0,0 +1,97 @@ +const defaultRank = ['t0', 'p00', 'p01', 'p02', 'p03', 'p04', 't1', 'p10', 'p11', 'p12', 'p13', 'p14', 'g'] + +export function getHighestAttKeys(cg) { + const att = [[0, ''], [0, ''], [0, '']]; + Object.keys(cg.attention).forEach(key => { + const val = cg.attention[key].avg; + if (val > att[0][0]) { + att.unshift([val, key]) + att.pop(); + } else if (val > att[1][0]) { + att.splice(1, 0, [val, key]); + att.pop(); + } else if (val > att[2][0]) { + att.splice(2, 1, [val, key]); + } + }) + return att.map(i => i[1]); +} + +export function getHighestAtt(cg) { + let att = []; + Object.keys(cg.attention).forEach(key => { + const vals = Object.values(cg.attention[key]); + const avgs = [] + Object.keys(vals).forEach(feature => { + avgs.push(vals[feature].avg); + }); + avgs.sort((a, b) => b - a); + att[key] = avgs.slice(0, 3); + }) + return att; +} + +export function getHighestAttIndices(cg) { + let attIndices = {}; + Object.keys(cg.attention).forEach(key => { + // Get entries of the current key's object, which are [feature, {avg: value}] + const entries = Object.entries(cg.attention[key]); + + // Map to [feature, avg] for easier sorting + const featureAvgs = entries.map(([feature, valObj]) => [feature, valObj.avg]); + + // Sort based on the avg values in descending order and get the top 3 + featureAvgs.sort((a, b) => b[1] - a[1]); + + // Extract the features (keys) of the top 3 avg values + attIndices[key] = featureAvgs.slice(0, 3).map(([feature, _]) => feature); + }); + return attIndices; +} + +function getAttVal(cg, keys) { + return keys.reduce((p, key) => p + (cg.attention[key]?.avg ?? cg.compAtt[key]?.avg ?? 0), 0); +} + +function getCompAttVal(cg, keys) { + return keys.reduce((p, key) => p + Math.abs(cg.compAtt[key].avg - cg.attention[key].avg), 0) +} + +const defaultComparator = (cga, cgb) => defaultRank.indexOf(cga.key) - defaultRank.indexOf(cgb.key) + +export const contextSortFunctions = { + default: { + disabled: () => false, + comparator: defaultComparator + }, + highAttFirst: { + disabled: store => store.selectedPredictors.length === 0 && store.comparedPredictors.length === 0, + comparator: (cga, cgb) => { + const aKeys = getHighestAttKeys(cga), bKeys = getHighestAttKeys(cgb); + const aVal = getAttVal(cga, aKeys), bVal = getAttVal(cgb, bKeys); + return (bVal - aVal) || defaultComparator(cga, cgb); + } + }, + lowAttFirst: { + disabled: store => store.selectedPredictors.length === 0 && store.comparedPredictors.length === 0, + comparator: (cga, cgb) => { + const aKeys = getHighestAttKeys(cga), bKeys = getHighestAttKeys(cgb); + const aVal = getAttVal(cga, aKeys), bVal = getAttVal(cgb, bKeys); + return (aVal - bVal) || defaultComparator(cga, cgb); + } + }, + highDiffFirst: { + disabled: store => store.selectedPredictors.length === 0 || store.comparedPredictors.length === 0, + comparator: (cga, cgb) => { + const aKeys = getHighestAttKeys(cga), bKeys = getHighestAttKeys(cgb); + return (getCompAttVal(cgb, bKeys) - getCompAttVal(cga, aKeys)) || defaultComparator(cga, cgb); + } + }, + lowDiffFirst: { + disabled: store => store.selectedPredictors.length === 0 || store.comparedPredictors.length === 0, + comparator: (cga, cgb) => { + const aKeys = getHighestAttKeys(cga), bKeys = getHighestAttKeys(cgb); + return (getCompAttVal(cga, aKeys) - getCompAttVal(cgb, bKeys)) || defaultComparator(cga, cgb); + } + }, +} \ No newline at end of file diff --git a/frontend/src/views/ContextView/index.jsx b/frontend/src/views/ContextView/index.jsx new file mode 100644 index 0000000..d9ca596 --- /dev/null +++ b/frontend/src/views/ContextView/index.jsx @@ -0,0 +1,30 @@ +import {inject, observer} from "mobx-react"; +import ContextGroup from "./ContextGroup.jsx"; +import {Box} from "@mui/material"; +import {contextSortFunctions} from "./contextSortFunctions.js"; +import useContextGroups from "./useContextGroups.jsx"; + +/** + * + * @param {import('src/store/store.js').Store} store + * @return {JSX.Element} + * @constructor + */ +function ContextView({store}) { + const cgs = useContextGroups(store); + cgs.sort(contextSortFunctions[store.contextSort].comparator); + + return + {cgs.map(cg => ( + + ))} + +} + +export default inject('store')(observer(ContextView)); + diff --git a/frontend/src/views/ContextView/useContextGroups.jsx b/frontend/src/views/ContextView/useContextGroups.jsx new file mode 100644 index 0000000..38d6381 --- /dev/null +++ b/frontend/src/views/ContextView/useContextGroups.jsx @@ -0,0 +1,77 @@ +import {teamNames, teamShapes} from "../../utils/game.js"; +import {styled} from "@mui/material/styles"; +import {useTranslation} from "react-i18next"; +import {contextFactory} from "../../utils/fakeData.js"; + +export default function useContextGroups(store) { + const {t} = useTranslation(); + const strat = store.selectedPredictorsAsAStrategy; + const attention = strat ? strat.attention : contextFactory(store.curContext, () => ({})); + const compStrat = store.comparedPredictorsAsAStrategy; + const compAtt = compStrat ? compStrat.attention : contextFactory(store.curContext, () => ({})); + const cgs = []; + [0, 1].forEach(teamId => { + cgs.push({ + key: `t${teamId}`, + colorLabel: , + groupName: t(`Game.${teamNames[teamId]}`), + context: store.curContext[`t${teamId}`], + attention: attention[`t${teamId}`], + compAtt: compAtt[`t${teamId}`], + }); + [0, 1, 2, 3, 4].forEach(playerId => { + cgs.push({ + key: `p${teamId}${playerId}`, + colorLabel: , + groupName: store.playerNames[teamId][playerId] || t('Game.PlayerPos', { + team: t(`Game.${teamNames[teamId]}`), + playerId: playerId + 1, + }), + context: store.curContext[`p${teamId}${playerId}`], + attention: attention[`p${teamId}${playerId}`], + compAtt: compAtt[`p${teamId}${playerId}`], + }); + }) + }) + cgs.push({ + key: 'g', + colorLabel: , + groupName: t('System.ContextView.GameContext'), + context: store.curContext['g'], + attention: attention['g'], + compAtt: compAtt['g'], + }); + return cgs; +} + +const PlayerIcon = styled('img', { + shouldForwardProp: propName => !['shape', 'selected', 'lifeState'].includes(propName) +})( + ({theme, shape, selected, lifeState}) => ({ + width: 20, + height: 20, + cursor: 'pointer', + borderRadius: shape === 'rect' ? theme.shape.borderRadius : 10, + boxShadow: 'none', + transition: 'border .3s ease; box-shadow .3s ease', + border: '1px solid grey', + ...(selected && { + border: '2px solid black', + boxShadow: '0 0 5px 0 rgba(0,0,0,.25)' + }), + ...(!lifeState && { + opacity: 0.1, + }) + })) + +const LogoIcon = styled('img', { + shouldForwardProp: propName => !['shape'].includes(propName) +})(({theme, shape}) => ({ + width: 20, + height: 20, + borderRadius: shape === 'rect' ? theme.shape.borderRadius : 10, +})) \ No newline at end of file diff --git a/frontend/src/views/MapView/GameItems/PlayerLayer.jsx b/frontend/src/views/MapView/GameItems/PlayerLayer.jsx new file mode 100644 index 0000000..a8945d2 --- /dev/null +++ b/frontend/src/views/MapView/GameItems/PlayerLayer.jsx @@ -0,0 +1,57 @@ +import {inject, observer} from "mobx-react"; +import {Circle, Group, Layer, Rect} from "react-konva"; +import {mapProject, teamShapes} from "../../../utils/game.js"; +import KonvaImage from "../../../components/KonvaImage.jsx"; +import {alpha} from "@mui/material"; + +/** + * @param {import('src/store/store.js').Store} store + * @param {number} mapSize + * @param {number} scaleBalance + * @returns {JSX.Element} + * @constructor + */ +function PlayerLayer({store, mapSize, scaleBalance}) { + const playerLifeStates = store.playerLifeStates; + const playerPositions = store.playerPositions; + const playerNames = store.playerNames; + const modelSize = mapSize * 0.007; + const iconSize = Math.max(mapSize * 0.025 * scaleBalance, modelSize); + return + {[0, 1].map(teamIdx => + [0, 1, 2, 3, 4].map(playerIdx => ( + playerLifeStates[teamIdx][playerIdx] && + + )) + )} + +} + +export default inject('store')(observer(PlayerLayer)) + + +function PlayerIcon({shape, selected, pos, name, size}) { + const scale = 1.1; + return + {shape === 'rect' && + } + {shape === 'circle' && + } + + +} diff --git a/src/views/MapView/StrategyRenderer.jsx b/frontend/src/views/MapView/Heatmap/StrategyRenderer.jsx similarity index 85% rename from src/views/MapView/StrategyRenderer.jsx rename to frontend/src/views/MapView/Heatmap/StrategyRenderer.jsx index 277956f..4c5f341 100644 --- a/src/views/MapView/StrategyRenderer.jsx +++ b/frontend/src/views/MapView/Heatmap/StrategyRenderer.jsx @@ -1,7 +1,7 @@ import {inject, observer} from "mobx-react"; -import KonvaImage from "../../components/KonvaImage.jsx"; -import {mapDis, mapProject} from "../../utils/game.js"; -import useHeatmap from "../StrategyView/MapRenderer/useHeatmap.js"; +import KonvaImage from "../../../components/KonvaImage.jsx"; +import {mapDis, mapProject} from "../../../utils/game.js"; +import useHeatmap from "./useHeatmap.js"; import {useEffect} from "react"; import {Layer} from "react-konva"; diff --git a/src/views/StrategyView/MapRenderer/useHeatmap.js b/frontend/src/views/MapView/Heatmap/useHeatmap.js similarity index 82% rename from src/views/StrategyView/MapRenderer/useHeatmap.js rename to frontend/src/views/MapView/Heatmap/useHeatmap.js index 724822a..ab00889 100644 --- a/src/views/StrategyView/MapRenderer/useHeatmap.js +++ b/frontend/src/views/MapView/Heatmap/useHeatmap.js @@ -1,4 +1,5 @@ import {useMemo} from "react"; +import {styled} from "@mui/material/styles"; function colorize(pixels, gradient) { for (let i = 0; i < pixels.length; i += 4) { @@ -13,17 +14,21 @@ function colorize(pixels, gradient) { const defaultOptions = { minOpacity: 0.05, - maxOpacity: 1, + maxOpacity: 0.7, maxValue: 20, radius: 5, blur: 3, gradient: { - 0.4: 'blue', - 0.6: 'cyan', - 0.7: 'lime', - 0.8: 'yellow', - 1.0: 'red' + 0.0: "#FFFF00", // Pure Yellow + 0.2: "#FFD700", // Golden Yellow + 0.4: "#FFA500", // Orange + 0.6: "#FF8C00", // Dark Orange + 0.8: "#FF4500", // Orange-Red + 1.0: "#FF0000" // Red } + + + }; function createCircleBrushCanvas(radius, blur) { @@ -98,4 +103,13 @@ export default function useHeatmap(size, data, options = defaultOptions) { return canvas.toDataURL(); }, [size, data, options]) -} \ No newline at end of file +} + +export const HeatmapColorMap = styled('div')({ + background: `linear-gradient(to right, rgba(255,255,255,0) 0%, ${ + Object.entries(defaultOptions.gradient) + .sort((a, b) => a[0] - b[0]) + .map(([stop, color]) => `${color} ${stop * 100}%`) + .join(', ') + })`, +}) \ No newline at end of file diff --git a/frontend/src/views/MapView/Legend/Legend.jsx b/frontend/src/views/MapView/Legend/Legend.jsx new file mode 100644 index 0000000..6ef750f --- /dev/null +++ b/frontend/src/views/MapView/Legend/Legend.jsx @@ -0,0 +1,64 @@ +import {Moving, MultipleStop} from "@mui/icons-material"; +import {styled} from "@mui/material/styles"; +import {useTranslation} from "react-i18next"; +import {HeatmapColorMap} from "../Heatmap/useHeatmap.js"; +import {Radio, Typography} from "@mui/material"; +import {inject, observer} from "mobx-react"; + +function Legend({store}) { + const {t} = useTranslation(); + return
+ + + {t('System.MapView.RealTraj')} + + + + + {t('System.MapView.PredTraj')} + + + + {t('System.MapView.PossibleDest')}: + + {t('System.MapView.LowPossibility')} + {t('System.MapView.HighPossibility')} + + + + {t('System.MapView.MapStyle')}: +
+ store.setMapStyle('colored')}/> + {t('System.MapView.ColoredMap')}: +
+
+ store.setMapStyle('sketch')}/> + {t('System.MapView.SketchMap')}: +
+
+ store.setMapStyle('grey')}/> + {t('System.MapView.GreyMap')}: +
+
+
+} + +export default inject('store')(observer(Legend)); + +const LegendItem = styled('div')(({theme}) => ({ + width: '100%', + display: 'flex', + alignItems: 'center', + margin: theme.spacing(1, 0), +})) \ No newline at end of file diff --git a/frontend/src/views/MapView/Legend/LegendIcon.jsx b/frontend/src/views/MapView/Legend/LegendIcon.jsx new file mode 100644 index 0000000..be004df --- /dev/null +++ b/frontend/src/views/MapView/Legend/LegendIcon.jsx @@ -0,0 +1,37 @@ +import {SvgIcon} from "@mui/material"; + +export default function LegendIcon() { + return + + + + + + + + + + + + +} \ No newline at end of file diff --git a/frontend/src/views/MapView/Legend/index.jsx b/frontend/src/views/MapView/Legend/index.jsx new file mode 100644 index 0000000..6123f78 --- /dev/null +++ b/frontend/src/views/MapView/Legend/index.jsx @@ -0,0 +1,30 @@ +import {Button, ClickAwayListener, Tooltip} from "@mui/material"; +import LegendIcon from "./LegendIcon.jsx"; +import {useState} from "react"; +import Legend from "./Legend.jsx"; +import {useTranslation} from "react-i18next"; + +function MapLegendTrigger() { + const [open, setOpen] = useState(false); + const handleTooltipClose = () => setOpen(false); + const handleTooltipToggle = () => setOpen(s => !s); + const {t} = useTranslation(); + + return +
+ }> + + +
+
+} + +export default MapLegendTrigger; \ No newline at end of file diff --git a/src/views/MapView/PlayerSelection.jsx b/frontend/src/views/MapView/Map/PlayerSelection.jsx similarity index 64% rename from src/views/MapView/PlayerSelection.jsx rename to frontend/src/views/MapView/Map/PlayerSelection.jsx index 846cca1..bbb2e81 100644 --- a/src/views/MapView/PlayerSelection.jsx +++ b/frontend/src/views/MapView/Map/PlayerSelection.jsx @@ -1,7 +1,7 @@ import {inject, observer} from "mobx-react"; import {styled} from "@mui/material/styles"; import {Tooltip, Typography} from "@mui/material"; -import {playerColors, teamNames, teamShapes} from "../../utils/game.js"; +import {teamNames, teamShapes} from "../../../utils/game.js"; import {useTranslation} from "react-i18next"; /** @@ -25,7 +25,7 @@ function PlayerSelection({ store.focusOnPlayer(team, playerIndex)}/> @@ -41,21 +41,21 @@ const Root = styled('div')({ alignItems: 'center', }) -const PlayerIcon = styled('div')(({theme, shape, color, selected, lifeState}) => ({ - width: 20, - height: 20, - backgroundColor: color, - marginRight: theme.spacing(1), - cursor: 'pointer', - borderRadius: shape === 'circle' ? '50%' : theme.shape.borderRadius, - border: 'none', - boxShadow: 'none', - transition: 'border .3s ease; box-shadow .3s ease', - ...(selected && { - border: '3px solid black', - boxShadow: '0 0 5px 0 rgba(0,0,0,.25)' - }), - ...(!lifeState && { - opacity: 0.1, - }) -})) +const PlayerIcon = styled('img')( + ({theme, shape, selected, lifeState}) => ({ + width: 20, + height: 20, + marginRight: theme.spacing(1), + cursor: 'pointer', + borderRadius: shape === 'rect' ? theme.shape.borderRadius : 10, + boxShadow: 'none', + transition: 'border .3s ease; box-shadow .3s ease', + border: '1px solid grey', + ...(selected && { + border: '2px solid black', + boxShadow: '0 0 5px 0 rgba(0,0,0,.25)' + }), + ...(!lifeState && { + opacity: 0.1, + }) + })) diff --git a/frontend/src/views/MapView/Map/index.jsx b/frontend/src/views/MapView/Map/index.jsx new file mode 100644 index 0000000..3a10f24 --- /dev/null +++ b/frontend/src/views/MapView/Map/index.jsx @@ -0,0 +1,69 @@ +import {inject, observer} from "mobx-react"; +import {Layer, Stage} from "react-konva"; +import useMapNavigation from "../Map/useMapNavigation.js"; +import KonvaImage from "../../../components/KonvaImage.jsx"; +import PlayerLayer from "../GameItems/PlayerLayer.jsx"; +import RealTrajectoryLayer from "../Trajectories/RealTrajectoryLayer.jsx"; +import StrategyRenderer from "../Heatmap/StrategyRenderer.jsx"; +import {useEffect, useRef} from "react"; +import PredictedTrajectoryLayer from "../Trajectories/PredictedTrajectoryLayer.jsx"; +import {styled, useTheme} from "@mui/material/styles"; + +/** + * @param {import('src/store/store.js').Store} store + * @param {number} size + * @constructor + */ +function MapRenderer({ + store, size, onNav, + }) { + const ref = useRef(null); + const { + scale, + displayRange, + autoFocus, + handleWheel, + handleRecover, + dragBoundFunc, + handleDragEnd + } = useMapNavigation(size, ref); + const scaleBalance = 2 / (scale + 1); + + useEffect(() => { + onNav(displayRange.x, displayRange.y) + }, [onNav, displayRange]); + + const theme = useTheme(); + + return + ref.current = n} + onWheel={handleWheel} + onDblClick={handleRecover} + fill={'black'} + draggable + onDragEnd={handleDragEnd} + dragBoundFunc={dragBoundFunc}> + + + + {store.selectedPredictors.length !== 0 && store.comparedPredictors.length === 0 && + } + + {store.focusedPlayer !== -1 && + } + + + +} + +export default inject('store')(observer(MapRenderer)) + +const Root = styled('div')({ + position: 'relative', +}) diff --git a/src/views/MapView/useMapNavigation.js b/frontend/src/views/MapView/Map/useMapNavigation.js similarity index 75% rename from src/views/MapView/useMapNavigation.js rename to frontend/src/views/MapView/Map/useMapNavigation.js index 6a5e3cc..9f0ad62 100644 --- a/src/views/MapView/useMapNavigation.js +++ b/frontend/src/views/MapView/Map/useMapNavigation.js @@ -1,5 +1,5 @@ -import {useState} from "react"; -import {MAX_Y, MIN_X, MIN_Y} from "../../utils/game.js"; +import {useMemo, useState} from "react"; +import {MAX_X, MAX_Y, MIN_X, MIN_Y} from "../../../utils/game.js"; const ensureSize = (x, scale, mapSize) => { return Math.min(0, Math.max(mapSize - mapSize * scale, x)); @@ -8,6 +8,18 @@ const ensureSize = (x, scale, mapSize) => { export default function useMapNavigation(mapSize, ref) { const [scale, setScale] = useState(1); const [offset, setOffset] = useState([0, 0]); + const displayRange = useMemo(() => { + return { + x: [ + -offset[0] / (scale * mapSize) * (MAX_X - MIN_X) + MIN_X, + (-offset[0] + mapSize) / (scale * mapSize) * (MAX_X - MIN_X) + MIN_X + ], + y: [ + MAX_Y - (-offset[1] + mapSize) / (scale * mapSize) * (MAX_Y - MIN_Y), + MAX_Y + offset[1] / (scale * mapSize) * (MAX_Y - MIN_Y), + ], + } + }, [scale, mapSize, offset]); const setNav = (scale, offset, anim = false) => { setScale(scale); setOffset(offset); @@ -53,6 +65,9 @@ export default function useMapNavigation(mapSize, ref) { ] ); } + const handleDragEnd = e => { + setOffset([ref.current.x(), ref.current.y()]); + } const dragBoundFunc = pos => { return { x: ensureSize(pos.x, scale, mapSize), @@ -61,7 +76,7 @@ export default function useMapNavigation(mapSize, ref) { } const autoFocus = (centerPos, radius) => { const WH = MAX_Y - MIN_Y; - const scale = WH / 2 / radius; + const scale = Math.max(1, WH / 2 / radius); setNav(scale, [ ensureSize(mapSize * scale * (radius + MIN_X - centerPos[0]) / WH, scale, mapSize), ensureSize(mapSize * scale * (centerPos[1] - MIN_X + radius - WH) / WH, scale, mapSize), @@ -71,9 +86,11 @@ export default function useMapNavigation(mapSize, ref) { return { scale, offset, + displayRange, handleRecover, handleWheel, autoFocus, dragBoundFunc, + handleDragEnd, } } diff --git a/frontend/src/views/MapView/MapContext/Corner.jsx b/frontend/src/views/MapView/MapContext/Corner.jsx new file mode 100644 index 0000000..ffbb388 --- /dev/null +++ b/frontend/src/views/MapView/MapContext/Corner.jsx @@ -0,0 +1,42 @@ +import {Group, Line, Rect, Text} from "react-konva"; +import newArr from "../../../utils/newArr.js"; +import {useTheme} from "@mui/material/styles"; + +export default function MapContextCorner({ + timeStep, + x, + y, + direction, + gridSize, + space, + onViewTime, + textSize = [16, 10], + onClick + }) { + const disStep = gridSize + space; + const pos = (step) => step * disStep + gridSize / 2 + space; + const xDir = direction.endsWith('l') ? -1 : 1, yDir = direction.startsWith('t') ? -1 : 1; + const theme = useTheme(); + + return + {newArr(timeStep, i => ( + onViewTime(i + 1)} + onMouseLeave={() => onViewTime(-1)}> + + + + + ))} + +} \ No newline at end of file diff --git a/frontend/src/views/MapView/MapContext/Matrix.jsx b/frontend/src/views/MapView/MapContext/Matrix.jsx new file mode 100644 index 0000000..89ece29 --- /dev/null +++ b/frontend/src/views/MapView/MapContext/Matrix.jsx @@ -0,0 +1,87 @@ +import {Group, Line} from "react-konva"; +import newArr from "../../../utils/newArr.js"; +import {useTheme} from "@mui/material/styles"; +import {useCallback} from "react"; +import MatrixCell from "./MatrixCell.jsx"; + +/** + * + * @param {number} numGrid + * @param {number} timeStep + * @param {number} x + * @param {number} y + * @param {'left' | 'right' | 'top' | 'bottom'} direction + * @param {number} gridSize + * @param {'rect' | 'circle'} gridVariant + * @param {number} space + * @param {import('src/model/System.js').MatrixCell[][]} data + * @param {import('src/model/System.js').MatrixCell[][]} compData + * @param {import('src/model/System.js').MatrixCell[][]} viewData + * @param {number} curPos + * @param {[number, number]} range + * @param {number[]} selectedPredictions + * @param {number[]} comparedPredictions + * @param {number[]} viewedPredictions + * @param {(g: number, t: number) => void} onEnter + * @param {(g: number, t: number) => void} onLeave + * @param {(g: number, t: number) => void} onClick + * @return {JSX.Element} + * @constructor + */ +function MapContextMatrix({ + numGrid, + timeStep, + x, y, + direction, + gridSize, gridVariant = 'rect', + space, + data, compData, viewData, + curPos, + range, + selectedPredictions, + comparedPredictions, + viewedPredictions, + onEnter, + onLeave, + onClick, + }) { + const gridArr = newArr(numGrid, g => newArr(timeStep, t => [g, t])).flat(); + const disStep = gridSize + space, mapSize = numGrid * disStep - space; + const theme = useTheme(); + const pos = useCallback(step => step * disStep + gridSize / 2, [disStep, gridSize]); + const gridPos = useCallback((g, t) => { + if (direction === 'top') return [pos(g), pos(timeStep - t - 1)]; + else if (direction === 'left') return [pos(timeStep - t - 1), pos(numGrid - g - 1)]; + else if (direction === 'right') return [pos(t), pos(numGrid - g - 1)]; + else return [pos(g), pos(t)]; + }, [direction, pos]); + + return + {curPos > range[0] && curPos < range[1] && + } + {gridArr.map(([g, t]) => { + const [x, y] = gridPos(g, t); + return onEnter(g, t)} + onMouseLeave={() => onLeave(g, t)} + onClick={() => onClick(g, t)}> + + + })} + +} + +export default MapContextMatrix \ No newline at end of file diff --git a/frontend/src/views/MapView/MapContext/MatrixCell.jsx b/frontend/src/views/MapView/MapContext/MatrixCell.jsx new file mode 100644 index 0000000..e7a3344 --- /dev/null +++ b/frontend/src/views/MapView/MapContext/MatrixCell.jsx @@ -0,0 +1,143 @@ +import {Fragment} from "react"; +import {Arrow, Line, Rect} from "react-konva"; +import {selectionColor} from "../../../utils/theme.js"; +import {useTheme} from "@mui/material/styles"; +import {rot} from "../../../utils/rot.js"; +import {alpha} from "@mui/material"; +import {probOpacity} from "../../../utils/encoding.js"; + +const arcRadiusFactory = r => [ + [r - 1, r], + [r - 3, r - 2], + [r - 5, r - 4], +]; +const calAngle = dirRange => { + if (dirRange.length === 1) return [dirRange[0] - 5, 10]; + dirRange.sort((a, b) => a - b); + let largestGap = dirRange[0] + 360 - dirRange[dirRange.length - 1], gapIdx = 0; + for (let i = 1; i < dirRange.length; i++) + if (dirRange[i] - dirRange[i - 1] > largestGap) { + largestGap = dirRange[i] - dirRange[i - 1]; + gapIdx = i; + } + if (gapIdx === 0) return [dirRange[0], dirRange[dirRange.length - 1] - dirRange[gapIdx]] + else return [dirRange[gapIdx], dirRange[gapIdx - 1] + 360 - dirRange[gapIdx]]; +} +const cornerTri = (outer, inner) => [ + -outer, outer, + -outer, -outer, + outer, -outer, + inner, -inner, + -inner, -inner, + -inner, inner, +] + +/** + * @param {number} gridSize + * @param {'rect' | 'circle'} variant + * @param {import('src/model/System.js').MatrixCell} sel + * @param {boolean} selEnabled + * @param {import('src/model/System.js').MatrixCell} comp + * @param {boolean} compEnabled + * @param {import('src/model/System.js').MatrixCell} view + * @param {boolean} viewEnabled + * @return {JSX.Element} + * @constructor + */ +function MatrixCell({ + gridSize, variant = 'rect', + sel, selEnabled, + comp, compEnabled, + view, viewEnabled + }) { + const theme = useTheme(); + const arcRadius = arcRadiusFactory(gridSize / 2); + // if (variant === 'rect') { + const arrowSize = gridSize * 0.4; + return + + + + {!viewEnabled && + } + {!viewEnabled && + } + + + // } else if (variant === 'circle') { + // const layers = []; + // if (selEnabled) layers.push('sel'); + // if (compEnabled) layers.push('comp'); + // if (viewEnabled) layers.push('view'); + // const options = { + // sel: { + // data: sel, + // color: alpha(selectionColor[0], probOpacity(sel.probability)), + // }, + // comp: { + // data: comp, + // color: alpha(selectionColor[1], probOpacity(comp.probability)), + // }, + // view: { + // data: view, + // color: alpha(theme.palette.secondary.main, probOpacity(view.probability)), + // } + // }; + // const arrowSize = layers.length ? arcRadius[layers.length - 1][0] * 0.9 : 0; + // return + // + // {layers.map((layer, lId) => { + // const {data, color} = options[layer]; + // if (data.probability === 0) return null; + // + // const [start, angle] = calAngle(data.dirRange); + // return + // + // {(!viewEnabled || layer === 'view') && + // } + // + // })} + // + // } +} + +export default MatrixCell; \ No newline at end of file diff --git a/frontend/src/views/MapView/MapContext/index.jsx b/frontend/src/views/MapView/MapContext/index.jsx new file mode 100644 index 0000000..cff639a --- /dev/null +++ b/frontend/src/views/MapView/MapContext/index.jsx @@ -0,0 +1,167 @@ +import {inject, observer} from "mobx-react"; +import {Layer, Stage} from "react-konva"; +import {styled} from "@mui/material/styles"; +import useAxisRange from "./useAxisRange.js"; +import MapContextMatrix from "./Matrix.jsx"; +import MapContextCorner from "./Corner.jsx"; +import useGeneralDir from "./useGeneralDir.js"; +import useKeyPressed from "../../../utils/useKeyPressed.js"; +import {unionSet} from "../../../utils/set.js"; +import {useState} from "react"; + +const space = 3; +const amp = 10; +const arrowSize = 0.7; +const minGridSize = 20; +const autoDecideNumGrid = (size, timeStep) => Math.floor((size + space) / (minGridSize + space) - timeStep); + +/** + * + * @param {import('src/store/store.js').Store} store + * @param size + * @param mapRenderer + * @param numGrid + * @param timeStep + * @return {JSX.Element} + * @constructor + */ +function MapContext({store, size, mapRenderer, numGrid = -1, timeStep = 5}) { + if (numGrid === -1) numGrid = autoDecideNumGrid(size, timeStep); + const disStep = (size + space) / (numGrid + timeStep); + const gridSize = disStep - space; + const mapSize = disStep * numGrid - space; + + const [gridVariant, setGridVariant] = useState('rect'); + const toggleGridVariant = () => setGridVariant(v => v === 'rect' ? 'circle' : 'rect'); + + const {xRange, yRange, onNav} = useAxisRange(); + const [xSel, ySel] = store.trajStat(xRange, yRange, numGrid, timeStep, store.selectedPredictorsAsAStrategy); + const [xComp, yComp] = store.trajStat(xRange, yRange, numGrid, timeStep, store.comparedPredictorsAsAStrategy); + const [xView, yView] = store.trajStat(xRange, yRange, numGrid, timeStep, store.viewedPredictorsAsAStrategy); + const [curPosX, curPosY] = store.focusedPlayerPosition; + + const generalDir = useGeneralDir(xSel, ySel); + + const shift = useKeyPressed('Shift'); + + const handleViewX = (g, t) => { + store.viewPredictions(Array.from(unionSet([xSel[g][t].predictionIdxes, xComp[g][t].predictionIdxes]))); + store.setViewedTime(t + 1); + } + const handleViewY = (g, t) => { + store.viewPredictions(Array.from(unionSet([ySel[g][t].predictionIdxes, yComp[g][t].predictionIdxes]))); + store.setViewedTime(t + 1); + } + const handleClearView = () => { + store.viewPredictions([]); + store.setViewedTime(-1); + } + + const handleSelectX = (g, t) => store.selectPredictors(Array.from(unionSet([xSel[g][t].predictionIdxes, xComp[g][t].predictionIdxes])), Number(shift)); + const handleSelectY = (g, t) => store.selectPredictors(Array.from(unionSet([ySel[g][t].predictionIdxes, yComp[g][t].predictionIdxes])), Number(shift)); + + return + + + {generalDir.startsWith('t') && + } + {generalDir.endsWith('l') && + } + {generalDir.endsWith('r') && + } + {generalDir.startsWith('b') && + } + {generalDir === 'tl' && + } + {generalDir === 'tr' && + } + {generalDir === 'bl' && + } + {generalDir === 'br' && + } + + + + {mapRenderer(mapSize, onNav)} + + +} + +export default inject('store')(observer(MapContext)); + +const Root = styled('div')({ + position: 'relative', +}) + +const MapContainer = styled('div')({ + position: 'absolute', + overflow: 'hidden', +}) \ No newline at end of file diff --git a/frontend/src/views/MapView/MapContext/useAxisRange.js b/frontend/src/views/MapView/MapContext/useAxisRange.js new file mode 100644 index 0000000..704f950 --- /dev/null +++ b/frontend/src/views/MapView/MapContext/useAxisRange.js @@ -0,0 +1,12 @@ +import {useCallback, useState} from "react"; +import {MAX_X, MAX_Y, MIN_X, MIN_Y} from "../../../utils/game.js"; + +export default function useAxisRange() { + const [xRange, setXRange] = useState([MIN_X, MAX_X]); + const [yRange, setYRange] = useState([MIN_Y, MAX_Y]); + const onNav = useCallback((xRange, yRange) => { + setXRange(c => c[0] === xRange[0] && c[1] === xRange[1] ? c : xRange); + setYRange(c => c[0] === yRange[0] && c[1] === yRange[1] ? c : yRange); + }, []); + return {xRange, yRange, onNav}; +} \ No newline at end of file diff --git a/frontend/src/views/MapView/MapContext/useGeneralDir.js b/frontend/src/views/MapView/MapContext/useGeneralDir.js new file mode 100644 index 0000000..3eeceba --- /dev/null +++ b/frontend/src/views/MapView/MapContext/useGeneralDir.js @@ -0,0 +1,20 @@ +/** + * + * @param {import('src/model/System.js').MatrixCell[][]} data + * @return {number[]} + */ +function probNext(data) { + const vec = [0, 0]; + for (const r of data) + for (const d of r) { + vec[0] += d.avgDirection[0] * d.probability; + vec[1] += d.avgDirection[1] * d.probability; + } + return vec; +} + +export default function useGeneralDir(xData, yData) { + const xNext = probNext(xData); + const yNext = probNext(yData); + return `${yNext[1] < 0 ? 'b' : 't'}${xNext[0] < 0 ? 'l' : 'r'}`; +} \ No newline at end of file diff --git a/frontend/src/views/MapView/Timeline/Timeline.jsx b/frontend/src/views/MapView/Timeline/Timeline.jsx new file mode 100644 index 0000000..173dbd4 --- /dev/null +++ b/frontend/src/views/MapView/Timeline/Timeline.jsx @@ -0,0 +1,113 @@ +import {inject, observer} from "mobx-react"; +import {Slider, Typography} from "@mui/material"; +import {styled} from "@mui/material/styles"; +import {viewSize} from "../../../utils/layout.js"; +import RangeSlider from 'react-range-slider-input'; +import "react-range-slider-input/dist/style.css"; +import {t} from "i18next"; +import {formatTime} from "../../../utils/game.js"; + +/** + * @param {import('src/store/store.js').Store} store + * @constructor + */ +function Timeline({ + store, + }) { + return + + + store.setFrame(e.target.value - 1)} + valueLabelDisplay={'auto'} + valueLabelFormat={v => formatTime(store.frameTime(v))}/> + + + + + + + +} + +export default inject('store')(observer(Timeline)) + +const Root = styled('div')(({theme}) => ({ + height: viewSize.timelineHeight, + padding: theme.spacing(1), +})) + +const Time = styled(Typography)({ + width: 70, + textAlign: 'center', + flex: '0 0 auto', +}) + +const Row = styled('div')({ + width: '100%', + display: 'flex', + alignItems: 'center', + '&:hover .range-slider__range': { + height: 6, + } +}) + +const WindowSlider = styled(RangeSlider)(({theme}) => ({ + "&.range-slider": { + height: 2, + }, + "&.range-slider .range-slider__range": { + background: theme.palette.primary.main, + transition: 'height 0.3s', + borderRadius: 0, + }, + "&.range-slider .range-slider__thumb": { + background: theme.palette.primary.dark, + width: 12, + height: 12, + transition: 'transform 0.3s', + }, + "&.range-slider .range-slider__range[data-active]": { + height: 8, + }, + "&.range-slider .range-slider__thumb[data-active]": { + transform: 'translate(-50%, -50%) scale(1.25)', + }, + "&.range-slider .range-slider__thumb[data-lower]": { + transform: 'translate(-50%, -50%)', + }, + "&.range-slider .range-slider__thumb[data-upper]": { + transform: 'translate(-50%, -50%)', + }, + "&::before": { + content: '""', + position: 'absolute', + left: '75%', + height: '10px', + top: '50%', + transform: 'translate(-50%, -50%)', + width: 1, + backgroundColor: theme.palette.primary.main, + }, + "&::after": { + content: `"${t("System.MapView.CurrentTime")}"`, + position: 'absolute', + left: '75%', + height: '10px', + top: '50%', + transform: 'translate(-50%, 10px)', + width: 200, + textAlign: 'center', + } +})) diff --git a/frontend/src/views/MapView/Trajectories/PredictedTrajectoryLayer.jsx b/frontend/src/views/MapView/Trajectories/PredictedTrajectoryLayer.jsx new file mode 100644 index 0000000..d2c14ea --- /dev/null +++ b/frontend/src/views/MapView/Trajectories/PredictedTrajectoryLayer.jsx @@ -0,0 +1,79 @@ +import {inject, observer} from "mobx-react"; +import {Group, Layer} from "react-konva"; +import {selectionColor} from "../../../utils/theme.js"; +import {useTheme} from "@mui/material/styles"; +import Traj from "./Traj.jsx"; +import {useCallback} from "react"; + +function ArrowGroup({ + trajectories, + color, + listening, + mapSize, + scaleBalance, + onMouseEnter, + onMouseLeave, + labelStyle = 'dot', + viewedTime = -1, + }) { + return + {trajectories.map((traj, tId) => ( + + ))} + +} + +function PredictedTrajectoryLayer({store, mapSize, scaleBalance}) { + const getPredictionTraj = i => store.predictions[i]; + const theme = useTheme(); + + const handleMouseEnter = useCallback((id, t) => { + store.viewPrediction(id); + store.setViewedTime(t); + }, []); + const handleMouseLeave = useCallback(() => { + store.viewPrediction(-1) + store.setViewedTime(-1); + }, []); + + return ( + + + + + + ); +} + +export default inject('store')(observer(PredictedTrajectoryLayer)); diff --git a/frontend/src/views/MapView/Trajectories/RealTrajectoryLayer.jsx b/frontend/src/views/MapView/Trajectories/RealTrajectoryLayer.jsx new file mode 100644 index 0000000..4c3358c --- /dev/null +++ b/frontend/src/views/MapView/Trajectories/RealTrajectoryLayer.jsx @@ -0,0 +1,57 @@ +import {inject, observer} from "mobx-react"; +import {Layer} from "react-konva"; +import {mapDis} from "../../../utils/game.js"; +import {useEffect} from "react"; +import {useTheme} from "@mui/material/styles"; +import Traj from "./Traj.jsx"; + +/** + * @param {import('src/store/store').Store} store + * @param {number} mapSize + * @param {number} scaleBalance + * @returns {JSX.Element} + * @constructor + */ +function RealTrajectoryLayer({store, mapSize, scaleBalance, onAutoFocus}) { + // 0. I have added some example files with some comments. + // You can find the file list in README.md. + + // 1. You can get the trajectory of the selected player from store here. + // You can refer to store.playerPositions, where I get the current positions of all players. + // But you need to get several future positions of the selected player. + // e.g., const trajectory = store.selectedPlayerTrajectory; + + const tra = store.selectedPlayerTrajectory; + const windowedTra = store.selectedPlayerTrajectoryInTimeWindow; + + useEffect(() => { + const centerPos = store.playerPositions[store.focusedTeam][store.focusedPlayer]; + const radius = Math.max(Math.min(Math.max(...tra.map(pos => mapDis( + pos, + store.playerPositions[store.focusedTeam][store.focusedPlayer] + ))) * 1.2, 3000), 2000); + onAutoFocus && onAutoFocus(centerPos, radius); + }, [windowedTra]); + + const theme = useTheme(); + return + + + +} + + +export default inject('store')(observer(RealTrajectoryLayer)); diff --git a/frontend/src/views/MapView/Trajectories/Traj.jsx b/frontend/src/views/MapView/Trajectories/Traj.jsx new file mode 100644 index 0000000..dfd15a8 --- /dev/null +++ b/frontend/src/views/MapView/Trajectories/Traj.jsx @@ -0,0 +1,72 @@ +import {memo} from "react"; +import {Arrow, Circle, Group, Rect, Text} from "react-konva"; +import {mapProject} from "../../../utils/game.js"; +import newArr from "../../../utils/newArr.js"; +import {useTheme} from "@mui/material/styles"; + +const timeLabelLayoutFactory = scaleBalance => ({ + x: -8 * scaleBalance, + y: -6 * scaleBalance, + width: 16 * scaleBalance, + height: 12 * scaleBalance, + cornerRadius: 6 * scaleBalance, + fontSize: 8 * scaleBalance, +}) + +const pos = (p, r, l) => Math.floor((p - r[0]) / (r[1] - r[0]) * (l - 1)); + +export default memo(function Traj({ + traj, + id, + mapSize, + scaleBalance, + color, + timeRange, + labelStyle = 'dot', + viewedLabel = -1, + noPointer = false, + noDash = false, + opacity = 1, + onMouseEnter, + onMouseLeave, + }) { + const t0 = Math.ceil(timeRange[0] / 30), t1 = Math.floor(timeRange[1] / 30); + const tAnno = newArr(t1 - t0 + 1, i => ({ + label: i + t0, + t: i + t0, + pos: mapProject(traj[pos((i + t0) * 30, timeRange, traj.length)], mapSize), + })); + if (labelStyle === 'dot' || viewedLabel !== 5) + if (!noPointer && t1 * 30 === timeRange[1]) tAnno.pop(); + const theme = useTheme(); + const timeLabelLayout = timeLabelLayoutFactory(scaleBalance); + + return + mapProject(p, mapSize)).flat()} + strokeWidth={1.2 * scaleBalance} // Line width + pointerWidth={noPointer ? 0 : 4 * scaleBalance} + pointerLength={noPointer ? 0 : 4 * scaleBalance} + lineCap={'round'} lineJoin={'round'} + dash={noDash ? [] : [6 * scaleBalance, 2 * scaleBalance]} + stroke={color} fill={color} + onMouseEnter={e => onMouseEnter && onMouseEnter(id, 5)} + onMouseLeave={e => onMouseLeave && onMouseLeave(id, 5)}/> + {tAnno.map(({label, pos}, i) => + onMouseEnter && onMouseEnter(id, i)} + onMouseLeave={e => onMouseLeave && onMouseLeave(id, i)} + preventDefault> + {(labelStyle === 'dot' || (viewedLabel !== -1 && viewedLabel !== label)) + ? + : + + + } + )} + +}) \ No newline at end of file diff --git a/frontend/src/views/MapView/index.jsx b/frontend/src/views/MapView/index.jsx new file mode 100644 index 0000000..1e9795f --- /dev/null +++ b/frontend/src/views/MapView/index.jsx @@ -0,0 +1,4 @@ +import MapRenderer from './Map/index.jsx'; +import MapContextRenderer from './MapContext/index.jsx'; + +export {MapRenderer, MapContextRenderer} \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Projection/ConvexHull.jsx b/frontend/src/views/StrategyView/Projection/ConvexHull.jsx new file mode 100644 index 0000000..6eab566 --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/ConvexHull.jsx @@ -0,0 +1,82 @@ +import {memo, useMemo} from "react"; +import {alpha, darken, Tooltip, List, ListItem} from "@mui/material"; +import {styled} from "@mui/material/styles"; +import BubbleSets from "bubblesets-js"; +import useKeyPressed from "../../../utils/useKeyPressed.js"; +import {selectionColor} from "../../../utils/theme.js"; + +const W = 1000, H = 1000; +const scale = 0.95; + +function useConvexHull(predictorGroup, points) { + return useMemo(() => { + // const samplePoints = predictorGroup + // .map(i => [ + // points[i][0] * W, + // points[i][1] * H, + // points[i][2] * W / 20 + // ]) + // .map(([x, y, r]) => { + // return newArr(360, deg => [ + // x + r * Math.cos(deg / 180 * Math.PI) * 1.5, + // y + r * Math.sin(deg / 180 * Math.PI) * 1.5, + // ]) + // }) + // .flat() + // const hullPoints = hull(samplePoints, 1000); + // return 'M' + hullPoints.map(p => p.join(' ')).join('L'); + const circles = points.map(([x, y, r]) => ({ + cx: x * W, + cy: y * H, + radius: r * W / 20 * scale, + })) + + const bubbles = new BubbleSets(); + bubbles.pushMember(...predictorGroup.map(i => circles[i])); + bubbles.pushNonMember(...circles.filter((_, i) => !predictorGroup.includes(i))); + const pointPath = bubbles.compute(); + const cleanPath = pointPath.sample(8).simplify(0).bSplines().simplify(0); + return cleanPath.toString(); + }, [predictorGroup, points]); +} + +function ConvexHull({predictorGroup, points, onViewGroup, onSelectGroup, tags, onContextMenu}) { + const convexHull = useConvexHull(predictorGroup, points); + const shift = useKeyPressed('Shift') + return + {Object.keys(tags).map(wId => + {+wId + 1}: {tags[wId].join(', ')} + )} + }> + onViewGroup(predictorGroup)} + onMouseLeave={() => onViewGroup([])} + onClick={e => onSelectGroup(predictorGroup, e.shiftKey ? 1 : 0)} + onContextMenu={onContextMenu}/> + +} + +export const LassoGroup = styled('path', { + shouldForwardProp: propName => !['width', 'selectable', 'color', 'shift'].includes(propName), +})(({theme, width, selectable, color, shift}) => ({ + stroke: color || darken(selectionColor[Number(shift)], .2), + fill: color || theme.palette.background.default, + ...(selectable + ? { + strokeWidth: 0.3, + cursor: 'pointer', + '&:hover': { + strokeWidth: width, + fill: alpha(color || selectionColor[Number(shift)], 0.2), + }, + } + : { + strokeWidth: width, + pointerEvents: 'none', + }), +})) + +export default memo(ConvexHull); \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Projection/Icons.jsx b/frontend/src/views/StrategyView/Projection/Icons.jsx new file mode 100644 index 0000000..343c418 --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/Icons.jsx @@ -0,0 +1,39 @@ +import {SvgIcon} from "@mui/material"; + +export function LassoIcon(props) { + return + + + + + + +} + +export function ShiftIcon(props) { + return + + + + + + Shift + + + +} \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Projection/Point.jsx b/frontend/src/views/StrategyView/Projection/Point.jsx new file mode 100644 index 0000000..4299961 --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/Point.jsx @@ -0,0 +1,100 @@ +import {styled, useTheme} from "@mui/material/styles"; +import {selectionColor} from "../../../utils/theme.js"; +import {alpha, Tooltip} from "@mui/material"; +import newArr from "../../../utils/newArr.js"; +import {rot} from "../../../utils/rot.js"; +import useKeyPressed from "../../../utils/useKeyPressed.js"; + +const arrowLength = 1.45; +const arrowWidth = 0.4; +const arrowR = Math.asin(arrowWidth); +const [x, y] = [Math.cos(arrowR), Math.sin(arrowR)]; +const arrowPath = `M${arrowLength} 0L${x} ${y}A1 1 0 0 0 ${x} ${-y}Z`; + +function Point({ + x, y, r, + traj, + pId, tags, + opacity, + selected, compared, viewed, + preSelected, preSelectColor, + isLassoing, + onContextMenu, + onClick, + onMouseEnter, + onMouseLeave, + }) { + const theme = useTheme(); + const color = theme.palette.primary.main; + const fill = alpha(color, opacity); + const [dx, dy] = newArr(2, i => traj[traj.length - 1][i] - traj[0][i]); + const deg = rot([dx, dy]); + const shift = useKeyPressed('Shift'); + + return + + + + + {/**/} + + + {isLassoing + ? + : {pId + 1}} + +} + +export default Point; + +const VizPoint = styled('g', { + shouldForwardProp: propName => !['selected', 'isLassoing', 'opacity', 'viewed', 'compared', 'shift'].includes(propName) +})(({theme, selected, isLassoing, viewed, compared, opacity, shift}) => ({ + ...(isLassoing && { + pointerEvents: 'none', + }), + ...(selected && { + stroke: selectionColor[0], + fill: selectionColor[0], + strokeWidth: 7, + }), + ...(compared && { + stroke: selectionColor[1], + fill: selectionColor[1], + strokeWidth: 7, + }), + ...(!isLassoing && viewed && { + stroke: selectionColor[Number(shift)], + fill: selectionColor[Number(shift)], + strokeWidth: 7, + }), +})) +const PointIdx = styled('text')({ + textAnchor: 'middle', + dominantBaseline: 'central', + fontSize: 34, + pointerEvents: 'none', +}) +const PointAnchor = styled('circle', { + shouldForwardProp: propName => !['preSelected', 'color'].includes(propName) +})(({theme, preSelected, color}) => ({ + r: 7, + fill: preSelected ? color : theme.palette.primary.main, +})) \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Projection/WorkerTagsMenu.jsx b/frontend/src/views/StrategyView/Projection/WorkerTagsMenu.jsx new file mode 100644 index 0000000..3072353 --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/WorkerTagsMenu.jsx @@ -0,0 +1,40 @@ +import {Autocomplete, Chip, TextField} from "@mui/material"; +import {inject, observer} from "mobx-react"; +import {useTranslation} from "react-i18next"; + +const defaultTags = t => [ + t('Game.StratTag.Attack'), + t('Game.StratTag.Defend'), + t('Game.StratTag.AntiGank'), + t('Game.StratTag.Farm'), + t('Game.StratTag.Gank'), + t('Game.StratTag.Fog'), +] + +function WorkerTagsMenu({store, tagSelection}) { + const tags = store.clusterTags(tagSelection); + + const {t} = useTranslation(); + + return + store.setTags(tagSelection, new Set(newValue), tags) + } + options={defaultTags(t)} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } + style={{width: window.innerWidth / 4}} + renderInput={params => ( + + )}/> +} + +export default inject('store')(observer(WorkerTagsMenu)); \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Projection/index.jsx b/frontend/src/views/StrategyView/Projection/index.jsx new file mode 100644 index 0000000..000ef8e --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/index.jsx @@ -0,0 +1,117 @@ +import usePointLassoSelection from "./usePointLassoSelection.js"; +import useLasso from "./useLasso.js"; +import {Fragment, useCallback, useRef} from "react"; +import ConvexHull, {LassoGroup} from "./ConvexHull.jsx"; +import {inject, observer} from "mobx-react"; +import useContextMenu from "../../../utils/useContextMenu.jsx"; +import {selectionColor} from "../../../utils/theme.js"; +import useKeyPressed from "../../../utils/useKeyPressed.js"; +import WorkerTagsMenu from "./WorkerTagsMenu.jsx"; +import {probOpacity} from "../../../utils/encoding.js"; +import Point from "./Point.jsx"; +import useProjLayout from "./useProjLayout.js"; +import {alpha} from "@mui/material"; + +const W = 1000, H = 1000, rScale = 25; + +/** + * + * @param {import('src/model/Strategy.js').Prediction[]} allPredictors + * @param {number[][]} predictorGroups + * @param {number[]} selectedPredictors + * @param {number[]} comparedPredictors + * @param {number[]} viewedPredictors + * @param {(predIds: number[], whichGroup: 0 | 1) => void} onSelectGroup + * @param {(predId: number[]) => void} onViewPredictors + * @constructor + */ +function PredictorsProjection({ + allPredictors, + predictorGroups, + selectedPredictors, + comparedPredictors, + viewedPredictors, + onSelectGroup, + onViewPredictors, + store, + }) { + /** + * Instructions on force-directed graph: + * 1. Variable "predProj" here recorded the projection location of each point, + * in the format of Array<[number, number]>, each number is in [0, 1] + * 2. Use a hook to re-layout the points with force-directed graph. + * */ + const predProj = store.predictionProjection; + const {points, onInit} = useProjLayout(predProj, predictorGroups, rScale); + const {lasso, isDrawing, handleMouseDown, handleMouseUp, handleMouseMove} = useLasso(); + const shift = useKeyPressed('Shift'); + const preSelectedPointsIdx = usePointLassoSelection(points, lasso); + const handleClear = useCallback(e => { + e.preventDefault(); + e.stopPropagation(); + onSelectGroup([], 0); + onSelectGroup([], 1); + }, []); + + const tagSelection = useRef([]); + const {menuFactory, onContextMenu} = useContextMenu(); + const handleContextMenu = pId => e => { + e.stopPropagation(); + e.preventDefault(); + onContextMenu(e); + tagSelection.current = pId; + } + const handleSelectPoint = pId => e => e.button === 0 && onSelectGroup([pId], Number(e.shiftKey)); + + return + { + if (isDrawing) onSelectGroup(preSelectedPointsIdx, shift ? 1 : 0); + handleMouseUp(e); + }}> + + {predictorGroups.map((g, gId) => ( + + ))} + + + {points.map((point, pId) => { + const opacity = probOpacity(allPredictors[pId].probability); + return onViewPredictors([pId])} + onMouseLeave={() => onViewPredictors([])}/> + })} + + {isDrawing && `${p[0] * W} ${p[1] * H}`).join('L')} + color={alpha(selectionColor[Number(shift)], 0.2)} + width={W / 200}/>} + + {menuFactory()} + +} + +export default inject('store')(observer(PredictorsProjection)); \ No newline at end of file diff --git a/src/views/StrategyView/Projection/useLasso.js b/frontend/src/views/StrategyView/Projection/useLasso.js similarity index 100% rename from src/views/StrategyView/Projection/useLasso.js rename to frontend/src/views/StrategyView/Projection/useLasso.js diff --git a/src/views/StrategyView/Projection/usePointLassoSelection.js b/frontend/src/views/StrategyView/Projection/usePointLassoSelection.js similarity index 100% rename from src/views/StrategyView/Projection/usePointLassoSelection.js rename to frontend/src/views/StrategyView/Projection/usePointLassoSelection.js diff --git a/frontend/src/views/StrategyView/Projection/useProjLayout.js b/frontend/src/views/StrategyView/Projection/useProjLayout.js new file mode 100644 index 0000000..fcea706 --- /dev/null +++ b/frontend/src/views/StrategyView/Projection/useProjLayout.js @@ -0,0 +1,61 @@ +import {useCallback, useEffect, useRef, useState} from "react"; +import {forceCollide, forceSimulation} from "d3"; + +/** + * + * @param {[number, number, number][]} ps + * @param {number[][]} pg + * @return {{points: [number, number][], onInit: () => void}} + */ +export default function useProjLayout(ps, pg, rScale) { + const [points, setPoints] = useState([]); + const [moving, setMoving] = useState(false); + const nodesRef = useRef([]); + + //region layout change + const onLayout = useCallback(() => { + const nodes = nodesRef.current; + const collide = forceCollide().radius(d => d.r).iterations(3); + return forceSimulation(nodes) + .alphaMin(0.9) + .force('collide', collide) + .on('tick', () => setPoints(nodes.map(n => [n.x, n.y, n.r * rScale]))) + .on('end', () => setMoving(false)); + }, []); + + useEffect(() => { + if (moving) { + const simulation = onLayout(); + return () => simulation.stop(); + } + }, [moving]); + //endregion + + //region data processing + const onInit = useCallback(() => { + nodesRef.current = []; + const nodes = nodesRef.current; + for (const [gid, g] of pg.entries()) + for (const pid of g) { + const p = ps[pid]; + nodes.push({ + x: p[0] * 0.8 + 0.1, + y: p[1] * 0.8 + 0.1, + r: p[2] / rScale, + group: gid, + id: pid, + }); + } + nodes.sort((a, b) => a.id - b.id); + setPoints(nodes.map(n => [n.x, n.y, n.r * rScale])) + setMoving(true); + }, [ps]); + + useEffect(() => onInit(), [ps]); + //endregion + + return { + points: points.length !== ps.length ? ps : points, + onInit, + }; +} \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Storyline/index.jsx b/frontend/src/views/StrategyView/Storyline/index.jsx new file mode 100644 index 0000000..9958811 --- /dev/null +++ b/frontend/src/views/StrategyView/Storyline/index.jsx @@ -0,0 +1,131 @@ +import {inject, observer} from "mobx-react"; +import {useRef} from "react"; +import {Group, Layer, Path, Rect, Stage, Text} from "react-konva"; +import {styled, useTheme} from "@mui/material/styles"; +import useStorylineLayout from "./useLayout.js"; +import useStorylineVisualElements from "./useLines.js"; +import {selectionColor} from "../../../utils/theme.js"; +import {lighten} from "@mui/material"; + +function createPath(line) { + let path = `M${line[0][0]} ${line[0][1]}` ; + for (let s = 1, t = 2; t < line.length; s += 2, t += 2) { + path += `V${line[s][1]}`; + path += `C${line[s][0]} ${line[s][1] * 0.7 + line[t][1] * 0.3} ${line[t][0]} ${line[s][1] * 0.3 + line[t][1] * 0.7} ${line[t][0]} ${line[t][1]}` + } + path += `V${line[line.length - 1][1]}`; + return path; +} + +const config = { + pt: 30, + pb: 10, + pl: 30, + pr: 20, + stageMaxGap: 100, + stageGapMaxRatio: 0.2, + lineMaxGap: 10, + lineGapMaxRatio: 0.2, + defaultFontSize: 7, + highlightFontSize: 20, +} + +/** + * + * @param {import('src/store/store.js').Store} store + * @param {number} width + * @param {number} height + * @return {JSX.Element} + * @constructor + */ +function PredictorsStoryline({store, width, height}) { + const containerRef = useRef(null); + const data = store.instancesData; + const layout = useStorylineLayout(data); + const {lines, groups, lineGap} = useStorylineVisualElements(layout, width, height, config); + + const theme = useTheme(); + + return + + + {groups.map((g, gId) => ( + + + {g.instances >= layout.totalInstances * 0.1 && ( + + + + )} + + ))} + {/*{layout.stages.map((stage, sId) => stage.instances >= layout.totalInstances * 0.05 && (*/} + {/* */} + {/* */} + {/* */} + {/*))}*/} + {lines.map((line, lId) => { + const selected = store.selectedPredictors.includes(lId), + compared = store.comparedPredictors.includes(lId), + viewed = store.viewedPredictions.includes(lId); + return + + store.viewPredictions([lId])} + onMouseLeave={() => store.viewPredictions([])} + // opacity={probOpacity(store.predictions[lId]?.probability)} + stroke={viewed ? theme.palette.secondary.main + : selected ? selectionColor[0] + : compared ? selectionColor[1] + : lighten(theme.palette.text.primary, 0.65)} + strokeWidth={(selected || compared || viewed) ? 1 : 1}/> + + })} + {groups.map((g, gId) => ( + + + {/*{newArr(g.cnt, i => (*/} + {/* */} + {/*))}*/} + + ))} + + + +} + +export default inject('store')(observer(PredictorsStoryline)); + +const Container = styled('div')({ + width: '100%', + height: '100%', + overflow: 'hidden auto', +}) \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Storyline/useData.js b/frontend/src/views/StrategyView/Storyline/useData.js new file mode 100644 index 0000000..e40ce73 --- /dev/null +++ b/frontend/src/views/StrategyView/Storyline/useData.js @@ -0,0 +1,87 @@ +import {randint, shuffle} from "../../../utils/fakeData.js"; +import newArr from "../../../utils/newArr.js"; + +function mergeGroup(groups) { + let i = randint(0, groups.length - 1), j = randint(0, groups.length - 1); + if (i === j) j = (j + 1) % groups.length; + if (i > j) [i, j] = [j, i]; + const groupJ = groups.splice(j, 1)[0], groupI = groups.splice(i, 1)[0]; + groups.push([...groupI, ...groupJ]); +} + +function splitGroup(groups) { + const largeGroups = groups.map((_, i) => i).filter(i => groups[i].length > 1); + const gIdToSplit = largeGroups[randint(0, largeGroups.length - 1)]; + const group = groups.splice(gIdToSplit, 1)[0]; + shuffle(group); + const splitIdx = randint(1, group.length - 1); + groups.push(group.slice(0, splitIdx), group.slice(splitIdx)); +} + +function moveItem(groups) { + const sourceGroupIdx = randint(0, groups.length - 1); + const targetGroupIdx = randint(0, groups.length - 1); + const sourceGroup = groups[sourceGroupIdx]; + const targetGroup = groups[targetGroupIdx]; + const sourceItemIdx = randint(0, sourceGroup.length - 1); + const itemToMove = sourceGroup.splice(sourceItemIdx, 1)[0]; + targetGroup.push(itemToMove); + + if (sourceGroup.length === 0) + groups.splice(sourceGroupIdx, 1); +} + +function changeGroup(groups) { + groups = JSON.parse(JSON.stringify(groups)); + for (let i = randint(4, 6); i--;) { + const op = Math.random(); + if (op < 0.2) mergeGroup(groups) + else if (op < 0.5) splitGroup(groups) + else moveItem(groups) + } + return groups; +} + +/** + * @return {import('src/model/Strategy.js').PredictorsStoryline} + */ +export default function genStorylineData(initGroups) { + const totalInstances = randint(300, 700); + const numPredictors = initGroups.reduce((p, c) => p + c.length, 0); + + const instances = newArr(randint(5, 7), () => randint(0, totalInstances)); + instances.sort((a, b) => a - b); + instances.push(totalInstances); + for (let i = instances.length - 1; i > 0; i--) + instances[i] -= instances[i - 1]; + instances.sort((a, b) => b - a); + while (instances[instances.length - 1] === 0) instances.pop(); + + let curGroups = JSON.parse(JSON.stringify(initGroups)); + /** + * @type {import('src/model/Strategy.js').StorylineStage[]} + */ + const stages = []; + instances.forEach(instances => { + stages.push({ + groups: curGroups, + instances + }) + curGroups = changeGroup(curGroups); + }) + + return { + stages, + numPredictors, + totalInstances, + } +} + +export function initStorylineData() { + return { + stages: [{ + groups: newArr(20, i => [i]), + instances: 0, + }], numPredictors: 20, totalInstances: 1 + } +} \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Storyline/useLayout.js b/frontend/src/views/StrategyView/Storyline/useLayout.js new file mode 100644 index 0000000..72606bc --- /dev/null +++ b/frontend/src/views/StrategyView/Storyline/useLayout.js @@ -0,0 +1,69 @@ +import {useMemo} from "react"; + +/** + * @param {import('src/model/Strategy.js').StorylinesStage} stage + */ +function getItemPos(stage) { + const res = []; + let pos = 0; + stage.groups.forEach(group => group.forEach(item => res[item] = pos++)); + return res; +} + +/** + * @param {import('src/model/Strategy.js').StorylineGroup} ga + * @param {import('src/model/Strategy.js').StorylineGroup} gb + * @param {number[]} lastPos + * @return {-1, 0, 1} + */ +function getCrossings(ga, gb, lastPos) { + let res = 0; + for (const i of ga) + for (const j of gb) + if (lastPos[i] < lastPos[j]) res--; + else res++; + return res; +} + +/** + * @param {import('src/model/Strategy.js').PredictorsStoryline} data + */ +function sortGroups(data) { + for (let i = 1; i < data.stages.length; i++) { + const lastPos = getItemPos(data.stages[i - 1]); + const stage = data.stages[i]; + stage.groups.sort((ga, gb) => + getCrossings(ga, gb, lastPos) + ) + } +} + +/** + * @param {import('src/model/Strategy.js').PredictorsStoryline} data + */ +function sortItems(data) { + let lastPos = getItemPos(data.stages[0]); + for (let i = 1; i < data.stages.length; i++) { + const stage = data.stages[i]; + for (const group of stage.groups) + group.sort((a, b) => lastPos[a] - lastPos[b]); + lastPos = getItemPos(stage); + } +} + +/** + * + * @param {import('src/model/Strategy.js').PredictorsStoryline} data + * @return {import('src/model/Strategy.js').PredictorsStoryline} + */ +export default function useStorylineLayout(data) { + return useMemo(() => { + data.stages.reverse(); + sortGroups(data); + sortItems(data); + data.stages.reverse(); + sortGroups(data); + sortItems(data); + return data; + }, [data]); +} \ No newline at end of file diff --git a/frontend/src/views/StrategyView/Storyline/useLines.js b/frontend/src/views/StrategyView/Storyline/useLines.js new file mode 100644 index 0000000..49eb0b9 --- /dev/null +++ b/frontend/src/views/StrategyView/Storyline/useLines.js @@ -0,0 +1,123 @@ +import {useMemo} from "react"; +import newArr from "../../../utils/newArr.js"; + +/** + * + * @param {number[]} nG + * @param {number[]} cG + * @return boolean + */ +function isSubGroup(nG, cG) { + if (nG.length > cG.length) return false; + if (nG.some(lId => !cG.includes(lId))) return false; + return cG.indexOf(nG[nG.length - 1]) - cG.indexOf(nG[0]) === nG.length - 1; + +} + +/** + * + * @param {import('src/model/Strategy.js').PredictorsStoryline} data + * @param {number} width + * @param {number} height + * @return {{lines: [number, number][][], groups: {x: number, y: number, width: number, height: number}[], lineGap: number}} + */ +export default function useStorylineLines(data, width, height, config) { + return useMemo(() => { + //region key parameters + const {pt, pb, pl, pr, stageMaxGap, stageGapMaxRatio, lineMaxGap, lineGapMaxRatio} = config; + const stageGap = Math.min( + stageMaxGap, + (height - pt - pb) * stageGapMaxRatio / (data.stages.length - 1) + ); + const instUnit = (height - pt - pb - (data.stages.length - 1) * stageGap) / data.totalInstances; + const maxGroups = data.stages.reduce((p, c) => Math.max(p, c.groups.length), 0); + const lineGap = Math.min( + lineMaxGap, + (width - pl - pr) / (data.numPredictors + (maxGroups - 1) / lineGapMaxRatio - maxGroups) + ); + const groupGap = (width - pl - pr - (data.numPredictors - 1) * lineGap) / (maxGroups - 1) + lineGap; + //endregion + + //region points + const lines = newArr(data.numPredictors, () => newArr(data.stages.length * 2, () => [0, 0])); + let y = pt; + for (const [i, stage] of data.stages.entries()) { + const yt = y, yb = y + stage.instances * instUnit; + y = yb + stageGap; + + const stageWidth = (stage.groups.length - 1) * groupGap + (data.numPredictors - stage.groups.length) * lineGap; + let x = width / 2 - stageWidth / 2; + for (const group of stage.groups) { + for (const lId of group) { + lines[lId][i * 2] = [x, yt]; + lines[lId][i * 2 + 1] = [x, yb]; + x += lineGap; + } + x += groupGap - lineGap; + } + } + //endregion + + //region layout refinement based on group + const sortSIds = newArr(data.stages.length, i => i); + sortSIds.sort((a, b) => data.stages[b].groups.length - data.stages[a].groups.length); + + function refine(sId, step) { + const nsId = sId + step; + if (nsId < 0 || nsId >= data.stages.length) return; + let modFlag = false; + const curGroups = data.stages[sId].groups, nxtGroups = data.stages[nsId].groups; + if (curGroups.length > nxtGroups.length) + for (const [initNGId, initGId, dir] of [[0, 0, 1], [nxtGroups.length - 1, curGroups.length - 1, -1]]) + for (let gId = initGId, ngId = initNGId; ngId >= 0 && ngId < nxtGroups.length; gId += dir, ngId += dir) { + if (!isSubGroup(nxtGroups[ngId], curGroups[gId])) break; + + for (const lId of nxtGroups[ngId]) { + const nx = lines[lId][sId * 2][0]; + lines[lId][nsId * 2][0] = nx; + lines[lId][nsId * 2 + 1][0] = nx; + } + modFlag = true; + } + if (modFlag) refine(nsId, step); + } + + for (const sId of sortSIds) { + refine(sId, -1); + refine(sId, 1); + } + //endregion + + //region groups + const uniqueGroups = {}; + for (const [i, stage] of data.stages.entries()) + for (const group of stage.groups) { + const x = lines[group[0]][i * 2][0].toFixed(2); + const groupId = `${group.join('|')},${x}`; + if (!uniqueGroups[groupId]) uniqueGroups[groupId] = []; + uniqueGroups[groupId].push(i); + } + + const groups = []; + for (const groupId of Object.keys(uniqueGroups)) { + const lineIds = groupId.split('|'); + const l1 = parseInt(lineIds[0]), l2 = parseInt(lineIds[lineIds.length - 1]); + const spannedStages = uniqueGroups[groupId]; + let start = 0, end = 0; + while (end < spannedStages.length) { + let instances = data.stages[spannedStages[start]].instances; + while (end + 1 < spannedStages.length && spannedStages[end + 1] - spannedStages[start] === end + 1 - start) { + end++; + instances += data.stages[spannedStages[end]].instances; + } + const [x, y] = lines[l1][spannedStages[start] * 2]; + const [x1, y1] = lines[l2][spannedStages[end] * 2 + 1]; + groups.push({x, y, width: x1 - x, height: y1 - y, cnt: lineIds.length, instances}); + start = end = end + 1; + } + } + //endregion + + return {lines, groups, lineGap}; + }, [data, width, height]) +} \ No newline at end of file diff --git a/src/views/StrategyView/index.jsx b/frontend/src/views/StrategyView/index.jsx similarity index 58% rename from src/views/StrategyView/index.jsx rename to frontend/src/views/StrategyView/index.jsx index ce48623..1b3cde9 100644 --- a/src/views/StrategyView/index.jsx +++ b/frontend/src/views/StrategyView/index.jsx @@ -1,7 +1,8 @@ import {inject, observer} from "mobx-react"; import {Divider} from "@mui/material"; -import {styled} from "@mui/material/styles"; +import {styled, useTheme} from "@mui/material/styles"; import PredictorsProjection from "./Projection/index.jsx"; +import PredictorsStoryline from "./Storyline/index.jsx"; /** * @@ -10,21 +11,25 @@ import PredictorsProjection from "./Projection/index.jsx"; * @constructor */ -function StrategyView({store}) { +function StrategyView({store, width, height}) { + const theme = useTheme(); return + onViewPredictors={store.viewPredictions}/> - {/**/} - {/* */} - {/**/} + + + } @@ -40,15 +45,22 @@ const Container = styled('div')({ const Projection = styled('div')({ position: 'relative', - paddingTop: '100%', + paddingTop: '75%', width: '100%', height: 0, }) const Wrapper = styled('div')({ position: 'absolute', - left: 0, + left: '12.5%', top: 0, - width: '100%', + width: '75%', height: '100%', +}) + +const Detail = styled('div')({ + position: 'relative', + flex: 1, + width: '100%', + overflow: 'hidden', }) \ No newline at end of file diff --git a/frontend/src/views/TitleBar/Settings.jsx b/frontend/src/views/TitleBar/Settings.jsx new file mode 100644 index 0000000..99c52fe --- /dev/null +++ b/frontend/src/views/TitleBar/Settings.jsx @@ -0,0 +1,38 @@ +import { + Checkbox, + Dialog, + DialogContent, + DialogTitle, + FormControlLabel, + ToggleButton, + ToggleButtonGroup +} from "@mui/material"; +import {useTranslation} from "react-i18next"; +import {inject, observer} from "mobx-react"; + +function Settings({open, store, onClose}) { + const {i18n} = useTranslation(); + + return + Settings + +
+ i18n.changeLanguage(v)}> + Eng + 中文 + +
+
+ store.setDevMode(checked)}/> + }/> +
+
+
+} + +export default inject('store')(observer(Settings)) \ No newline at end of file diff --git a/frontend/src/views/TitleBar/index.jsx b/frontend/src/views/TitleBar/index.jsx new file mode 100644 index 0000000..7c8b780 --- /dev/null +++ b/frontend/src/views/TitleBar/index.jsx @@ -0,0 +1,95 @@ +/** + * + * @param {number} width + * @constructor + */ +import {Button, Typography} from "@mui/material"; +import {styled} from "@mui/material/styles"; +import {viewSize} from "../../utils/layout.js"; +import {inject, observer} from "mobx-react"; +import {readJSONFile, selectFile} from "../../utils/file.js"; +import {useTranslation} from "react-i18next"; +import {Signpost} from "@mui/icons-material"; +import {useState} from "react"; +import Settings from "./Settings.jsx"; + +/** + * @param {number} width + * @param {import('src/store/store').Store} store + * @returns {JSX.Element} + * @constructor + */ +function TitleBar({width, store}) { + const {t} = useTranslation(); + const [settings, setSettings] = useState(false); + const handleOpenSettings = () => setSettings(true); + const handleCloseSettings = () => setSettings(false); + + const handleImportData = () => { + selectFile() + .then(file => { + store.setWaiting(true); + return readJSONFile(file); + }) + .catch(window.alert) + .then(data => store.setData(data.filename, data.data)) + .finally(() => store.setWaiting(false)); + } + const handleImportCase = () => { + selectFile() + .then(file => { + store.setWaiting(true); + return readJSONFile(file); + }) + .catch(window.alert) + .then(data => { + if (data.data.match_id !== store.gameData.gameInfo.match_id) + throw new Error("The case is not for this match!"); + store.setCase(data.data); + }) + .catch(window.alert) + .finally(() => store.setWaiting(false)); + } + const handleSaveCase = () => store.saveCase(); + + return + + {t('System.SystemName')} +
+ + + + + + +} + +export default inject('store')(observer(TitleBar)); + +const Bar = styled('div')(({theme}) => ({ + position: 'absolute', + left: 0, + top: 0, + height: viewSize.appTitleBarHeight, + backgroundColor: theme.palette.primary.main, + borderBottomRightRadius: theme.shape.borderRadius, + color: theme.palette.primary.contrastText, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +})) diff --git a/vite.config.js b/frontend/vite.config.js similarity index 100% rename from vite.config.js rename to frontend/vite.config.js diff --git a/yarn.lock b/frontend/yarn.lock similarity index 90% rename from yarn.lock rename to frontend/yarn.lock index a7fd8df..01850de 100644 --- a/yarn.lock +++ b/frontend/yarn.lock @@ -916,6 +916,11 @@ browserslist@^4.21.9: node-releases "^2.0.13" update-browserslist-db "^1.0.13" +bubblesets-js@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.2.tgz#56d0bca6a38f9364486b29ce64ef501dc7584147" + integrity sha512-TkofIls7vjYlqgLERl817zi53ig6d/MSCh5XnxR7LAiZkL90prirLZP6QjtwMPRiWTXfCxLih1U/mzDqOlWgkw== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -951,6 +956,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + clsx@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" @@ -980,6 +990,11 @@ color-name@~1.1.4: resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@7: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -995,6 +1010,11 @@ convert-source-map@^2.0.0: resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-js@^3.22.4: + version "3.34.0" + resolved "https://registry.npmmirror.com/core-js/-/core-js-3.34.0.tgz#5705e6ad5982678612e96987d05b27c6c7c274a5" + integrity sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag== + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -1020,6 +1040,250 @@ csstype@^3.0.2, csstype@^3.1.2: resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.2" + resolved "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.4" + resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo@3: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" + integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-random@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-scale-chromatic@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" + integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.2.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@^7.8.5: + version "7.8.5" + resolved "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c" + integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: version "4.3.4" resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -1050,6 +1314,13 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, de has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delaunator@5: + version "5.0.1" + resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -1367,6 +1638,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + find-root@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -1584,6 +1860,13 @@ i18next@^23.5.1: dependencies: "@babel/runtime" "^7.22.5" +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.0: version "5.2.4" resolved "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" @@ -1624,6 +1907,11 @@ internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" @@ -2174,6 +2462,14 @@ react-konva@^18.2.10: react-reconciler "~0.29.0" scheduler "^0.23.0" +react-range-slider-input@^3.0.7: + version "3.0.7" + resolved "https://registry.npmmirror.com/react-range-slider-input/-/react-range-slider-input-3.0.7.tgz#88ceb118b33d7eb0550cec1f77fc3e60e0f880f9" + integrity sha512-yAJDDMUNkILOcZSCLCVbwgnoAM3v0AfdDysTCMXDwY/+ZRNRlv98TyHbVCwPFEd7qiI8Ca/stKb0GAy//NybYw== + dependencies: + clsx "^1.1.1" + core-js "^3.22.4" + react-reconciler@~0.29.0: version "0.29.0" resolved "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.29.0.tgz#ee769bd362915076753f3845822f2d1046603de7" @@ -2265,6 +2561,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + rollup@^3.27.1: version "3.29.4" resolved "https://registry.npmmirror.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" @@ -2279,6 +2580,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@1: + version "1.3.3" + resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + safe-array-concat@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" @@ -2298,6 +2604,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" diff --git a/index.html b/index.html deleted file mode 100644 index 074ac6e..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - RoadSign for DOTA 2 - - -
- - - diff --git a/public/locales/cn/translation.json b/public/locales/cn/translation.json deleted file mode 100644 index 7f0125c..0000000 --- a/public/locales/cn/translation.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "Game": { - "Dire": "夜魇", - "Radiant": "天辉" - }, - "System": { - "Actions": { - "import": "导入" - }, - "ContextView": { - "GameContext": "游戏环境", - "More": "显示更多", - "ViewName": "环境视图" - }, - "MapView": "地图视图", - "StrategyView": { - "PredNum": "{{num}}条路", - "Predict": "预测", - "SID": "策略{{sId}}", - "ViewName": "策略视图" - }, - "SystemName": "DOTA 2 玩家轨迹预测分析系统" - } -} \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json deleted file mode 100644 index 530a3ea..0000000 --- a/public/locales/en/translation.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "Game": { - "Dire": "Dire", - "Radiant": "Radiant" - }, - "System": { - "Actions": { - "import": "import" - }, - "ContextView": { - "GameContext": "Game Context", - "More": "show more", - "ViewName": "Context View" - }, - "MapView": "Map View", - "StrategyView": { - "PredNum": "{{num}} pred.", - "Predict": "Predict", - "SID": "Strat. {{sId}}", - "ViewName": "Strategy View" - }, - "SystemName": "RoadSign for DOTA 2" - } -} \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js deleted file mode 100644 index 780859c..0000000 --- a/src/store/store.js +++ /dev/null @@ -1,223 +0,0 @@ -import {makeAutoObservable, observable} from "mobx"; -import {playerColors, updateGameData} from "../utils/game.js"; -import {genRandomStrategies, genRandomStrategy} from "../utils/fakeData.js"; - -class Store { - constructor() { - // 此处是利用了MobX的监听机制,makeAutoObservable会按照以下规则,处理该类中定义的变量与函数 - // 1. 该类中的变量将被设置为observable,每当监听到变量改变,引用变量的视图将自动刷新 - // 3. 该类中的getter函数将被设置为computed数值缓存,即根据其他observable的变量计算出一个新的值;当依赖变量改变,缓存值将被重新计算 - // 2. 该类中的其他函数将被设置为action,只有action中修改变量会被监听,并触发computed值的重新计算 - - // 此处的gameData额外设置为observable.shallow有两个原因。 - // 1. observable会监听整个hierarchical的结构,observable.shallow只会监听根部引用的改变,提高效率 - // 2. gameData导入后是不变的,因此没必要监听其内部元素变化 - makeAutoObservable(this, {gameData: observable.shallow}); - - this.setStrategies(genRandomStrategies()); - } - - /** - * 系统状态,在执行一些费时的操作时,如果不希望用户在此期间和系统交互,可以设置waiting为true - * @type {boolean} - */ - waiting = false - setWaiting = state => this.waiting = state; - - /** - * 当前选中的队伍 - * @type {-1 | 0 | 1} : -1 for none, 0 for team radiant, 1 for team dire - */ - focusedTeam = -1 - - /** - * 当前选中的玩家 - * @type {-1 | 0 | 1 | 2 | 3 | 4} : -1 for none, 0~4 for five playerNames - */ - focusedPlayer = -1 - - /** - * 选中一名玩家,预测他的行为 - * @param {0 | 1} teamIndex - * @param {0 | 1 | 2 | 3 | 4} playerIndex - */ - focusOnPlayer = (teamIndex, playerIndex) => { - if (teamIndex === this.focusedTeam && playerIndex === this.focusedPlayer) { - this.focusedTeam = -1; - this.focusedPlayer = -1; - } else { - this.focusedTeam = teamIndex; - this.focusedPlayer = playerIndex; - } - } - - /** - * Game gameData instance - * @type {import('src/model/D2Data.d.ts').D2Data | null} - */ - gameData = null; - /** - * Import gameData - * @param {import('src/model/D2Data.d.ts').D2Data} gameData - */ - setData = gameData => { - this.gameData = updateGameData(gameData); - } - - /** - * 当前帧 - * @type {number} - */ - frame = 0; - setFrame = f => this.frame = f; - - /** - * 从gameData中提取玩家名 - * @returns {string[][]} - */ - get playerNames() { - if (this.gameData === null) return [['', '', '', '', ''], ['', '', '', '', '']] - return [ - this.gameData.gameInfo.radiant.players.map(p => p.hero), - this.gameData.gameInfo.dire.players.map(p => p.hero), - ] - } - - /** - * 从gameData中提取总的数据帧数 - * @returns {number} - */ - get numFrames() { - if (this.gameData === null) return 0; - return this.gameData.gameRecords.length; - } - - /** - * 从gameData中提取第frame帧的玩家位置 - * @return {[number, number][][]} : the positions of 2*5 players - */ - get playerPositions() { - if (!this.gameData) return [ - new Array(5).fill(0).map(() => [0, 0]), - new Array(5).fill(0).map(() => [0, 0]) - ] - const curFrame = this.gameData.gameRecords[this.frame]; - return curFrame.heroStates.map(team => - team.map(player => - player.pos - ) - ) - } - - /** - * 从gameData中提取第frame帧的玩家存活状态 - * @return {boolean[][]}: life state of 2*5 players - */ - get playerLifeStates() { - if (!this.gameData) return [ - new Array(5).fill(false), - new Array(5).fill(false), - ] - const curFrame = this.gameData.gameRecords[this.frame]; - return curFrame.heroStates.map(team => - team.map(player => - player.life === 0 - ) - ) - } - - /** - * 从gameData中提取第frame帧的游戏中时间(单位:秒) - * @return {number}:从-90~0为比赛正式开始前的准备时间,0~+∞是比赛正式进行的时间。 - */ - get curTime() { - if (!this.gameData) return 0; - const curFrame = this.gameData.gameRecords[this.frame]; - return curFrame.game_time; - } - - get curFrame() { - return this.frame; - } - - get curColor() { - return playerColors[this.focusedTeam][this.focusedPlayer]; - } - - get selectedPlayerTrajectory() { - // Check if a player is selected - if (this.focusedPlayer === -1 || !this.gameData) { - return [] - } - let selectedPlayerTra = [] - // for (var i=1; i <= this.gameData.gameRecords.length-1; i++) { - for (var i= Math.max(1, this.frame-450); i <= Math.min(this.gameData.gameRecords.length-1, this.frame+150); i++) { - var pos = this.gameData.gameRecords[i].heroStates.map(team => - team.map(player => - player.pos - ) - ); - var pos3d = pos[this.focusedTeam][this.focusedPlayer]; - let pos2d = [pos3d[0], pos3d[1]]; - selectedPlayerTra.push(pos2d); - } - return selectedPlayerTra; - } - - get allPlayerTrajectory () { - let allPlayerTra = Array.from({ length: 2 }, () => - Array.from({ length: 5 }, () => []) - ); - - const startFrame = Math.max(1, this.frame - 450); - const endFrame = Math.min(this.gameData.gameRecords.length - 1, this.frame + 150); - - for (let i = startFrame; i <= endFrame; i++) { - const frameData = this.gameData.gameRecords[i].heroStates; - for (let teamIdx = 0; teamIdx < 2; teamIdx++) { - for (let playerIdx = 0; playerIdx < 5; playerIdx++) { - const playerPos = frameData[teamIdx][playerIdx].pos; - var pos = [playerPos[0], playerPos[1]] - allPlayerTra[teamIdx][playerIdx].push(pos); - } - } - } - console.log(allPlayerTra) - return allPlayerTra; - } - - /** - * Strategies - * @type {import('src/model/Strategy.d.ts').StrategyList} - */ - strategies = [] - setStrategies = s => this.strategies = s; - expandedStrategy = -1 - expandStrategy = sId => this.expandedStrategy = (this.expandedStrategy === sId) ? -1 : sId; - viewedStrategy = -1 - viewStrategy = sId => { - if (this.viewedPrediction === -1) { - const targetSId = (this.viewedStrategy === sId) ? -1 : sId; - this.viewedStrategy = targetSId; - this.expandedStrategy = targetSId; - } else { - this.viewedStrategy = sId; - this.expandedStrategy = sId; - this.viewedPrediction = -1; - } - } - viewedPrediction = -1 - viewPrediction = (sId, pId) => { - if (this.viewedStrategy === sId && this.viewedPrediction === pId) { - this.expandedStrategy = -1; - this.viewedStrategy = -1; - this.viewedPrediction = -1; - } else { - this.expandedStrategy = sId; - this.viewedStrategy = sId; - this.viewedPrediction = pId; - } - } -} - -export default Store; diff --git a/src/utils/fakeData.js b/src/utils/fakeData.js deleted file mode 100644 index 0cbd7f9..0000000 --- a/src/utils/fakeData.js +++ /dev/null @@ -1,101 +0,0 @@ -const DIS = 175; - -function randint(min, max) { - return Math.floor(Math.random() * (max + 1 - min)) + min; -} - -function rand(min, max) { - return Math.random() * (max - min) + min; -} - -export function getStratAttention(predictors, groupName, itemName) { - const values = predictors.map(p => p.attention[groupName][itemName]); - return { - avg: values.reduce((p, c) => p + c, 0) / values.length, - min: Math.min(...values), - max: Math.max(...values), - } -} - -export function contextFactory(factory) { - return Object.fromEntries(['p00', 'p01', 'p02', 'p03', 'p04', 'p10', 'p11', 'p12', 'p13', 'p14', 'g'].map(g => [ - g, - { - [g + 'a']: factory(g, g + 'a'), - [g + 'b']: factory(g, g + 'b'), - [g + 'c']: factory(g, g + 'c'), - [g + 'd']: factory(g, g + 'd'), - [g + 'e']: factory(g, g + 'e'), - [g + 'f']: factory(g, g + 'f'), - } - ])) -} - -/** - * @return {import('src/model/Strategy.d.ts').Prediction} - */ -function genRandomPrediction(startPos, startDir) { - const trajectory = [startPos]; - let lastPos = startPos, lastDir = startDir; - for (let i = 0; i < 10; i++) { - lastDir += rand(-Math.PI / 8, Math.PI / 8); - const dis = DIS * rand(0.9, 1.1); - const newPos = [lastPos[0] + dis * Math.cos(lastDir), lastPos[1] + dis * Math.sin(lastDir)]; - trajectory.push(newPos) - lastPos = newPos; - } - return { - trajectory, - probability: rand(0, 1), - attention: contextFactory(() => rand(0, 1)) - } -} - -/** - * @return {import('src/model/Strategy.d.ts').Strategy} - */ -export function genRandomStrategy(startPos, predNum) { - const randomDir = rand(0, 2 * Math.PI); - const predictors = new Array(predNum).fill(0) - .map(() => genRandomPrediction(startPos, randomDir)) - .sort((a, b) => b.probability - a.probability); - return { - predictors, - attention: contextFactory((g, i) => getStratAttention(predictors, g, i)), - } -} - -export function genContext(frame) { - return contextFactory((g, i) => { - if (i.endsWith('a') || i.endsWith('b')) return rand(0, 100) - if (i.endsWith('c') || i.endsWith('d')) return rand(0, 1) > 0.5 - return String.fromCharCode(...new Array(7) - .fill(0) - .map(() => randint(97, 122)) - ); - }) -} - -function stratProb(strat) { - return strat.predictors.reduce((p, c) => p + c.probability, 0); -} - -function randomSplit(sum, n) { - sum -= n; - const splits = new Array(n - 1).fill(0).map(() => randint(0, sum)) - splits.push(0, sum) - splits.sort((a, b) => a - b); - const res = []; - for (let i = 1; i < splits.length; i++) res.push(splits[i] - splits[i - 1] + 1); - return res; -} - -export function genRandomStrategies(startPos, predCnt = 20) { - const stratCnt = randint(4, 6); - const predNums = randomSplit(predCnt, stratCnt); - const res = predNums.map(cnt => genRandomStrategy(startPos, cnt)) - const sumProb = res.reduce((p, c) => p + stratProb(c), 0); - res.forEach(s => s.predictors.forEach(p => p.probability /= sumProb)); - res.sort((a, b) => stratProb(b) - stratProb(a)); - return res; -} \ No newline at end of file diff --git a/src/utils/game.js b/src/utils/game.js deleted file mode 100644 index 4d5d0cd..0000000 --- a/src/utils/game.js +++ /dev/null @@ -1,71 +0,0 @@ -export const teamNames = ['Radiant', 'Dire'] -export const teamShapes = ['rect', 'circle'] -export const playerColors = [ - ['#3476FF', '#67FFC0', '#C000C0', '#F3F00C', '#FF6C00'], - ['#FE87C3', '#A2B548', '#66D9F7', '#008422', '#A56A00'], -] -export const MIN_X = 8240, MAX_X = 24510; -export const MIN_Y = 8220, MAX_Y = 24450; -const x = v => (v - MIN_X) / (MAX_X - MIN_X); -const y = v => (MAX_Y - v) / (MAX_Y - MIN_Y); -const rx = v => v * (MAX_X - MIN_X) + MIN_X; -const ry = v => MAX_Y - v * (MAX_Y - MIN_Y); - -/** - * Project the positions in data onto the map - * @param {[number, number]} posInData - * @param {number} mapSize - * @param {boolean=False} reverse : project the position on the map to that in data - */ -export const mapProject = (posInData, mapSize, reverse = false) => { - if (reverse) - return [ - rx(posInData[0] / mapSize), - ry(posInData[1] / mapSize), - ] - else - return [ - x(posInData[0]) * mapSize, - y(posInData[1]) * mapSize, - ] -} - -/** - * Convert compressed game data into released form - * @param {import('src/model/D2Data.d.ts').D2Data} compressedGameData - * @return {import('src/model/D2Data.d.ts').D2Data} - */ -export const updateGameData = (compressedGameData) => { - const preRecord = { - tick: 0, - game_time: 0, - roshan_hp: 0, - is_night: false, - events: [], - heroStates: [[], []], - teamStates: [], - }; - const updateGR = (gr) => { - preRecord.tick = gr.tick; - preRecord.game_time = gr.game_time; - if (gr.hasOwnProperty('is_night')) preRecord.is_night = gr.is_night; - if (gr.hasOwnProperty('roshan_hp')) preRecord.roshan_hp = gr.roshan_hp; - preRecord.events = gr.events; - for (let i = 0; i < 2; i++) - for (let j = 0; j < 5; j++) - preRecord.heroStates[i][j] = Object.assign({}, preRecord.heroStates[i][j], gr.heroStates[i][j]); - for (let i = 0; i < 2; i++) - preRecord.teamStates[i] = Object.assign({}, preRecord.teamStates[i], gr.teamStates[i]); - } - return { - gameInfo: compressedGameData.gameInfo, - gameRecords: compressedGameData.gameRecords.map(gr => { - updateGR(gr); - return JSON.parse(JSON.stringify(preRecord)); - }) - }; -} - -export function mapDis(pos1, pos2) { - return Math.sqrt((pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) + (pos1[1] - pos2[1]) * (pos1[1] - pos2[1])) -} diff --git a/src/utils/useSize.js b/src/utils/useSize.js deleted file mode 100644 index 7794329..0000000 --- a/src/utils/useSize.js +++ /dev/null @@ -1,11 +0,0 @@ -import {useLayoutEffect, useState} from "react"; - -export default function useSize(ref) { - const [size, setSize] = useState({width: 1, height: 1}); - useLayoutEffect(() => { - if (!ref.current) return; - const box = ref.current.getBoundingClientRect(); - setSize({width: box.width, height: box.height}); - }, []) - return size; -} \ No newline at end of file diff --git a/src/views/ContextView/AttentionItem.jsx b/src/views/ContextView/AttentionItem.jsx deleted file mode 100644 index ea23fa0..0000000 --- a/src/views/ContextView/AttentionItem.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import {styled} from "@mui/material/styles"; -import {alpha, Box, Slider as MuiSlider, Typography} from "@mui/material"; -import {Check, Close} from "@mui/icons-material"; - -function AttentionItem({colorLabel, label, value, attention, curAtt}) { - return - {colorLabel} - {label} - - {typeof value === 'number' && {value.toFixed(0)}} - {typeof value === 'string' && {value}} - {typeof value === 'boolean' && {value ? : }} - - - {curAtt && } - {attention && attention.avg && v.toFixed(2)}/>} - {attention && attention.length && - {attention.map((v, vId) => )} - } - - -} - -export default AttentionItem; - -const Container = styled('div')({ - display: 'flex', - height: 40, - width: '100%', - alignItems: 'center', -}) - -const Slider = styled(MuiSlider)(({theme}) => ({ - color: theme.palette.primary.main, - height: 15, - '& .MuiSlider-track': { - border: 'none', - borderRadius: 0, - }, - '& .MuiSlider-thumb': { - width: 4, - height: 4, - shadow: `0 0 4px 4px ${theme.palette.background.paper}`, - }, - '& .MuiSlider-rail': { - backgroundColor: theme.palette.background.paper, - borderRadius: 0, - }, - '& .MuiSlider-mark': { - backgroundColor: theme.palette.background.paper, - height: 30, - width: 1, - } -})) - -const Anchor = styled('div')(({theme}) => ({ - position: 'absolute', - width: 0, - height: 21, - transition: 'left .3s ease', - transform: 'translateX(-50%)', - top: 10, - borderTop: `3px solid ${theme.palette.secondary.main}`, - borderBottom: `3px solid ${theme.palette.secondary.main}`, - borderLeft: `3px solid white`, - borderRight: `3px solid white`, -})) - -const BarContainer = styled('div')(({theme}) => ({ - position: 'relative', - height: 15, - width: '100%', - backgroundColor: theme.palette.background.paper, -})) - -const Bar = styled('div')(({theme}) => ({ - position: 'absolute', - height: '100%', - top: 0, - left: 0, - backgroundColor: alpha(theme.palette.primary.main, 0.4), - borderRight: `1px solid ${theme.palette.background.default}`, -})) diff --git a/src/views/ContextView/ContextGroup.jsx b/src/views/ContextView/ContextGroup.jsx deleted file mode 100644 index b3b1d0c..0000000 --- a/src/views/ContextView/ContextGroup.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import {styled} from "@mui/material/styles"; -import {Button, Checkbox, Collapse, darken, Divider} from "@mui/material"; -import {useCallback, useEffect, useState} from "react"; -import AttentionItem from "./AttentionItem.jsx"; -import {inject, observer} from "mobx-react"; -import {useTranslation} from "react-i18next"; -import {KeyboardArrowDown} from "@mui/icons-material"; - -function useExpand(length, initDisplayCnt = 3, expandDisplayCnt = 3) { - const [open, setOpen] = useState(false); - const [displayCnt, setDisplayCnt] = useState(initDisplayCnt); - const expand = useCallback(() => setDisplayCnt(c => Math.min(length, c + expandDisplayCnt)), []); - const toggle = useCallback(() => setOpen(o => !o), []); - useEffect(() => { - if (open) setDisplayCnt(initDisplayCnt); - else setDisplayCnt(initDisplayCnt); - }, [open]); - return {open, displayCnt, expand, toggle} -} - -function calHeight(open, displayCnt, totalCnt) { - if (!open) return 60; - if (displayCnt === totalCnt) return 71 + displayCnt * 40; - return displayCnt * 40 + 101.75; -} - -function ContextGroup({store, colorLabel, groupName, context, attention, curAtt}) { - const {open, displayCnt, expand, toggle} = useExpand(Object.keys(context).length); - const {t} = useTranslation(); - const sortedContextKeys = Object.keys(context).sort((a, b) => attention[b].avg - attention[a].avg); - - return - attention[key].avg)}/> - {open && } - - {sortedContextKeys - .slice(0, displayCnt) - .map(key => ( - e.stopPropagation()} - onChange={(e, checked) => - checked - ? store.addContextLimit(groupName, key) - : store.rmContextLimit(groupName, key)}/>} - label={key} - value={context[key]} - attention={attention[key]} - curAtt={curAtt[key]}/> - ))} - {displayCnt < Object.keys(context).length && - - } - - -} - -const Container = styled('div')(({theme}) => ({ - backgroundColor: theme.palette.background.default, - borderRadius: theme.shape.borderRadius, - transition: 'all .3s ease', - display: 'block', - padding: theme.spacing(1), - textAlign: 'center', - width: '100%', - overflow: 'hidden', - '&:hover': { - backgroundColor: darken(theme.palette.background.default, .02), - }, - '& ~ &': { - marginTop: theme.spacing(1), - } -})) - -export default inject('store')(observer(ContextGroup)); \ No newline at end of file diff --git a/src/views/ContextView/index.jsx b/src/views/ContextView/index.jsx deleted file mode 100644 index 20b6ab8..0000000 --- a/src/views/ContextView/index.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import {inject, observer} from "mobx-react"; -import ContextGroup from "./ContextGroup.jsx"; -import {styled} from "@mui/material/styles"; -import {playerColors, teamNames, teamShapes} from "../../utils/game.js"; -import {contextFactory} from "../../utils/fakeData.js"; -import {Box} from "@mui/material"; -import {useTranslation} from "react-i18next"; - -/** - * - * @param {import('src/store/store.js').Store} store - * @return {JSX.Element} - * @constructor - */ -function ContextView({store}) { - const strat = store.selectedPredictorsAsAStrategy; - const attention = strat ? strat.attention : contextFactory(() => ({})); - const pred = store.predictions[store.viewedPrediction]; - const predAtt = pred ? pred.attention : contextFactory(() => undefined); - const {t} = useTranslation(); - - return - {[0, 1].map(teamId => [0, 1, 2, 3, 4].map(playerId => ( - } - groupName={store.playerNames[teamId][playerId] || `${teamNames[teamId]} Pos ${playerId + 1}`} - context={store.curContext[`p${teamId}${playerId}`]} - attention={attention[`p${teamId}${playerId}`]} - curAtt={predAtt[`p${teamId}${playerId}`]}/> - )))} - - -} - -export default inject('store')(observer(ContextView)); - -const PlayerIcon = styled('div')(({theme, shape, color, selected, lifeState}) => ({ - width: 20, - height: 20, - backgroundColor: color, - marginRight: theme.spacing(1), - cursor: 'pointer', - borderRadius: shape === 'circle' ? '50%' : theme.shape.borderRadius, - border: 'none', - boxShadow: 'none', - transition: 'border .3s ease; box-shadow .3s ease', - ...(selected && { - border: '3px solid black', - boxShadow: '0 0 5px 0 rgba(0,0,0,.25)' - }), - ...(!lifeState && { - opacity: 0.1, - }) -})) \ No newline at end of file diff --git a/src/views/MapView/MapRenderer.jsx b/src/views/MapView/MapRenderer.jsx deleted file mode 100644 index 30c29ec..0000000 --- a/src/views/MapView/MapRenderer.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import {inject, observer} from "mobx-react"; -import {Layer, Stage} from "react-konva"; -import useMapNavigation from "./useMapNavigation.js"; -import KonvaImage from "../../components/KonvaImage.jsx"; -import PlayerLayer from "./PlayerLayer.jsx"; -import RealTrajectoryLayer from "./RealTrajectoryLayer.jsx"; -import StrategyRenderer from "./StrategyRenderer.jsx"; -import {useRef} from "react"; -import PredictedTrajectoryLayer from "./PredictedTrajectoryLayer.jsx"; - -/** - * @param {import('src/store/store.js').Store} store - * @param {number} size - * @constructor - */ -function MapRenderer({ - store, - size, - }) { - const ref = useRef(null); - const {scale, autoFocus, handleWheel, handleRecover, dragBoundFunc} = useMapNavigation(size, ref); - const scaleBalance = 2 / (scale + 1); - - return ref.current = n} - onWheel={handleWheel} - onDblClick={handleRecover} - fill={'black'} - draggable - dragBoundFunc={dragBoundFunc}> - - - - {store.selectedPredictorsAsAStrategy && - } - {store.viewedPrediction !== -1 && - } - {store.focusedPlayer !== -1 && - } - - -} - -export default inject('store')(observer(MapRenderer)) diff --git a/src/views/MapView/PlayerLayer.jsx b/src/views/MapView/PlayerLayer.jsx deleted file mode 100644 index 26c8f0f..0000000 --- a/src/views/MapView/PlayerLayer.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import {inject, observer} from "mobx-react"; -import {Circle, Layer, Rect} from "react-konva"; -import {mapProject, playerColors, teamShapes} from "../../utils/game.js"; - -/** - * @param {import('src/store/store').Store} store - * @param {number} mapSize - * @param {number} scaleBalance - * @returns {JSX.Element} - * @constructor - */ -function PlayerLayer({store, mapSize, scaleBalance}) { - const playerLifeStates = store.playerLifeStates; - const playerPositions = store.playerPositions; - const modelSize = mapSize * 0.007; - const iconSize = Math.max(mapSize * 0.02 * scaleBalance, modelSize); - return - {[0, 1].map(teamIdx => - [0, 1, 2, 3, 4].map(playerIdx => ( - playerLifeStates[teamIdx][playerIdx] && - - )) - )} - -} - -export default inject('store')(observer(PlayerLayer)) - - -function PlayerIcon({teamIdx, playerIdx, selected, pos, size}) { - const style = { - fill: playerColors[teamIdx][playerIdx], - ...(selected && { - stroke: 'black', - strokeWidth: size * 0.1, - shadowColor: 'black', - shadowBlur: 5, - shadowOffsetX: 0, - shadowOffsetY: 0, - shadowOpacity: 1, - }) - } - if (teamShapes[teamIdx] === 'circle') - return - else - return -} diff --git a/src/views/MapView/PredictedTrajectoryLayer.jsx b/src/views/MapView/PredictedTrajectoryLayer.jsx deleted file mode 100644 index 33c14ba..0000000 --- a/src/views/MapView/PredictedTrajectoryLayer.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import {Arrow, Layer} from "react-konva"; -import {mapProject} from "../../utils/game.js"; - -function PredictedTrajectoryLayer({mapSize, scaleBalance, prediction, color}) { - const traj = prediction.map(p => mapProject(p, mapSize)); - return - - -} - -export default PredictedTrajectoryLayer; diff --git a/src/views/MapView/RealTrajectoryLayer.jsx b/src/views/MapView/RealTrajectoryLayer.jsx deleted file mode 100644 index 4d58bc3..0000000 --- a/src/views/MapView/RealTrajectoryLayer.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import {inject, observer} from "mobx-react"; -import {Layer, Line, Arrow} from "react-konva"; -import {mapProject, playerColors, teamShapes} from "../../utils/game.js"; - -/** - * @param {import('src/store/store').Store} store - * @param {number} mapSize - * @param {number} scaleBalance - * @returns {JSX.Element} - * @constructor - */ -function RealTrajectoryLayer({store, mapSize, scaleBalance}) { - // 0. I have added some example files with some comments. - // You can find the file list in README.md. - - // 1. You can get the trajectory of the selected player from store here. - // You can refer to store.playerPositions, where I get the current positions of all players. - // But you need to get several future positions of the selected player. - // e.g., const trajectory = store.selectedPlayerTrajectory; - - //player current position - const playerPositions = store.playerPositions; - //current frame - const curfra = store.curFrame; - // selected player trajectory - const tra = store.selectedPlayerTrajectory; - const transformedTrajectory = tra.map(point => mapProject(point, mapSize)); - const points = transformedTrajectory.flatMap(([x, y]) => [Math.max(x * 0.02 * scaleBalance,x), Math.max(y * 0.02 * scaleBalance,y)]); - - // console.log(points); - // 2. You can render the trajectory here. - // You may want to refer to: - // - how to draw a simple shape: https://konvajs.org/docs/react/Shapes.html and - // - the API documentation of an arrow shape: https://konvajs.org/api/Konva.Arrow.html - - var alltra = store.allPlayerTrajectory; - return - - -} - - - -export default inject('store')(observer(RealTrajectoryLayer)) diff --git a/src/views/MapView/Timeline.jsx b/src/views/MapView/Timeline.jsx deleted file mode 100644 index 8fe38c1..0000000 --- a/src/views/MapView/Timeline.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import {inject, observer} from "mobx-react"; -import {Slider, Typography} from "@mui/material"; -import {styled} from "@mui/material/styles"; -import {viewSize} from "../../utils/layout.js"; - -/** - * @param {import('src/store/store').Store} store - * @constructor - */ -function Timeline({ - store, - }) { - return - {formatTime(store.curTime)} - store.setFrame(e.target.value - 1)} - valueLabelDisplay={'auto'}/> - -} - -export default inject('store')(observer(Timeline)) - -const Root = styled('div')(({theme}) => ({ - height: viewSize.timelineHeight, - padding: theme.spacing(1), -})) - -function formatTime(t) { - const sign = t < 0 ? '-' : ''; - t = Math.abs(t); - const h = Math.floor(t / 3600) - t -= h * 3600; - const m = Math.floor(t / 60) - t -= m * 60; - const s = Math.floor(t); - - const str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; - if (h === 0) return `${sign}${str}`; - else return `${sign}${h}:${str}`; -} diff --git a/src/views/StrategyView/MapRenderer/MapSliceRenderer.jsx b/src/views/StrategyView/MapRenderer/MapSliceRenderer.jsx deleted file mode 100644 index f4a3a3d..0000000 --- a/src/views/StrategyView/MapRenderer/MapSliceRenderer.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import {Layer, Stage} from "react-konva"; -import {styled} from "@mui/material/styles"; -import {useRef} from "react"; -import useSize from "../../../utils/useSize.js"; -import KonvaImage from "../../../components/KonvaImage.jsx"; -import useFocusMapArea from "./useFocusMapArea.js"; - -/** - * - * @param {[number, number]} centerPos - * @param {number} radius - * @param {Function} children - * @return {JSX.Element} - * @constructor - */ -function MapSliceRenderer({centerPos, radius, children}) { - const containerRef = useRef(null); - const {width, height} = useSize(containerRef); - const size = Math.min(width, height); - const {scale, offset} = useFocusMapArea(centerPos, radius, size); - - return - - - - - - {children && children(size, scale)} - - - -} - -const Container = styled('div')({ - width: '100%', - height: '100%', -}) - -export default MapSliceRenderer; \ No newline at end of file diff --git a/src/views/StrategyView/MapRenderer/PredictionMapRenderer.jsx b/src/views/StrategyView/MapRenderer/PredictionMapRenderer.jsx deleted file mode 100644 index 819ee31..0000000 --- a/src/views/StrategyView/MapRenderer/PredictionMapRenderer.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import MapSliceRenderer from "./MapSliceRenderer.jsx"; -import {Arrow, Group} from "react-konva"; -import {mapDis, mapProject} from "../../../utils/game.js"; - -/** - * @param {import('src/model/Strategy.js').Strategy} strat - * @param {import('src/model/Strategy.js').Prediction} pred - * @return {JSX.Element} - * @constructor - */ -function PredictionMapRenderer({strat, pred}) { - const centerPos = pred.trajectory[0]; - const radius = Math.max(...strat.predictors.map(p => Math.max(...p.trajectory.map(pos => mapDis(pos, centerPos))))) - return - {(mapSize, scale) => { - const traj = pred.trajectory.map(p => mapProject(p, mapSize)); - return - - - }} - -} - -export default PredictionMapRenderer; \ No newline at end of file diff --git a/src/views/StrategyView/MapRenderer/StrategyMapRenderer.jsx b/src/views/StrategyView/MapRenderer/StrategyMapRenderer.jsx deleted file mode 100644 index a6075e9..0000000 --- a/src/views/StrategyView/MapRenderer/StrategyMapRenderer.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import MapSliceRenderer from "./MapSliceRenderer.jsx"; -import {mapDis, mapProject} from "../../../utils/game.js"; -import KonvaImage from "../../../components/KonvaImage.jsx"; -import useHeatmap from "./useHeatmap.js"; - -/** - * @param {import('src/model/Strategy.js').Strategy} strat - * @return {JSX.Element} - * @constructor - */ -function StrategyMapRenderer({strat}) { - const centerPos = strat.predictors[0].trajectory[0]; - const radius = Math.max(...strat.predictors.map(p => - Math.max(...p.trajectory.map(pos => - mapDis(pos, centerPos) - )) - )) - const heatmap = useHeatmap(500, strat.predictors.map(p => { - const pos = mapProject(p.trajectory[p.trajectory.length - 1], 500); - return { - x: pos[0], - y: pos[1], - value: Math.floor(p.probability * 200), - } - }), { - radius: 20, - blur: 20, - }); - - return - {(mapSize) => } - -} - -export default StrategyMapRenderer; \ No newline at end of file diff --git a/src/views/StrategyView/MapRenderer/useFocusMapArea.js b/src/views/StrategyView/MapRenderer/useFocusMapArea.js deleted file mode 100644 index 2d56842..0000000 --- a/src/views/StrategyView/MapRenderer/useFocusMapArea.js +++ /dev/null @@ -1,20 +0,0 @@ -import {MAX_Y, MIN_X, MIN_Y} from "../../../utils/game.js"; - -const WH = MAX_Y - MIN_Y; -/** - * - * @param {[number, number]} centerPos - * @param {number} radius - * @param {number} mapSize - * @return {{scale: number, offset: [number, number]}} - */ -export default function useFocusMapArea(centerPos, radius, mapSize) { - const scale = WH / 2 / radius; - return { - scale, - offset: [ - mapSize * scale * (radius + MIN_X - centerPos[0]) / WH, - mapSize * scale * (centerPos[1] - MIN_X + radius - WH) / WH, - ], - } -} diff --git a/src/views/StrategyView/PredictionItem.jsx b/src/views/StrategyView/PredictionItem.jsx deleted file mode 100644 index 5942e03..0000000 --- a/src/views/StrategyView/PredictionItem.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import {inject, observer} from "mobx-react"; -import VisualItem from "./VisualItem.jsx"; -import {useCallback} from "react"; -import PredictionMapRenderer from "./MapRenderer/PredictionMapRenderer.jsx"; - -/** - * - * @param {number} sId - * @param {number} pId - * @param {import('src/store/store.js').Store} store - * @param {import('src/model/Strategy.d.ts').Strategy} strat - * @param {import('src/model/Strategy.d.ts').Prediction} pred - * @returns {JSX.Element} - * @constructor - */ -function PredictionItem({sId, pId, store, strat, pred}) { - const handleView = useCallback(() => store.viewPrediction(sId, pId), [sId, pId]); - - return } - prob={pred.probability}/> -} - -export default inject('store')(observer(PredictionItem)) diff --git a/src/views/StrategyView/Projection/ConvexHull.jsx b/src/views/StrategyView/Projection/ConvexHull.jsx deleted file mode 100644 index d8358ae..0000000 --- a/src/views/StrategyView/Projection/ConvexHull.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import {memo, useMemo} from "react"; -import hull from "hull.js"; -import LassoGroup from "./Group.js"; - -const W = 1000, H = 1000; - -function sameArray(a1, a2) { - return a1.length === a2.length && a1.every(v => a2.includes(v)); -} - -function useConvexHull(predictorGroup, points) { - return useMemo(() => { - const samplePoints = predictorGroup - .map(i => [ - points[i][0] * W, - points[i][1] * H, - points[i][2] * W / 20 - ]) - .map(([x, y, r]) => { - return new Array(360).fill(0) - .map((_, deg) => [ - x + r * Math.cos(deg / 180 * Math.PI) * 1.5, - y + r * Math.sin(deg / 180 * Math.PI) * 1.5, - ]) - }) - .flat() - const hullPoints = hull(samplePoints, 1000); - return 'M' + hullPoints.map(p => p.join(' ')).join('L'); - }, [predictorGroup, points]); -} - -function ConvexHull({predictorGroup, points, selected, onSelectGroup}) { - const convexHull = useConvexHull(predictorGroup, points); - return onSelectGroup(predictorGroup)}/> -} - -export default memo(ConvexHull); \ No newline at end of file diff --git a/src/views/StrategyView/Projection/Group.js b/src/views/StrategyView/Projection/Group.js deleted file mode 100644 index 12a8aa3..0000000 --- a/src/views/StrategyView/Projection/Group.js +++ /dev/null @@ -1,27 +0,0 @@ -import {styled} from "@mui/material/styles"; -import {alpha} from "@mui/material"; - -const LassoGroup = styled('path', { - shouldForwardProp: propName => !['width', 'selectable', 'selected'].includes(propName), -})(({theme, width, selectable, selected}) => ({ - stroke: theme.palette.success.dark, - fill: alpha(theme.palette.success.main, 0.1), - ...(selectable - ? { - strokeWidth: 0, - cursor: 'pointer', - '&:hover': { - strokeWidth: width, - fill: alpha(theme.palette.success.main, 0.2), - }, - ...(selected && { - strokeWidth: width, - }) - } - : { - strokeWidth: width, - pointerEvents: 'none', - }), -})) - -export default LassoGroup; \ No newline at end of file diff --git a/src/views/StrategyView/Projection/index.jsx b/src/views/StrategyView/Projection/index.jsx deleted file mode 100644 index eab2a6b..0000000 --- a/src/views/StrategyView/Projection/index.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import useProjection from "./useProjection.js"; -import usePointLassoSelection from "./usePointLassoSelection.js"; -import useLasso from "./useLasso.js"; -import {styled, useTheme} from "@mui/material/styles"; -import {lighten} from "@mui/material"; -import {useCallback} from "react"; -import ConvexHull from "./ConvexHull.jsx"; -import LassoGroup from "./Group.js"; - -const W = 1000, H = 1000; - -/** - * - * @param {import('src/model/Strategy.js').Prediction[]} allPredictors - * @param {number[][]} predictorGroups - * @param {number[]} selectedPredictors - * @param {(predIds: number[]) => void} onSelectGroup - * @param {(predId: number) => void} onViewPredictor - * @constructor - */ -function PredictorsProjection({allPredictors, predictorGroups, selectedPredictors, onSelectGroup, onViewPredictor}) { - const points = useProjection(allPredictors, predictorGroups); - const {lasso, isDrawing, handleMouseDown, handleMouseUp, handleMouseMove} = useLasso(); - const preSelectedPointsIdx = usePointLassoSelection(points, lasso); - const handleClear = useCallback(e => { - e.preventDefault(); - e.stopPropagation(); - onSelectGroup([]); - }, []); - - const theme = useTheme(); - return { - if (isDrawing) onSelectGroup(preSelectedPointsIdx); - handleMouseUp(e); - }}> - - {predictorGroups.map((g, gId) => ( - - ))} - - - {allPredictors.map((p, pId) => { - const opacity = Math.min(p.probability * 5, 1); - return - onViewPredictor(pId)} - onMouseLeave={() => onViewPredictor(-1)}/> - {isDrawing - ? - : {pId + 1}} - - })} - - {isDrawing && `${p[0] * W} ${p[1] * H}`).join('L')} - width={W / 200}/>} - -} - -export default PredictorsProjection; -const Point = styled('circle', { - shouldForwardProp: propName => !['selected', 'isDrawing'].includes(propName) -})(({theme, selected, isDrawing}) => ({ - fill: theme.palette.primary.main, - ...(isDrawing && { - pointerEvents: 'none', - }), - ...(!isDrawing && { - '&:hover': { - stroke: theme.palette.secondary.main, - strokeWidth: W / 200, - } - }), - ...(selected && { - stroke: theme.palette.success.main, - strokeWidth: W / 200, - }) -})) -const PointIdx = styled('text')({ - textAnchor: 'middle', - dominantBaseline: 'central', - fontSize: W / 20, - pointerEvents: 'none', -}) -const PointAnchor = styled('circle', { - shouldForwardProp: propName => !['preSelected'].includes(propName) -})(({theme, preSelected}) => ({ - r: W / 100, - fill: preSelected ? theme.palette.success.main : theme.palette.primary.main, -})) \ No newline at end of file diff --git a/src/views/StrategyView/Projection/useProjection.js b/src/views/StrategyView/Projection/useProjection.js deleted file mode 100644 index 5d69a8b..0000000 --- a/src/views/StrategyView/Projection/useProjection.js +++ /dev/null @@ -1,18 +0,0 @@ -import {useMemo} from "react"; - -export default function useProjection(predictors, groups) { - return useMemo(() => { - const pos = []; - const sectionRad = Math.PI * 2 / groups.length; - groups.forEach((g, i)=> { - const rad = (i + Math.random()) * sectionRad, r = Math.random() * 0.15 + 0.25; - const cx = 0.5 + Math.cos(rad) * r, cy = 0.5 + Math.sin(rad) * r; - g.forEach(i => pos[i] = [ - (Math.random() * 2 - 1) * 0.15 + cx, - (Math.random() * 2 - 1) * 0.15 + cy, - 1, - ]) - }) - return pos; - }, [predictors]); -} \ No newline at end of file diff --git a/src/views/StrategyView/StrategyItem.jsx b/src/views/StrategyView/StrategyItem.jsx deleted file mode 100644 index 61eee5c..0000000 --- a/src/views/StrategyView/StrategyItem.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import {inject, observer} from "mobx-react"; -import {Collapse, List} from "@mui/material"; -import {Fragment, useCallback} from "react"; -import PredictionItem from "./PredictionItem.jsx"; -import VisualItem from "./VisualItem.jsx"; -import StrategyMapRenderer from "./MapRenderer/StrategyMapRenderer.jsx"; -import {useTranslation} from "react-i18next"; - -/** - * - * @param {number} sId the index of the strategy - * @param {import('src/store/store.js').Store} store - * @param {import('src/model/Strategy.js').Strategy | null} strat - * @returns {JSX.Element} - * @constructor - */ -function StrategyItem({sId, store, strat = null}) { - const open = sId === store.expandedStrategy; - const handleView = useCallback(() => store.viewStrategy(sId), [sId]); - const handleOpen = useCallback(() => store.expandStrategy(sId), [sId]); - const {t} = useTranslation() - if (strat === null) strat = store.strategies[sId]; - - return - } - prob={strat.predictors.reduce((p, c) => p + c.probability, 0)}/> - - - {strat.predictors.map((pred, pId) => ( - - ))} - - - -} - -export default inject('store')(observer(StrategyItem)) \ No newline at end of file diff --git a/src/views/StrategyView/VisualItem.jsx b/src/views/StrategyView/VisualItem.jsx deleted file mode 100644 index 0373e0a..0000000 --- a/src/views/StrategyView/VisualItem.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import {Button, ListItem, ListItemSecondaryAction, Typography} from "@mui/material"; -import {ExpandLess, ExpandMore} from "@mui/icons-material"; -import {styled} from "@mui/material/styles"; -import {useCallback, useRef} from "react"; -import useHover from "../../utils/useHover.js"; - -/** - * @param {string} name - * @param {'small' | 'large'} size - * @param {boolean} selected - * @param {Function} onSelect - * @param {boolean} expanded - * @param {string} expandLabel - * @param {Function | undefined} onExpand - * @param {JSX.Element} mapSlice - * @param {number} prob - * @returns {JSX.Element} - * @constructor - */ - -const sizes = { - small: 130, - large: 150, -} - -function VisualItem({ - name, - size, - shallow = false, - selected, onSelect, - expanded = false, expandLabel, onExpand, - mapSlice, prob - }) { - const handleExpand = useCallback(e => { - e.preventDefault(); - e.stopPropagation(); - onExpand && onExpand(); - }, [onExpand]); - const buttonRef = useRef(null); - const hover = useHover(buttonRef); - const indent = (sizes["large"] - sizes[size]) / 2; - const formatProb = `${(prob * 100).toFixed(2)}%`; - const visualProb = `${(prob * 100 * 2).toFixed(2)}%`; - - return - {name} - - {mapSlice} - - - {formatProb} - - {formatProb} - - 50% - - {onExpand && - - } - -} - -const Title = styled(Typography, {shouldForwardProp: propName => propName !== 'lift'}) -(({lift}) => ({ - position: 'absolute', - left: 0, - width: 65, - textAlign: 'center', - pointerEvents: 'none', - ...(lift ? { - transform: 'translateY(-16px)', - } : { - transform: 'translateX(15px)' - }) -})) -const SecondaryAction = styled(ListItemSecondaryAction)({ - left: 0, - width: 65, - textAlign: 'center', - top: 'calc(50% + 16px)' -}) -const MapSlice = styled('div', {shouldForwardProp: propName => propName !== 'size'}) -(({theme, size}) => ({ - marginLeft: theme.spacing(1), - width: size - parseInt(theme.spacing(2)), - flex: '0 0 auto', - height: size - parseInt(theme.spacing(2)), - border: `1px solid ${theme.palette.divider}`, -})) -const RightArea = styled('div')(({theme}) => ({ - position: 'relative', - backgroundColor: theme.palette.background.default, - color: theme.palette.getContrastText(theme.palette.background.default), - marginLeft: theme.spacing(1), - flex: 1, - height: 30, - lineHeight: '30px', - paddingLeft: theme.spacing(1), - overflow: 'hidden', -})) -const Max = styled('div')(({theme}) => ({ - position: 'absolute', - top: 0, - right: theme.spacing(1), -})) -const Bar = styled('div')(({theme}) => ({ - position: 'absolute', - top: 0, - left: 0, - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - height: '100%', - overflow: 'hidden', - paddingLeft: 'inherit', -})) - -export default VisualItem; \ No newline at end of file diff --git a/src/views/TitleBar/index.jsx b/src/views/TitleBar/index.jsx deleted file mode 100644 index 0f18429..0000000 --- a/src/views/TitleBar/index.jsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * - * @param {number} width - * @constructor - */ -import {Button, Typography} from "@mui/material"; -import {styled} from "@mui/material/styles"; -import {viewSize} from "../../utils/layout.js"; -import {inject, observer} from "mobx-react"; -import {readJSONFile, selectFile} from "../../utils/file.js"; -import {useTranslation} from "react-i18next"; -import {Settings, Translate} from "@mui/icons-material"; - -/** - * @param {number} width - * @param {import('src/store/store').Store} store - * @returns {JSX.Element} - * @constructor - */ -function TitleBar({width, store}) { - const {t, i18n} = useTranslation(); - const toggleLanguage = () => i18n.changeLanguage(i18n.language === 'en' ? 'cn' : 'en'); - const handleImport = () => { - selectFile() - .then(file => { - store.setWaiting(true); - return readJSONFile(file); - }) - .catch(err => window.alert(err)) - .then(data => store.setData(data.filename, data.data)) - .finally(() => store.setWaiting(false)); - } - return - {t('System.SystemName')} -
- - - - -} - -export default inject('store')(observer(TitleBar)); - -const Bar = styled('div')(({theme}) => ({ - position: 'absolute', - left: 0, - top: 0, - height: viewSize.appTitleBarHeight, - backgroundColor: theme.palette.primary.main, - borderBottomRightRadius: theme.shape.borderRadius, - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - paddingLeft: theme.spacing(1), -})) - -const Title = styled(Typography)(({theme}) => ({ - color: theme.palette.primary.contrastText, -}))