Skip to content

Instantly share code, notes, and snippets.

@jjcf89
Last active December 17, 2021 16:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jjcf89/5b677ef6cd3a9d1ca706cc31f9bcf1cf to your computer and use it in GitHub Desktop.
Save jjcf89/5b677ef6cd3a9d1ca706cc31f9bcf1cf to your computer and use it in GitHub Desktop.
Javascript to draw lines in Yucata's Oracle of Delphi
// 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);
@henningit
Copy link

Future Ideas:

  • Detect when a player teleports and add an icon/drawing

You could add your code to the following original function:

y$.game.getAnimateObjectMoveToCoordFunction = function($object, targetCoord, offX, offY, scale, easing, duration) {
	return function() {
		var d = $.Deferred();
		if (!$object) {
			return d.resolve().promise();
		}
		var row = y$.game.Row(targetCoord);
		var isEvenRow = row % 2 === 0;
		$object.animate({transform: 'translate(' +
			(y$.game.hexColumnToPixelColumn(y$.game.Column(targetCoord), isEvenRow) + offX) + ',' +
			(y$.game.hexRowToPixelRow(row, isEvenRow) + offY) + ')' +
			(scale === 1 ? '' : ' scale(' + scale + ')')
		}, duration, easing, function() { d.resolve(); });
		return d.promise();
	};
}

In it you could check action.ActionId like this:

switch (action.ActionId) {
	case y$.game.Actions.SelectShipDestination:
		// Normal ship movement
		break;
	case y$.game.Actions.PoseidonSelectTargetField:
		// ship jumps via Poseidon
		break;
}

@jjcf89
Copy link
Author

jjcf89 commented Jul 23, 2020

@henningit Any ideas of what a warp icon could look like? Something simple to draw or taken from the Unicode character set perhaps...

@jjcf89
Copy link
Author

jjcf89 commented Jul 23, 2020

@jjcf89
Copy link
Author

jjcf89 commented Jul 23, 2020

@henningit How did you get the pre minified js version?

All I see is which is difficult to decipher.

        getAnimateObjectMoveToCoordFunction: function(n, t, i, r, u, f, e) {
            return function() {
                var o = $.Deferred(), s, h;
                return n ? (s = y$.game.Row(t),
                h = s % 2 == 0,
                n.animate({
                    transform: "translate(" + (y$.game.hexColumnToPixelColumn(y$.game.Column(t), h) + i) + "," + (y$.game.hexRowToPixelRow(s, h) + r) + ")" + (u === 1 ? "" : " scale(" + u + ")")
                }, e, f, function() {
                    o.resolve()
                }),
                o.promise()) : o.resolve().promise()
            }
        },

Also action doesn't appear to be a global, nor is it declared in getAnimateObjectMoveToCoordFunction that you mentioned.

Perhaps I can hook into y$.game.animate and make a global copy of action.ActionId

@jjcf89
Copy link
Author

jjcf89 commented Jul 23, 2020

Tried testing with the milkyway emoji but looks like it's not going to be so easy. Maybe I need to give up on doing all this work in translate...
https://emojipedia.org/milky-way/

image

@henningit
Copy link

@henningit How did you get the pre minified js version?

That's not possible without the complete yucata sources, compiling the solution and running it locally.

Also action doesn't appear to be a global, nor is it declared in getAnimateObjectMoveToCoordFunction that you mentioned.

Perhaps I can hook into y$.game.animate and make a global copy of action.ActionId

Uups.
Then you'd need to read it in y$.game.animate already. It's the second parameter of this function (which then calls function y$.game.getAnimateObjectMoveToCoordFunction).

@jjcf89
Copy link
Author

jjcf89 commented Jul 24, 2020

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

@jjcf89
Copy link
Author

jjcf89 commented Jul 24, 2020

image

@jjcf89
Copy link
Author

jjcf89 commented Jul 24, 2020

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

@henningit
Copy link

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

@jjcf89
Copy link
Author

jjcf89 commented Jul 25, 2020

What is action.ActionEffects? Is this the list of each move required to get to a specific tile?

@henningit
Copy link

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.

@henningit
Copy link

henningit commented Jul 26, 2020

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).

@jjcf89
Copy link
Author

jjcf89 commented Jul 26, 2020

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?

@henningit
Copy link

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.

@jjcf89
Copy link
Author

jjcf89 commented Jul 27, 2020

Thanks. So just returning false isn't quick enough, good to know.

@jjcf89
Copy link
Author

jjcf89 commented Aug 2, 2020

Updated to use animate function

@jjcf89
Copy link
Author

jjcf89 commented Aug 6, 2020

image

@jjcf89
Copy link
Author

jjcf89 commented Aug 7, 2020

image

@jjcf89
Copy link
Author

jjcf89 commented Aug 7, 2020

image

@jjcf89
Copy link
Author

jjcf89 commented Aug 16, 2020

Added arrows for movements, not sure if its too busy though.
image

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

@jjcf89
Copy link
Author

jjcf89 commented Aug 16, 2020

@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
Copy link

@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).

@jjcf89
Copy link
Author

jjcf89 commented Aug 22, 2020

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

image

@jjcf89
Copy link
Author

jjcf89 commented Aug 25, 2020

I created my own round counter. But it only works if someone starts from round one... eh close enough
But now there are too many numbers on the board
image

@henningit
Copy link

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

@jjcf89
Copy link
Author

jjcf89 commented Sep 5, 2020

Updated to only print round numbers at end of turn if the person moved this turn. So much less spam of numbers.

image

@jjcf89
Copy link
Author

jjcf89 commented Sep 28, 2020

image

@jjcf89
Copy link
Author

jjcf89 commented Oct 12, 2020

Added outline to each number so they are more readable
image

@jjcf89
Copy link
Author

jjcf89 commented Dec 17, 2021

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
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment