How to create a simulator and simulate real-time strategy game like Age of Empires Rise of Rome? - simulation

I would be interested to simulate an Age of Empires Rise of Rome gathering process of food, wood etc. Such simulator would help to acheive the economy growth and would find the optimized order to build buildings and gather recources. Note that the simulator would examine only economy process, not fighting. Is there any free software that would be easy to use and make such simulation? The question is how to simulate the process with simulator (not how to create real-time game).

Peladao asked for a example of a RTS engine. I am writting a example code here, and commenting it.
pseudocode:
loop:
"decide what want to build";
"try to build that"; (a verification is made if resources are enough)
"generate resources";
goto loop;
Real code (javascript)
1) not interesting. defining functions to use later.
var rand = function(){
return Math.random()*100;
};
var random = (function(){
return {
betwen:function(min,max){
return Math.random()*(max-min)+min;
},
element:function(datarray){
var len = datarray.length;
if(!len) return;
var r = parseInt(this.betwen(0,len),10);
return datarray[r];
}
};
})();
var scroll = (function(){
var index = 0;
var maximo = 10;
return {
cortarViejas:function(){
var edadCorte = index - maximo;
$(".logline").each(function(){
if($(this).data("nace")<edadCorte)
$(this).remove();
});
},
add:function(txt){
var line = $("<div>");
line.addClass("logline");
line.data("nace",index++);
line.html(txt);
$(line).insertAfter('#marca');
this.cortarViejas();
},
error:function(txt){
this.add("ERROR: "+txt);
},
battle:function(txt){
this.add("Battle: "+ txt);
},
oceaniaDice:function(txt){
this.add("[Oceania broadcast] "+ txt);
},
debugOceania:function(txt){
this.add("[Oceania debug] "+ txt);
}
};
})();
2) heres the interesting part. Oceania is a empire that generate resources, and invest these resources in tanks or soldiers
var oceania = (function(){
var tanks = 0;
var soldiers = 0;
var build = "";//build strategy
var dead = 0;
var prices = {"tank":5,"soldier":1};
var hateLines = [ "Eastasia will be destroyed","We hate Eurasia","Our troops are doing good",
"We have always ben at war with Eastasia","Under the spreading chestnut tree.I sold you and you sold me"];
var metal = 0;
var powerplants = 1;
var factory = 3;
return {
info:function(){
scroll.debugOceania("Build strategy:"+ build);
scroll.debugOceania("Metal:"+ metal);
scroll.debugOceania("Army: soldier "+soldiers + ", tanks "+tanks );
scroll.debugOceania("Dead:"+ dead);
},
militarTick:function(stuff){
if(tanks>10 && soldiers > 20){
tanks -= 10;
soldiers -= 20;
scroll.battle("Some units losts! 10 tanks, 20 soldiers");
dead += 10+ 20;
}
},
3) Here, the empire execute his desire to build [stuff]
buy:function(stuff){
if(!prices[stuff]) {
scroll.error("buying what?:"+stuff);
return;
}
if(prices[stuff]>metal) {
scroll.debugOceania(stuff + " is too expensive");
return;
}
metal -= prices[stuff];
switch(stuff){
case "tank":
tanks++;
//scroll.debugOceania("Oceania create tank");
break;
case "soldier":
soldiers++;
//scroll.debugOceania("Oceania create soldier");
break;
}
},
buildTick:function(){
switch(build){
case "tanks":
this.buy("tank");
break;
case "soldiers":
this.buy("soldier");
break;
}
},
3) Here, the empire decide what is his desire, based on tanks/soldiers ratio, ammount of soldiers, ammount of tanks, or any other strategy-level issue.
strategyTick:function(){
var likeSoldiers=0,likeTanks=0;
build = "nothing";
//You have something, build something!
if( metal> 10) //we have metal, lets buy soldiers!
likeSoldier = 10;
//minimum this
if ( tanks < 10)
likeTanks += 10;
if ( soldiers< 20)
likeSoldiers += 10;
//we want 5 soldiers for 1 tank
if ( soldiers < tanks * 5)
likeSoldiers += 10;
//if we have at least 40 soldiers, we would love to have more tanks!
if ( soldiers > 40)
likeTanks += 10;
//if we have at least 40 tanks, we would love to have moresoldiers!
if ( tanks > 40)
likeSoldiers += 10;
//we want soldiers more than anything else, lets build soldiers!
if(likeSoldiers > likeTanks ){
build = "soldiers";
}else
//we want tanks more than anything else, lets build soldiers!
if( likeTanks > 5) {
build = "tanks";
}
},
4) Everything after this line can be ignored.
produccionTick:function(){
var t;
var energy = powerplants * 3;
var factorycount = factory;
var metalInitial = metal;
for(t=0;t<energy,factorycount>0;t++,factorycount--){
metal = metal + 1;
}
//var produce = metal - metalInitial;
//scroll.debugOceania("Oceania create "+ produce + " metal. Total now:"+metal);
},
propagandaTick:function(){
if(rand()<5) {
var hate = random.element(hateLines);
scroll.oceaniaDice(hate);
}
},
tick:function(){
this.propagandaTick();
this.produccionTick();
this.strategyTick();
this.buildTick();
this.militarTick();
}
};
})();
$(function(){
$("#cmd").keyup(function(event){
if (event.which == 13) {
var command = $(this).val();
$(this).val("");
scroll.add(command);
$("#cmd").focus();
event.preventDefault();
}
});
$("#reload").click(function(){
document.location = "game.htm?r="+Math.random();
});
oceania.tick();
$(document).ready(function(){
setInterval(function(){
oceania.tick();
},300);
});
});

Related

OfflineAudioContext Latency Shift

I'm using OfflineAudioContext to download an input file with effects applied. The download works great and it's really fast, but the problem I'm running into is that when I apply gain, I'm using an analyser to signal when the gain should increase or decrease.
This works great when playing the audio using an AudioContext, but the offline version causes the timing of the gain to shift very noticeably. The increase is starting late and the decrease is starting late. It's like there's a latency shift overall.
Is there a way to combat this shift? I'm fine with the rendering process taking longer.
var chunks = [];
var fileInput = document.getElementById("input");
var process = document.getElementById("process");
//Load audio file listener
process.addEventListener(
"click",
function () {
// Web Audio
var audioCtx2 = new (AudioContext || webkitAudioContext)();
// Reset buttons and log
$("#log").empty();
$("#download_link").addClass("d-none");
$("#repeat_link").addClass("d-none");
// Check for file
if (fileInput.files[0] == undefined) {
if ($("#upload_err").hasClass("d-none")) {
$("#upload_err").removeClass("d-none");
}
return false;
}
var reader1 = new FileReader();
reader1.onload = function (ev) {
// console.log("Reader loaded.");
var tempBuffer = audioCtx2.createBufferSource();
// Decode audio
audioCtx2.decodeAudioData(ev.target.result).then(function (buffer) {
// console.log("Duration1 = " + buffer.duration);
var offlineAudioCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100 * buffer.duration,
sampleRate: 44100,
});
// console.log("test 1");
// Audio Buffer Source
var soundSource = offlineAudioCtx.createBufferSource();
var analyser2d = offlineAudioCtx.createAnalyser();
var dgate1 = offlineAudioCtx.createGain();
var dhpf = offlineAudioCtx.createBiquadFilter();
var dhum60 = offlineAudioCtx.createBiquadFilter();
var dcompressor = offlineAudioCtx.createDynamicsCompressor();
dhpf.type = "highpass";
dhpf.Q.value = 0.5;
dhum60.type = "notch";
dhum60.Q.value = 130;
dcompressor.knee.setValueAtTime(40, offlineAudioCtx.currentTime);
dcompressor.attack.setValueAtTime(0.1, offlineAudioCtx.currentTime);
dcompressor.release.setValueAtTime(0.2, offlineAudioCtx.currentTime);
var reader2 = new FileReader();
// console.log("Created Reader");
reader2.onload = function (ev) {
// console.log("Reading audio data to buffer...");
$("#log").append("<p>Buffering...</p>");
soundSource.buffer = buffer;
let context = offlineAudioCtx;
//Before Effects
analyser2d = context.createAnalyser();
analyser2d.fftSize = 2048;
analyser2d.smoothingTimeConstant = 0.85;
const sampleBuffer = new Float32Array(analyser2d.fftSize);
function loop() {
analyser2d.getFloatTimeDomainData(sampleBuffer);
let sumOfSquares = 0;
for (let i = 0; i < sampleBuffer.length; i++) {
sumOfSquares += sampleBuffer[i] ** 2;
}
const avgPowerDecibels = Math.round(
10 * Math.log10(sumOfSquares / sampleBuffer.length)
);
const gainset = avgPowerDecibels > -50 ? 1 : 0;
//real-time effects choices start
if (
document.getElementById("gate").getAttribute("data-active") ===
"true"
) {
dgate1.gain.setTargetAtTime(
gainset,
offlineAudioCtx.currentTime,
0.05
);
} else if (
document.getElementById("gate").getAttribute("data-active") ===
"false"
) {
dgate1.gain.setTargetAtTime(
1,
offlineAudioCtx.currentTime,
0.05
);
}
if (
document.getElementById("hpf").getAttribute("data-active") ===
"true"
) {
dhpf.frequency.value = 90;
} else if (
document.getElementById("hpf").getAttribute("data-active") ===
"false"
) {
dhpf.frequency.value = 0;
}
if (
document.getElementById("hum").getAttribute("data-active") ===
"true"
) {
dhum60.frequency.value = 60;
} else if (
document.getElementById("hum").getAttribute("data-active") ===
"false"
) {
dhum60.frequency.value = 0;
}
if (
document.getElementById("comp").getAttribute("data-active") ===
"true"
) {
dcompressor.threshold.setValueAtTime(
-30,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(
3.5,
offlineAudioCtx.currentTime
);
} else if (
document.getElementById("comp").getAttribute("data-active") ===
"false"
) {
dcompressor.threshold.setValueAtTime(
0,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(1, offlineAudioCtx.currentTime);
}
// Display value.
requestAnimationFrame(loop);
}
loop();
soundSource
.connect(analyser2d)
.connect(dhpf)
.connect(dhum60)
.connect(dgate1)
.connect(dcompressor);
dcompressor.connect(offlineAudioCtx.destination);
offlineAudioCtx
.startRendering()
.then(function (renderedBuffer) {
// console.log('Rendering completed successfully.');
$("#log").append("<p>Rendering new file...</p>");
//var song = offlineAudioCtx.createBufferSource();
console.log(
"OfflineAudioContext.length = " + offlineAudioCtx.length
);
split(renderedBuffer, offlineAudioCtx.length);
$("#log").append("<p>Finished!</p>");
})
.catch(function (err) {
// console.log('Rendering failed: ' + err);
$("#log").append("<p>Rendering failed.</p>");
});
soundSource.loop = false;
};
reader2.readAsArrayBuffer(fileInput.files[0]);
soundSource.start(0);
});
};
reader1.readAsArrayBuffer(fileInput.files[0]);
},
false
);
I've included what I believe is the relevant portion of the code. Let me know if more is needed. Thanks!
This is doesn't work because the OfflineAudioContext runs faster (sometimes hundreds of times faster) than realtime. Your loop that gets the analyser data is done every 16 ms or so. In a realtime system, this might be accurate enough, but the offline context could be running much, much faster than realtime so by the time you've grabbed the analyser data, much more time has passed. (You can probably see this by printing out the context currentTime in the loop. It will probably increment by more (much more) than 16 ms.
The best way to do this is to use context.suspend(t) to suspend the context at known times so you can grab the analyser data synchronously. Note that the time is rounded so it might not be exactly the time you want but perhaps close enough. Note also that AFAIK, Firefox has not implemented this, and neither has Safari (but will soon).
Here's a short snippet with how do use suspend. There's more than one way though. Untested, but I think the general idea is correct:
// Grab the data every 16 ms, roughly. `suspend` rounds the time up to
// the nearest multiple of 128 frames (or 128/context.sampleRate time).
for (t = 0; t < <length of buffer>; t += 0.016) {
context.suspend(t)
.then(() => {
// Use analyser to grab the data and compute the values.
// Use setValueAtTime and friends to adjust the gain and compressor
// appropriately. Use context.currentTime to know at what time
// the context was suspended, and schedule the modifications to be
// at least 128 samples in the future. (I think).
})
.then(() => context.resume());
}
An alternative is to create a realtime context and process it as you do now, and when the buffer is finished playing, close the context. Of course, you'll have to add something (ScriptProcessorNode, AudioWorkletNode, or MediaRecorder) to capture the rendered data.
If none of these work for you, then I'm not sure what the alternatives would be.

How do I leave the clicked point highlighted in dygraphs?

I am using the selected shapes to draw a larger diamond shape on my graph. When a user clicks a point. I display the data in another div, but I want to leave the clicked point highlighted. In other words, I want to 'toggle' data behind the points on and off and the clicked points need to show if they are included in the dataset. I believe I have seen this somewhere but I cannot find it. Is there a 'standard' way of leaving a clicked point in the 'highlight' state when you mouse away after clicking?
Here is my code. The pointClickCallback is getting the data through ajax and displaying it in another div. That part works. I just want to leave the point highlighted so I know which points I have clicked on.
I also need the point to revert back to normal when I click a second time. This is a toggle that allows me to select and unselect points.
EDIT: I found the interaction model example but when I add it to my code I lose my pointClickCallback functionality. I saw the call to captureCanvas and the interaction model structure.
var g = new Dygraph(document.getElementById('rothmangraph'), lines, {
//showRangeSelector: true,
title: "Personal Wellness Index (PWI)",
labels: ['Date', 'Index'],
color: ['#006699'],
valueRange: [0, 101],
axisLabelFontSize: 12,
drawPoints: true,
gridLineColor: "#aaaaaa",
includeZero: true,
strokeWidth: 2,
rightGap: 20,
pointSize: 4,
highlightCircleSize: 8,
series : {
Index: {
drawHighlightPointCallback : Dygraph.Circles.DIAMOND
},
},
axes: {
y: {
pixelsPerLabel: 20,
},
x: {
valueFormatter: function(ms) {
return ' ' + strftime('%m/%d/%Y %r',new Date(ms)) + ' ';
},
axisLabelWidth: 60,
axisLabelFormatter: function(d, gran) {
return strftime('%m/%d %I:%M %p',new Date(d.getTime())) ;
}
}
},
underlayCallback: function (canvas, area, g) {
var warning = g.toDomCoords(0,41);
var critical = g.toDomCoords(0,66);
// set background color
canvas.fillStyle = graphCol;
canvas.fillRect(area.x, area.y, area.w, area.h);
// critical threshold line
canvas.fillStyle = "#cc0000";
canvas.fillRect(area.x,warning[1],area.w,2);
// warning threshold line
canvas.fillStyle = "#cccc00";
canvas.fillRect(area.x,critical[1],area.w,2);
},
pointClickCallback: function(e,point) {
var idx = point.idx;
var line = lines[idx];
var sqltime = strftime('%Y-%m-%d %H:%M:%S',new Date(line[0]));
var dispdate = strftime('%m/%d %r',new Date(line[0]));
_secureAjax({
url: '/ajax/getDataPoint',
data: {'patient_id': pid, "rdate": sqltime},
success: function (result) {
// parse and add row to table if not exists.
var data = JSON.parse(result);
var aid = data['id'];
var indexCol = "#a9cced"
if (line[1] <= 65) indexCol = "#ede1b7";
if (line[1] <= 40) indexCol = "#e5bfcc";
var headerinfo = '<th class="'+aid+'"><span class="showindex" style="background-color:'+indexCol+'">'+line[1]+'</span></th>';
var fixdate = dispdate.replace(' ','<br>');
var headerdate = '<th class="'+aid+'">'+fixdate+'</th>';
// skip if already exists
var found = false;
var whichone = false;
$('#headerdate tr th').each(function(idx, item) {
if (fixdate == $(this).html()) {
found = true;
whichone = idx;
}
});
if (!found) {
$.each(data, function (idx, item) {
$('#' + idx).append('<td class="'+aid+'" style="width:70px">' + item + '</td>');
});
$('#headerdate tr').append(headerdate);
$('#headerinfo tr').append(headerinfo);
} else {
$('tr').each(function() {
$('.'+aid).remove();
});
}
}
});
}
});
}

Chartjs: display different average line while grouping

Currently I'm working on chartjs and I found that is extremely fast to learn(at least for normal task).Currently I'm facing a problem: I was asked to display a grouped bar chart. like in figure.
grouped bar char
as you can see for the date 30-08-2016 there will be 3 distinct values for B,C,D and for 31-09-2016 same group of data but with different values.
I was asked also to add an average line for each different group in the chart
to look like this:
grouped bar chart with averages
I need to bind the start of one average line with the associated bar group.
I serached on internet but i couldn't find any example.Can you tell me if there is an example or give some suggestion? thanks in advance
this was my solution: i created a plugin and than tried to draw a line for each component of the chart. the code is quite messy (i'll try to clean)
// Define a plugin to provide average for different groups of data
Chart.plugins.register({
afterDatasetsDraw: function(chartInstance, easing) {
// To only draw at the end of animation, check for easing === 1
{
var ctx = chartInstance.chart.ctx;
var mapAverageLinePoints = {};
chartInstance.data.datasets.forEach(function (dataset, i) {
var meta = chartInstance.getDatasetMeta(i);
if (!meta.hidden) {
meta.data.forEach(function(element, index) {
var dataString = dataset.label;
var groupAverageLine = mapAverageLinePoints[dataString];
if(groupAverageLine==null)
{
groupAverageLine = [];
}
//store the point coordinate and the value
var linePoint =
{
x : position.x,
y : position.y,
value: dataset.data[index],
avg : 0
}
//adding the point to the array going to be stored in the map that group the point by the label
groupAverageLine.push(linePoint);
mapAverageLinePoints[dataString]=groupAverageLine;
}
);
}
});
for (var type in mapAverageLinePoints) {
var avgLinePoints = mapAverageLinePoints[type];
//NON E' in valore bensì rispecchia la sommatoria dei posY utilizzati nella rappresentazione
var totalYAxis=0;
var totale=0;
var labelNumero=0;
for(var k=0;k<avgLinePoints.length;k++)
{
var point = avgLinePoints[k];
totalYAxis+=point.y;
totale+=point.value;
//jump the first one
if(k>=1)
{
var prevPoint = avgLinePoints[k-1];
//k start from 0!!!!
var avgYAxis = (totalYAxis/(k+1));
var avg = (totale/(k+1));
// here i draw the line starting from the previous average
ctx.beginPath();
ctx.moveTo(point.x, avgYAxis);
ctx.strokeStyle = '#979797';
ctx.lineTo((prevPoint.x), prevPoint.avg);
ctx.stroke();
point.avg=avgYAxis;
//this one is for drawing a "o" where two segments collide
var fontSize = 12;
var fontStyle = 'normal';
var fontFamily = 'Helvetica Neue';
ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.fillText("avg: "+(avg/range).toLocaleString() + rangeSuffix + ' €', point.x, point.y+(point.value>0?-30:+10));
}
else{
//for the first one only record the y as the avg
point.avg=point.y;
}
var fontSize = 10;
var fontStyle = 'normal';
var fontFamily = 'Helvetica Neue';
ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.fillText("o", point.x, point.avg);
}
labelNumero=labelNumero+1;
}
}
}
});
the result is this one:
chart result

How to remove L.rectangle(boxes[i])

I few days ago I implement a routingControl = L.Routing.control({...}) which works perfect for my needs. However I need for one of my customer also the RouteBoxer which I was also able to implement it. Now following my code I wants to remove the boxes from my map in order to draw new ones. However after 2 days trying to find a solution I've given up.
wideroad is a param that comes from a dropdown list 10,20,30 km etc.
function routeBoxer(wideroad) {
this.route = [];
this.waypoints = []; //Array for drawBoxes
this.wideroad = parseInt(wideroad); //Distance in km
this.routeArray = routingControl.getWaypoints();
for (var i=0; i<routeArray.length; i++) {
waypoints.push(routeArray[i].latLng.lng + ',' + routeArray[i].latLng.lat);
}
this.route = loadRoute(waypoints, this.drawRoute);
}; //End routeBoxer()
drawroute = function (route) {
route = new L.Polyline(L.PolylineUtil.decode(route)); // OSRM polyline decoding
boxes = L.RouteBoxer.box(route, this.wideroad);
var bounds = new L.LatLngBounds([]);
for (var i = 0; i < boxes.length; i++) {
**L.rectangle(boxes[i], {color: "#ff7800", weight: 1}).addTo(this.map);**
bounds.extend(boxes[i]);
}
console.log('drawRoute:',boxes);
this.map.fitBounds(bounds);
return route;
}; //End drawRoute()
loadRoute = function (waypoints) {
var url = '//router.project-osrm.org/route/v1/driving/';
var _this = this;
url += waypoints.join(';');
var jqxhr = $.ajax({
url: url,
data: {
overview: 'full',
steps: false,
//compression: false,
alternatives: false
},
dataType: 'json'
})
.done(function(data) {
_this.drawRoute(data.routes[0].geometry);
//console.log("loadRoute.done:",data);
})
.fail(function(data) {
//console.log("loadRoute.fail:",data);
});
}; //End loadRoute()
Well, my problem is now how to remove previously drawn boxes in order to draw new ones because of changing the wideroad using a dropdown list. Most of this code I got from the leaflet-routeboxer application.
Thanks in advance for your help...
You have to keep a reference to the rectangles so you can manipulate them (remove them) later. Note that neither Leaflet nor Leaflet-routeboxer will do this for you.
e.g.:
if (this._currentlyDisplayedRectangles) {
for (var i = 0; i < this._currentlyDisplayedRectangles.length; i++) {
this._currentlyDisplayedRectangles[i].remove();
}
} else {
this._currentlyDisplayedRectangles = [];
}
for (var i = 0; i < boxes.length; i++) {
var displayedRectangle = L.rectangle(boxes[i], {color: "#ff7800", weight: 1}).addTo(this.map);
bounds.extend(boxes[i]);
this._currentlyDisplayedRectangles.push(displayedRectangle);
}
If you don't store a reference to the L.rectangle() instance, you obviously won't be able to manipulate it later. This applies to other Leaflet layers as well - not storing explicit references to Leaflet layers is a usual pattern in Leaflet examples.

Leaflet Marker Cluster add weight to marker

I have a leaflet map and I am using the Leaflet.markerCluster plugin to cluster my markers. I have some markers that represent multiple points on the same location. Unfortunately when it gets clustered it only represents one single point. Is there a way to add a weight to each marker? So that the cluster sees it as more than one point?
Basically I am hoping for a clusterWeight property like the follwing:
var newMarker = L.marker(coordinates, {
icon: myIcon,
clusterWeight: 5
});
This propety does not exist however. Anyoneknow of a work around? Thanks!
First you will need to create a marker that supports custom properties. You can do this by extending the default L.Marker like so:
var weightMarker = L.Marker.extend({
options: {
customWeight: 0
}
});
Then you can make use of Leaflet.markercluster's iconCreateFunction to create a custom cluster marker, by changing what is displayed on the marker:
var markers = L.markerClusterGroup({
iconCreateFunction: function(cluster) {
// iterate all markers and count
var markers = cluster.getAllChildMarkers();
var weight = 0;
for (var i = 0; i < markers.length; i++) {
if(markers[i].options.hasOwnProperty("customWeight")){
weight += markers[i].options.customWeight;
}
}
var c = ' marker-cluster-';
if (weight < 10) {
c += 'small';
} else if (weight < 100) {
c += 'medium';
} else {
c += 'large';
}
// create the icon with the "weight" count, instead of marker count
return L.divIcon({
html: '<div><span>' + weight + '</span></div>',
className: 'marker-cluster' + c, iconSize: new L.Point(40, 40)
});
}
});
Demo: https://jsfiddle.net/chk1/0hq1t13t/