-
-
Save jjcf89/5b677ef6cd3a9d1ca706cc31f9bcf1cf to your computer and use it in GitHub Desktop.
// Draw paths of each ship through out the game | |
// | |
// Need to run code in console and replay the game | |
// Howto for Chrome: | |
// * Open finished game https://www.yucata.de/en/Game/Delphi/10373044#page | |
// * Click on first move in game log | |
// * Right click on page and select "Inspect" | |
// * Select the "Sources" tab and in left pane, change Page to Snippets | |
// * Create "+ New Snippet" and give it a name | |
// * Copy/paste this code into the snippet | |
// * Right click on snippet and select Run | |
// * (Optional) In Oracle window, go to General Settings tab and uncheck "show animations", this will speed up the replay | |
// * Go back to Oracle window and in the replay tab and press the "Start replay" play button | |
// * Wait till game has finished running, lines should be getting drawn behind the ships | |
// * Snippet will stay saved so you can run it again without having to recreate it | |
// | |
// Clearing lines: | |
// * You can clear the lines by running the following javascript in the console or in a different snippet | |
// ** $(".paths").remove(); $(".warp").remove() | |
// | |
// SNAP SVG Cheatsheet: https://gist.github.com/osvik/0185cb4381b35aad3d3e1f5438ca5ca4 | |
if (!y$.game.animateOld) { | |
y$.game.animateOld = y$.game.animate; | |
} | |
y$.game.animate = function(idx, action) { | |
// Override the animate function so we can draw our lines and skip the other animation effects to save time | |
var playerNum = y$.utils.getIdx(action.PID); | |
// Determine color of player | |
const playerColorIdx = y$.basegame.getColorIdx(playerNum); | |
const colorPicker = ["red", "green", "blue", "yellow"]; | |
const color = colorPicker[playerColorIdx]; | |
// Offset drawn lines so they don't overlap other ships | |
const offsetPicker = [ | |
{ x: 0, y: 20 }, // Red: Top left | |
{ x: 80, y: 10 }, // Green: Top Right | |
{ x: 0, y: 70 }, // Blue: Bot Left | |
{ x: 80, y: 80 }, // Yellow: Bot Right | |
]; | |
const offset = offsetPicker[playerColorIdx]; | |
// Select players ship | |
const ship = y$.game.snapBoard.select(".boardship" + playerNum); | |
// The total number of actions since start of game | |
const moveNum = action.MoveNr; | |
/* Track round number | |
* I can't find a round counter so lets make one | |
*/ | |
// Track the last move number so we can tell if we stopped and restarted | |
// We have to assume we restarted in the first round... | |
if (!ship.lastMove || moveNum < ship.lastMove) | |
{ | |
ship.roundNum = 1; | |
} | |
ship.lastMove = moveNum; | |
/** | |
* Draws a ships path as the ships move on the board | |
*/ | |
function addShipPath(startPosition, targetCoord, offX, offY, scale, easing, duration) { | |
var row = y$.game.Row(targetCoord); | |
var isEvenRow = row % 2 === 0; | |
// Get start position | |
const x1 = startPosition[0]; | |
const y1 = startPosition[1]; | |
// Get end position | |
const x2 = (y$.game.hexColumnToPixelColumn(y$.game.Column(targetCoord), isEvenRow) + offX); | |
const y2 = (y$.game.hexRowToPixelRow(row, isEvenRow) + offY); | |
// Ignore zero length moves | |
if (x1 != x2 || y1 != y2) { | |
switch (action.ActionId) { | |
case y$.game.Actions.PoseidonSelectTargetField: | |
// ship jumps via Poseidon | |
console.log(color + ": Warping from " + x1 + "," + y1 + " to " + x2 + "," + y2); | |
// TODO Not sure if there is some css which would center the text instead of doing it manually | |
const centerX = -20; | |
const centerY = 15; | |
y$.game.snapBoard.text(x1+offset["x"]+centerX, y1+offset["y"]+centerY, "🌌").addClass("warp"); | |
y$.game.snapBoard.text(x2+offset["x"]+centerX, y2+offset["y"]+centerY, "🌌").addClass("warp"); | |
// fall into normal move handling so we draw a line as well | |
case y$.game.Actions.SelectShipDestination: | |
// Normal ship movement - Draw line | |
console.log(color + ": Moving from " + x1 + "," + y1 + " to " + x2 + "," + y2); | |
line = y$.game.snapBoard.line(x1+offset["x"], y1+offset["y"], x2+offset["x"], y2+offset["y"]).attr({ | |
stroke: color, | |
}).addClass("paths"); | |
// Workaround: For some reason you can't add this using attr() | |
line.node.style['marker-end'] = "url(#arrow)" | |
// Track if we moved | |
ship.movedThisRound = true; | |
break; | |
} | |
} | |
// return end position, in pixels | |
return [x2, y2]; | |
}; | |
// Handle action | |
switch (action.ActionId) { | |
case y$.game.Actions.SelectShipDestination: | |
// Source from real animate function | |
// for (i = 1; action.ActionEffects.length >= 4 && i < action.ActionEffects[3].length; i++) { | |
// aSequence.push(y$.game.getAnimateObjectMoveToCoordFunction(y$.game.snapBoard.select('.boardship' + plrIdx), action.ActionEffects[3][i], 24, 39, 1, mina.easeout, 200)); | |
// } | |
// addAnimationOfOracleResourceCleanup(); | |
// Get start position, this is in pixels | |
var startPosition = [ship.matrix.e, ship.matrix.f]; | |
for (i = 1; action.ActionEffects.length >= 4 && i < action.ActionEffects[3].length; i++) { | |
startPosition = addShipPath(startPosition, action.ActionEffects[3][i], 24, 39, 1, mina.easeout, 200); | |
} | |
break; | |
case y$.game.Actions.PoseidonSelectTargetField: | |
// Source from real animate function | |
// aSequence.push(y$.game.getAnimateObjectMoveToCoordFunction(y$.game.snapBoard.select('.boardship' + plrIdx), action.ActionParams, 24, 39, 1, mina.linear, 600)); | |
// aSequence.push(y$.game.getAnimateGodStepFunction(plrIdx, y$.game.GOD.POSEIDON, false, y$.game.GODSTEPS.BOTTOM)); | |
// Get start position, this is in pixels | |
var startPosition = [ship.matrix.e, ship.matrix.f]; | |
addShipPath(startPosition, action.ActionParams, 24, 39, 1, mina.linear, 600); | |
break; | |
case y$.game.Actions.finishTurn: | |
// Write current round to ship position so we can track time | |
// but only if we've movedThisRound | |
if (ship.movedThisRound) { | |
delete ship.movedThisRound; | |
// TODO Not sure if there is some css which would center the text instead of doing it manually | |
const moveTextX = -20; | |
const moveTextY = 15; | |
y$.game.snapBoard.text(ship.matrix.e+offset["x"]+moveTextX, ship.matrix.f+offset["y"]+moveTextY, ship.roundNum).attr({ | |
stroke: color, | |
}).addClass("paths"); | |
} | |
// Next round | |
ship.roundNum++; | |
break; | |
} | |
// If set to skip animations, exit and resolve promise to skip timeout | |
if (!y$.userPreferences.getPreference('animated')) { | |
return $.Deferred().resolve().promise(); | |
} else { | |
return y$.game.animateOld(idx, action); | |
} | |
} | |
// arrowhead marker definition | |
marker = ` | |
<marker id="arrow" viewBox="0 0 10 5" refX="10" refY="2.5" | |
markerWidth="6" markerHeight="6" | |
orient="auto-start-reverse"> | |
<path d="M 0 0 L 10 2.5 L 0 5 z" /> | |
</marker>` | |
// Create element case sensitive | |
markElem = Snap.parse(marker) | |
$("#arrow").remove() | |
y$.game.snapBoard.append(markElem) | |
y$.game.snapBoard.select("#arrow").toDefs() | |
// Add css, delete the first two rules so if this is run multiple times, we only have one copy of these rules | |
// Note the first run does delete some unrelated css but at present they aren't used for anything... | |
/* 1 pixel black shadow to left, top, right and bottom */ | |
/* text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; */ | |
document.styleSheets[0].deleteRule(0); | |
document.styleSheets[0].deleteRule(1); | |
document.styleSheets[0].insertRule(".paths { stroke-width: 5px; font-size: 3em; text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; }", 0); | |
document.styleSheets[0].insertRule(".warp { font-size: 2em; }", 0); |
Thanks. Could you post the top section of the animate function? I can't figure out where r, c, v, y are coming from since they aren't passed into the function.
function() {
$.extend(y$.game, {
animate: function(n, t) {
function e() {
y$.game.isOracleDiceActionResource(y$.gameState.GameData.OracleActionResource) ? i.push(y$.game.getAnimateOracleDieToCenterFunction(r)) : y$.game.isOracleCardActionResource(y$.gameState.GameData.OracleActionResource) && i.push(y$.game.getAnimateOracleCardOutOfCircleFunction(r))
}
var u, f, o, h, i = [], r, c, v, y;
if (!y$.userPreferences.getPreference("animated"))
return !1;
r = y$.utils.getIdx(t.PID);
Alright bit rewrite of the snippet so it uses the getAnimateObjectMoveToCoordFunction call instead of the translate() function. This creates a ton fewer lines which is good. And only two warp icons.
I think I would like to still move it up one more layer and put this code in the animate function itself but would be nice to have the non-minified code first
Thanks. Could you post the top section of the animate function? I can't figure out where r, c, v, y are coming from since they aren't passed into the function.
function() { $.extend(y$.game, { animate: function(n, t) { function e() { y$.game.isOracleDiceActionResource(y$.gameState.GameData.OracleActionResource) ? i.push(y$.game.getAnimateOracleDieToCenterFunction(r)) : y$.game.isOracleCardActionResource(y$.gameState.GameData.OracleActionResource) && i.push(y$.game.getAnimateOracleCardOutOfCircleFunction(r)) } var u, f, o, h, i = [], r, c, v, y; if (!y$.userPreferences.getPreference("animated")) return !1; r = y$.utils.getIdx(t.PID);
Here you are:
y$.game.animate = function (idx, action) {
function addAnimationOfOracleResourceCleanup() {
if (y$.game.isOracleDiceActionResource(y$.gameState.GameData.OracleActionResource)) {
aSequence.push(y$.game.getAnimateOracleDieToCenterFunction(plrIdx));
} else if (y$.game.isOracleCardActionResource(y$.gameState.GameData.OracleActionResource)) {
aSequence.push(y$.game.getAnimateOracleCardOutOfCircleFunction(plrIdx));
}
}
var i;
var $set;
var aParallel, aParallelOuter;
var aSequence = [];
if (!y$.userPreferences.getPreference('animated')) { return false; }
var plrIdx = y$.utils.getIdx(action.PID);
switch (action.ActionId) {
//(...)
case y$.game.Actions.SelectShipDestination:
for (i = 1; action.ActionEffects.length >= 4 && i < action.ActionEffects[3].length; i++) {
aSequence.push(y$.game.getAnimateObjectMoveToCoordFunction(y$.game.snapBoard.select('.boardship' + plrIdx), action.ActionEffects[3][i], 24, 39, 1, mina.easeout, 200));
}
addAnimationOfOracleResourceCleanup();
break;
case y$.game.Actions.PoseidonSelectTargetField:
aSequence.push(y$.game.getAnimateObjectMoveToCoordFunction(y$.game.snapBoard.select('.boardship' + plrIdx), action.ActionParams, 24, 39, 1, mina.linear, 600));
aSequence.push(y$.game.getAnimateGodStepFunction(plrIdx, y$.game.GOD.POSEIDON, false, y$.game.GODSTEPS.BOTTOM));
break;
//(...)
}
if (aSequence.length === 0) {
return false;
} else {
return y$.animate.animateSerial(aSequence);
}
}
What is action.ActionEffects? Is this the list of each move required to get to a specific tile?
What is action.ActionEffects?
It specifies the effects of some action like consulting the oracle.
The first value in the action.ActionEffects
list is the type of the effect which can be one of the following:
public enum ActionEffects { Score, FinalScore, RoundEnd, CONSULT_ORACLE_PREGAME, CONSULT_ORACLE, CONSULT_ORACLE_GOD_ADVANCE_AND_TITAN }
The rest of the values in the action.ActionEffects
list are parameters of the effect and depend on its type, e.g. those could be some coordinates.
For your analysis I have uploaded all javascript files (animate.js
and game.js
) in the forked repo (you need to check the previous revision to see the game.js
- I was not aware that gist supports only a single file).
Wow, that's some complicated logic. Glad I don't have to understand it all :D
So my current thoughts are to override animate so I can do my line drawing even when y$.userPreferences.getPreference('animated')) is false. That way I can turn animations off and draw everything much faster.
Any issues you can forsee?
I see no issues with that. However, although setting animation to false
will make it faster, it will still take time for each historic action to execute because of a default timeout. You can avoid that with the following code (which is from my implementation of VOLT):
animate: function (idx, action) {
// Skip the default timeout for actions with no animation in order to make the whole animation faster:
if (action.ActionEffects === null || action.ActionId === y$.game.Actions.ProgramRobot ||
action.ActionId === y$.game.Actions.DecideCurrentModuleUsage || action.ActionId === y$.game.Actions.DecideRepairOrDrawModule) {
return $.Deferred().resolve().promise();
}
if (!y$.userPreferences.getPreference('animated')) { return false; }
(...)
Returning the resolved promise skips the default time used for the execution of each action.
Thanks. So just returning false isn't quick enough, good to know.
Updated to use animate function
Added arrows for movements, not sure if its too busy though.
Note: For some reason trying to create a marker using javascript threw an exception so I created the marker in HTML...
test=y$.game.snapBoard.marker(0, 0, 10, 10, 5, 5)
game_Delphi?v=-Ot0aAZtnHc0v3kTdaKaSsNhcLq-oIQkFvnYsni-zEM1:1 Uncaught DOMException: Failed to execute 'appendChild' on 'Node': The new child element contains the parent.
at v.o.marker (https://www.yucata.de/bundles/game_Delphi?v=-Ot0aAZtnHc0v3kTdaKaSsNhcLq-oIQkFvnYsni-zEM1:1:32061)
at <anonymous>:1:24
@henningit Any idea where to get the move number?
I'd like to add the current round next to the ship when "y$.game.Actions.finishTurn:" happens
@henningit Any idea where to get the move number?
I'd like to add the current round next to the ship when "y$.game.Actions.finishTurn:" happens
action.MoveNr
within function animate (idx, action)
.
@henningit hmm moveNr is much higher than I expected. It seems to be a global counter of every single action taken.
Is there a way to convert this to the current round number?
@henningit hmm moveNr is much higher than I expected. It seems to be a global counter of every single action taken.
Is there a way to convert this to the current round number?
If the game log of the sidebar has fully been rendered and is visible then you'll find elements like <div class="move move3 action4">
under the div
with id 'gameLog'. The action number is the action.MoveNr
whereas the move number is round number you're looking for. Not for every action number you will find such a move element in the DOM ("out of round" actions don't have it). But for the ship navigation actions you should find it. So I suggest you process the DOM based on the action.MoveNr
you have available.
The implementation derives the move numbers in a similar way, see the definition of var cntMoves
in the base function updateGameLog
:
updateGameLog: function () {
var newActions = y$.data.PartialActionList ? y$.data.PartialActionList : y$.data.Moves;
var currentAction;
if (newActions && newActions.length) {
var lastEntry = $('#gameLog .move .player > div').filter('.name, .action').last();
if (lastEntry.length) { // remove all 'old' gamelog entries
var lastMoveNr = lastEntry.data('yucataBaseLog').action;
for (i = newActions[0].MoveNr; i <= lastMoveNr; i++)
$('#gameLog .action' + i).remove();
}
var cntMoves = $('#gameLog .endOfTurn').length + 1;
newActions.forEach(function (action) {
cntMoves = y$.basegame.addAction2GameLog(action, cntMoves);
});
currentAction = newActions[newActions.length - 1];
}
if ((!currentAction || currentAction.EndOfTurn) && y$.data.GameInfo.PlayerOnTurnIdx >= 0) { // add a header in case a new move has just started and the game has not yet ended
y$.basegame.addMoveHeader2GameLog(y$.data.GameInfo.PlayerOnTurnIdx, { MoveNr: $('#gameLog .action').length + 1, PID: y$.data.GameInfo.PlayerOnTurn }, $('#gameLog .endOfTurn').length + 1);
$('#gameLog .player.color' + y$.basegame.getColorIdx(y$.data.GameInfo.PlayerOnTurnIdx) + ' .name').last().addClass('headerOnly');
}
$('#gameLog .mayClick.new').addClass('old').removeClass('new'); // only add click handling once
$('#gameLog .mayClick').not('.old').addClass('new');
$('#gameLog .mayClick.new').on('click', y$.basegame.gameLogClicked).css('cursor', 'pointer');
if (y$.basegame.isActive()) {
$('#gameLog .mayClick4Undo.new').css('cursor', 'pointer').prepend(y$.basegame.getImg({ basic: 'undo.png' })
.addClass('undo')).on('click.undoredo', y$.basegame.undoClicked);
$('#gameLog .mayClick4Redo.new').css('cursor', 'pointer').prepend(y$.basegame.getImg({ basic: 'redo.png' })
.addClass('redo')).on('click.undoredo', y$.basegame.redoClicked);
}
// now already handled with last IsCommitted
/* else // no undo/redo if not on turn
y$.basegame.removeClick4Undo();
*/
y$.utils.scroll2Bottom($('#gameLog'));
y$.animate.showButtons();
if ($.isFunction(y$.game.onGameLogLoaded))
y$.game.onGameLogLoaded();
}
Ran it on a 16 move game for the fun of it. From this post https://www.yucata.de/en/Forum?ForumID=4&postid=138475#138475
That's not possible without the complete yucata sources, compiling the solution and running it locally.
Uups.
Then you'd need to read it in
y$.game.animate
already. It's the second parameter of this function (which then calls functiony$.game.getAnimateObjectMoveToCoordFunction
).