/**
 * Created by Emil Riedeman on 2025-12-27.
 */

// Hex utils
const Point = (x, y) => ({x, y});

const inToOutRadius = (inRadius) => (inRadius * 2 / Math.sqrt(3));

const createHexPoints = (inRadius, rotated = false) => {
	const outRadius = inToOutRadius(inRadius);
	let points = [];
	const step = 2 * Math.PI / 6;
	let ang = rotated ? 0 : step / 2;
	for (let i = 0; i < 6; ++i) {
		points.push(Point(outRadius * Math.cos(ang), outRadius * Math.sin(ang)));
		ang += step;
	}
	return points;
};

const polyPath = (ctx, points, beginPath = true) => {
	const [{ x: startX, y: startY }] = points;
	if (beginPath) ctx.beginPath();
	ctx.moveTo(startX, startY);
	points.forEach(({ x, y }) => { ctx.lineTo(x, y); });
	if (beginPath) ctx.closePath();
	else ctx.lineTo(startX, startY);
};

const polyPathInset = (ctx, points, inset, beginPath = true) => {
	points.push(points[0]);
	points.push(points[1]);
	let insetPoints = [];
	for (let i = 1; i < points.length - 1; ++i) {
		let {x: bx, y: by} = points[i];
		let {x: ax, y: ay} = points[i-1];
		let {x: cx, y: cy} = points[i+1];
		const AB = Math.hypot(bx - ax, by - ay);
		const BC = Math.hypot(bx - cx, by - cy);
		insetPoints.push({
			x: bx - inset * ((by - ay) / AB + (cy - by) / BC),
			y: by + inset * ((bx - ax) / AB + (cx - bx) / BC),
		})
	}
	polyPath(ctx, insetPoints, beginPath);
};

const drawPoly = (ctx, origin, points, fillStyle, strokeStyle = null, lineWidth = null, inset = null) => {
	ctx.strokeStyle = strokeStyle;
	ctx.fillStyle = fillStyle;
	ctx.save();
	ctx.translate(origin.x, origin.y);
	if (inset === null)
		polyPath(ctx, points, true);
	else
		polyPathInset(ctx, points, inset, true);
	ctx.restore();
	if (lineWidth) ctx.lineWidth = lineWidth;
	if (fillStyle) ctx.fill();
	if (strokeStyle) ctx.stroke();
};

const drawCircle = (ctx, origin, radius, fillStyle, strokeStyle = null, lineWidth = null) => {
	ctx.strokeStyle = strokeStyle;
	ctx.fillStyle = fillStyle;
	ctx.beginPath();
	ctx.arc(origin.x, origin.y, radius, 0, 2 * Math.PI);
	ctx.closePath();
	if (lineWidth) ctx.lineWidth = lineWidth;
	if (fillStyle) ctx.fill();
	if (strokeStyle) ctx.stroke();
};

const hexTransform = (i, j, r) => (Point(r * (2 * j - i), Math.sqrt(3) * r * i));

// Cell
const emptyCell = {color: null, value: 0};
const neutralCell = {color: 'neutral', value: 2};
const playerCell = (color, value) => ({color, value});

const colors = {
	bg: "#efd756",

	red: "#ec3e54",
	redCell: "#ff9494",
	redCellText: "#ffffff",
	redArea: "#e8808d68",
	redAreaOutline: "#ec3e54",

	white: "#f8f8f8",
	whiteCell: "#ddffff80",
	whiteCellText: "#000000",
	whiteArea: "#ffffff58",
	whiteAreaOutline: "#ffffff",

	mix: "#ff9da7",
	mixCell: "#ffc9c9",

	neutral: "#888888",
	neutralCell: "#55555500",
	neutralCellText: "#ffffff",

	outline: "#666666",
	outlineHighlight: "#333333",
};

const getTurn = (move_nr) => (move_nr % 2 === 1 ? 'red' : 'white');

const getOpponentColor = (color) => (color === 'red' ? 'white' : 'red');

/*******************************************************************************
 * Tumbleweed is the object which manages the game visualisation.
 * This part makes sure that the board transitions can be made.
 * When the board is changed, it sends the board to all its controllers.
 ******************************************************************************/

const defaultSettings = {
	canvasId: 'canvas',
	onlyShowBoard: false,
	boardSize: 6,
};

function Tumbleweed(playerNames, initialMoves, moves, settings = {}) {
	this.settings = {...defaultSettings, ...settings};
	this.onlyShowBoard = this.settings.onlyShowBoard;

	this.playerNames = playerNames;
	this.isRunning = false;

	this.boardSize = this.settings.boardSize;
	this.gridSize = 2 * this.boardSize - 1;
	this.cellCount = 3 * this.boardSize * (this.boardSize - 1) + 1;

	this.cellRadius = 33;
	this.boardPadding = 50;
	this.boardWidthPx = 2 * this.boardPadding + 2 * (2 * this.boardSize - 1) * this.cellRadius;
	this.boardHeightPx = 2 * this.boardPadding + (3 * this.boardSize - 1) * inToOutRadius(this.cellRadius);

	this.font = "Verdana";

	this.stackFontSize = 25;

	this.coordinateLabelSize = 18;
	this.coordinateLabelDistance = 7;

	this.lineWidth = 2;
	this.areaWidth = 4;

	this.lastMove = null;

	// Build the canvas, we are working with:
	canvas = document.getElementById(this.settings.canvasId);
	canvas.width = this.boardWidthPx;
	canvas.height = this.boardHeightPx;
	canvas.style.width = (this.boardWidthPx) + "px";
	canvas.style.height = (this.boardHeightPx) + "px";

	this.moves = moves;
	[this.redFirstMove, this.whiteFirstMove] = initialMoves;

	this.registerEvents();
	this.buildMoveTable();
	this.initScores();

	// Create the board:
	this.ctx = canvas.getContext('2d');
	this.initBoard();

	this.drawBoard();
	if (this.onlyShowBoard) {
		this.setMove(this.moveCount() - 1);
	}

	this.updateSelectedRow();
}

Tumbleweed.prototype.initBoard = function() {
	this.curMove = 0;
	this.board = Array(this.gridSize);
	this.sight = {
		red: Array(this.gridSize),
		white: Array(this.gridSize),
	};
	for (let i = 0; i < this.gridSize; i++) {
		this.board[i] = Array(this.gridSize);
		this.sight.red[i] = Array(this.gridSize);
		this.sight.white[i] = Array(this.gridSize);
		for (let j = 0; j < this.gridSize; j++) {
			this.board[i][j] = this.isInBounds(i, j) ? emptyCell : null;
			this.sight.red[i][j] = this.isInBounds(i, j) ? 0 : null;
			this.sight.white[i][j] = this.isInBounds(i, j) ? 0 : null;
		}
	}

	this.board[this.boardSize - 1][this.boardSize - 1] = neutralCell;

	this.cellPoints = createHexPoints(this.cellRadius);
	this.borderPoints = createHexPoints(this.cellRadius-2*this.lineWidth)

	this.doMove(this.redFirstMove, 'red', 1);
	this.doMove(this.whiteFirstMove, 'white', 1);
	this.lastMove = null;
}

// +1 because initial move is not included in this.moves list
Tumbleweed.prototype.moveCount = function() {
	return this.moves.length + 1;
}

Tumbleweed.prototype.drawBoard = function () {
	const bgHexPoints = createHexPoints(this.boardHeightPx / 2 - 20, true);
	drawPoly(this.ctx, Point(this.boardWidthPx / 2, this.boardHeightPx / 2), bgHexPoints, colors.bg, colors.outline, 4);

	this.ctx.save();

	this.ctx.translate(this.boardPadding + this.cellRadius * this.boardSize, this.boardPadding + inToOutRadius(this.cellRadius));

	// Territorium + influence scores
	let territoriumSizes = {};
	let territoria = {};
	for (const color of ['red', 'white']) {
		const {area, size} = this.getTerritorium(color);
		territoria[color] = area;
		territoriumSizes[color] = size;
	}
	let redInfluenceScore = 0;
	let whiteInfluenceScore = 0;

	// Background + stack
	this.ctx.font = `${this.stackFontSize}px ${this.font}`;
	this.ctx.textAlign = "center";
	this.ctx.textBaseline = "middle";
	for (let i = 0; i < this.gridSize; i++) {
		for (let j = 0; j < this.gridSize; j++) {
			if (!this.isInBounds(i, j)) continue;
			let bg = null;
			const {color, value} = this.board[i][j];

			const redSight = this.sight.red[i][j];
			const whiteSight = this.sight.white[i][j];
			if (redSight <= value && whiteSight <= value && value > 0) {
				bg = color;
			} else {
				if (redSight > this.sight.white[i][j]) bg = 'red';
				if (redSight < this.sight.white[i][j]) bg = 'white';
				if (redSight === this.sight.white[i][j]) {
					if (this.sight.red[i][j] !== 0) bg = 'mix';
				}
			}

			redInfluenceScore += ((bg === 'red' && territoria['white'][i][j] !== 1) || territoria['red'][i][j] === 1);
			whiteInfluenceScore += ((bg === 'white' && territoria['red'][i][j] !== 1) || territoria['white'][i][j] === 1);

			const center = hexTransform(i, j, this.cellRadius);
			drawPoly(this.ctx, center, this.cellPoints, colors[`${bg}Cell`], null);

			if (color !== null) {
				drawCircle(this.ctx, center, this.cellRadius * 0.8, colors[color], colors.outline, this.lineWidth);
				this.ctx.fillStyle = colors[`${color}CellText`];
				this.ctx.fillText(value, center.x, center.y);
			}
		}
	}

	// Coordinates
	this.ctx.font = `${this.coordinateLabelSize}px ${this.font}`;
	this.ctx.fillStyle = colors.outline;
	this.ctx.strokeStyle = colors.outline;
	this.ctx.lineWidth = 1;

	// row coordinates
	this.ctx.textAlign = "right";
	this.ctx.textBaseline = "middle";
	const drawRowCoordinateLabel = (i, j) => {
		let {x, y} = hexTransform(i, j, this.cellRadius);
		x -= this.cellRadius + this.coordinateLabelDistance;

		this.ctx.beginPath();
		this.ctx.moveTo(x, y);
		this.ctx.lineTo(x + this.coordinateLabelDistance, y);
		this.ctx.stroke();

		const label = i + 1;
		this.ctx.fillText(label, x, y);
	};

	for (let [i, j] of this.genLeft(false)) {
		drawRowCoordinateLabel(i, j);
	}
	for (let [i, j] of this.genLeftDiag(true)) {
		drawRowCoordinateLabel(i, j);
	}

	// column coordinates
	this.ctx.textAlign = "center";
	this.ctx.textBaseline = "bottom";
	const drawColCoordinateLabel = (i, j) => {
		this.ctx.save();

		const {x, y} = hexTransform(i, j, this.cellRadius);

		this.ctx.translate(x, y);
		this.ctx.rotate(Math.PI / 6);

		const totalDist = this.cellRadius + this.coordinateLabelDistance;

		const label = String.fromCharCode('A'.charCodeAt(0) + j);
		this.ctx.fillText(label, 0, -totalDist);

		this.ctx.beginPath();
		this.ctx.moveTo(0, -totalDist);
		this.ctx.lineTo(0, -totalDist + this.coordinateLabelDistance);
		this.ctx.stroke();

		this.ctx.restore();
	};

	for (let [i, j] of this.genUp(false)) {
		drawColCoordinateLabel(i, j);
	}
	for (let [i, j] of this.genRightDiag(true)) {
		drawColCoordinateLabel(i, j);
	}

	// Outline
	for (let i = 0; i < this.gridSize; i++) {
		for (let j = 0; j < this.gridSize; j++) {
			if (!this.isInBounds(i, j)) continue;
			const center = hexTransform(i, j, this.cellRadius);
			drawPoly(this.ctx, center, this.cellPoints, null, colors.outline, this.lineWidth);
		}
	}

	// Territorium outline + highlight
	const inset = (this.areaWidth + this.lineWidth) / 2 * Math.sqrt(3) / 2 - 0.5;
	for (const color of ['red', 'white']) {
		this.ctx.save();
		this.ctx.strokeStyle = colors[`${color}AreaOutline`];
		this.ctx.fillStyle = colors[`${color}Area`];;
		this.ctx.lineWidth = this.areaWidth;
		this.ctx.beginPath();
		for (const borderPoints of this.getAreaBorders(territoria[color])) {
			polyPathInset(this.ctx, borderPoints, inset, false);
		}
		this.ctx.fill();
		this.ctx.stroke();
		this.ctx.restore();
	}

	// Last move outline
	if (this.lastMove !== null) {
		const [i, j] = this.lastMove;
		const center = hexTransform(i, j, this.cellRadius);
		drawPoly(this.ctx, center, this.cellPoints, null, colors.outlineHighlight, this.lineWidth);
	}
	this.ctx.restore();

	this.updateScores(territoriumSizes.red, redInfluenceScore, territoriumSizes.white, whiteInfluenceScore);
};

Tumbleweed.prototype.initScores = function () {
	if (this.onlyShowBoard) return;

	const scoreElement = document.getElementById('scores');
	const children = scoreElement.children;

	scoreElement.style.setProperty('--_bg', colors.bg);

	let i = 0;
	children[i].style.setProperty('--_text-color', 'white');
	children[i].style.setProperty('--_color1', colors.red);
	children[i].style.setProperty('--_color2', colors.red);
	children[i].style.setProperty('--_align', 'end');

	i++;
	children[i].style.setProperty('--_text-color', 'white');
	children[i].style.setProperty('--_color1', colors.red);
	children[i].style.setProperty('--_color2', colors.neutralCell);
	children[i].style.setProperty('--_align', 'end');

	i++;
	children[i].style.setProperty('--_text-color', 'black');
	children[i].style.setProperty('--_color1', colors.neutralCell);
	children[i].style.setProperty('--_color2', colors.neutralCell);
	children[i].style.setProperty('--_align', 'center');

	i++;
	children[i].style.setProperty('--_text-color', 'black');
	children[i].style.setProperty('--_color1', colors.white);
	children[i].style.setProperty('--_color2', colors.neutralCell);
	children[i].style.setProperty('--_align', 'start');

	i++;
	children[i].style.setProperty('--_text-color', 'black');
	children[i].style.setProperty('--_color1', colors.white);
	children[i].style.setProperty('--_color2', colors.white);
	children[i].style.setProperty('--_align', 'start');
}

Tumbleweed.prototype.updateScores = function(redScore, redInfluenceScore, whiteScore, whiteInfluenceScore) {
	if (this.onlyShowBoard) return;

	const scoreElement = document.getElementById('scores');
	const children = scoreElement.children;

	let scoresSizes = [redScore, redInfluenceScore - redScore, this.cellCount - redInfluenceScore - whiteInfluenceScore, whiteInfluenceScore - whiteScore, whiteScore];
	let scores = [redScore, redInfluenceScore, this.cellCount - redInfluenceScore - whiteInfluenceScore, whiteInfluenceScore, whiteScore];

	for (i = 0; i < 5; ++i) {
		children[i].style.setProperty('--_width', `${100 * scoresSizes[i] / this.cellCount}%`);
		children[i].firstChild.innerText = scores[i];
	}
};

Tumbleweed.prototype.getAreaBorders = function(area) {
	const snap0 = (n, eps = 5e-6) => Math.abs(n) < eps ? 0 : n;
	const key = ([x, y]) => `${snap0(x).toFixed(5)},${snap0(y).toFixed(5)}`;

	let segments = new Map();
	for (let i = 0; i < this.gridSize; i++) {
		for (let j = 0; j < this.gridSize; j++) {
			if (!this.isInBounds(i, j)) continue;
			if (area[i][j] === 0) continue;

			const {x: xFrom, y: yFrom} = hexTransform(i, j, this.cellRadius);
			for (const [pdi, pdj] of [[1, 0], [0, 1], [1, 1]]) {
				for (const [di, dj] of [[pdi, pdj], [-pdi, -pdj]]) {
					const ni = i + di, nj = j + dj;
					if (this.isInBounds(ni, nj) && area[ni][nj] === 1) continue;
					const {x: xTo, y: yTo} = hexTransform(ni, nj, this.cellRadius);
					const dx = (xTo - xFrom) / 2, dy = (yTo - yFrom) / 2;
					const px = -dy * Math.sqrt(3) / 3, py = dx * Math.sqrt(3) / 3;
					const segFromX = xFrom + dx - px, segFromY = yFrom + dy - py;
					const segToX = xFrom + dx + px, segToY = yFrom + dy + py;
					segments.set(key([segFromX, segFromY]), [segToX, segToY]);
				}
			}
		}
	}
	let result = [];
	while (segments.size > 0) {
		const start = segments.entries().next().value[1];
		let currentBorder = [];

		let cur = start;
		do {
			currentBorder.push(Point(cur[0], cur[1]));
			const prev = key(cur);
			cur = segments.get(prev);
			segments.delete(prev);
		} while (key(cur) !== key(start));

		result.push(currentBorder);
	}
	return result;
}

Tumbleweed.prototype.simulate = function (color) {
	while (true) {
		let noMoves = true;
		for (let i = 0; i < this.gridSize; i++) {
			for (let j = 0; j < this.gridSize; j++) {
				if (!this.isInBounds(i, j)) continue;
				if (this.sight[color][i][j] > this.board[i][j].value && this.sight[color][i][j] >= this.sight[getOpponentColor(color)][i][j]) {
					this.board[i][j] = playerCell(color, this.sight[color][i][j]);
					noMoves = false;
				}
			}
		}
		if (noMoves) break;
		this.updateSightNumbers();
	}
}

Tumbleweed.prototype.getTerritorium = function(color) {
	const opponentColor = getOpponentColor(color);

	const boardCopy = structuredClone(this.board);
	const sightCopy = structuredClone(this.sight);

	this.simulate(opponentColor);
	this.simulate(color);

	const result = Array.from({ length: this.gridSize }, () => new Uint8Array(this.gridSize));

	let size = 0;
	for (let i = 0; i < this.gridSize; i++) {
		for (let j = 0; j < this.gridSize; j++) {
			if (!this.isInBounds(i, j)) continue;
			if (this.board[i][j].color !== opponentColor) {
				result[i][j] = 1;
				size++;
			}
		}
	}

	this.board = boardCopy;
	this.sight = sightCopy;

	return {area: result, size};
};

Tumbleweed.prototype.isInBounds = function(i, j) {
	return 0 <= i && i < this.gridSize && 0 <= j && j < this.gridSize && Math.abs(i - j) < this.boardSize;
};

Tumbleweed.prototype.genUp = function*(skipFirst) {
	for (let j = skipFirst ? 1 : 0; j < this.boardSize; ++j) yield [0, j];
}
Tumbleweed.prototype.genRightDiag = function*(skipFirst) {
	for (let x = skipFirst ? 1 : 0; x < this.boardSize; ++x) yield [x, this.boardSize + x - 1];
}
Tumbleweed.prototype.genLeft = function*(skipFirst) {
	for (let i = skipFirst ? 1 : 0; i < this.boardSize; ++i) yield [i, 0];
}
Tumbleweed.prototype.genLeftDiag = function*(skipFirst) {
	for (let x = skipFirst ? 1 : 0; x < this.boardSize; ++x) yield [this.boardSize + x - 1, x];
}

Tumbleweed.prototype.updateSightNumbers = function() {
	// First reset sight lines
	for (let i = 0; i < this.gridSize; i++) {
		this.sight.red[i] = Array(this.gridSize);
		this.sight.white[i] = Array(this.gridSize);
		for (let j = 0; j < this.gridSize; j++) {
			this.sight.red[i][j] = this.isInBounds(i, j) ? 0 : null;
			this.sight.white[i][j] = this.isInBounds(i, j) ? 0 : null;
		}
	}

	const self = this;
	const boardSize = this.boardSize;
	const directions = [
		[1, 0, function* () {
			yield* self.genUp(false);
			yield* self.genRightDiag(true);
		}],
		[0, 1, function* () {
			yield* self.genLeft(false);
			yield* self.genLeftDiag(true);
		}],
		[1, 1, function* () {
			yield* self.genUp(false);
			yield* self.genLeft(true);
		}],
	];

	for (const [di, dj, gen] of directions) {
		for (let [i, j] of gen()) {
			let color = null;

			const updateSight = (i, j) => {
				if (color !== null) {
					this.sight[color][i][j]++;
				}

				const newColor = this.board[i][j].color;

				if (newColor === null) return;

				if (newColor === 'neutral') color = null;
				else color = newColor;
			};

			for (; this.isInBounds(i, j); i += di, j += dj) {
				updateSight(i, j);
			}

			color = null;
			for (i -= di, j -= dj; this.isInBounds(i, j); i -= di, j -= dj) {
				updateSight(i, j);
			}
		}
	}
};

Tumbleweed.prototype.doMove = function(move, color, value = null) {
	if (move === "Pass") {
		this.lastMove = null;
		return;
	}
	const j = move.charCodeAt(0) - 'A'.charCodeAt(0);
	const i = new Number(move.substring(1)) - 1;
	if (value === null) {
		value = this.sight[color][i][j];
	}
	this.board[i][j] = playerCell(color, value);
	this.updateSightNumbers();
	this.lastMove = [i, j];
};

// returns whether the move has been processed
Tumbleweed.prototype.setMove = function(move_nr) {
	if (move_nr < 0 || move_nr >= this.moveCount()) return false;
	if (move_nr == this.curMove) return true;

	while (this.curMove < move_nr) {
		++this.curMove;
		this.doMove(this.moves[this.curMove - 1], getTurn(this.curMove));
	}

	if (this.curMove > move_nr) {
		this.initBoard();
		for (let i = 0; i < move_nr; i++) this.doMove(this.moves[i], getTurn(i + 1));
			this.curMove = move_nr;
	}

	this.drawBoard();
	this.updateSelectedRow();
	return true;
};

// -----------------------------------------------------------------------------
// Event handlers:
// -----------------------------------------------------------------------------

Tumbleweed.prototype.setRunning = function(running) {
	if (this.onlyShowBoard) return;

	if (this.isRunning == running) return;

	this.isRunning = running;
	this.playPause.innerText = running ? 'Pause' : 'Play';

	let self = this, run = function() {
		if (!self.isRunning) return;

		if (self.setMove(self.curMove + 1)) {
			setTimeout(run, 500);
		} else {
			// last move has been reached.
			self.setRunning(false);
		}
	}
	if (this.isRunning) run();
}

Tumbleweed.prototype.prevMove = function() { this.setRunning(false); this.setMove(this.curMove - 1); };
Tumbleweed.prototype.nextMove = function() { this.setRunning(false); this.setMove(this.curMove + 1); };

Tumbleweed.prototype.registerEvents = function() {
	if (this.onlyShowBoard) return;

	this.playPause = document.getElementById('play-pause');

	let self = this;
	document.getElementById('first').onclick = function() { self.setRunning(false); self.setMove(0); };
	document.getElementById('last').onclick = function() { self.setRunning(false); self.setMove(self.moveCount() - 1); };
	document.getElementById('prev').onclick = function() { self.prevMove(); };
	document.getElementById('next').onclick = function() { self.nextMove(); };
	this.playPause.onclick = function() { self.setRunning(!self.isRunning); };
};


Tumbleweed.prototype.buildMoveTable = function() {
	if (this.onlyShowBoard) return;

	let C = function(tag, kids, attrs) {
		let x = document.createElement(tag);
		for (kid in (kids||[])) x.appendChild(kids[kid]);
			if (attrs) {
				if (attrs['class']) x.className = attrs['class'];
				if (attrs['text']) x.innerText = attrs['text'];
				if (attrs['colspan']) x.setAttribute('colspan', attrs['colspan']);
			}
			return x;
	};

	let self = this, nplayers = 2;
	this.moveTable = document.getElementById('move-table').children[0];

	let head = [ C('th') ];
	for (let p = 0; p < nplayers; p++) {
		// cut-off on more than 20 characters in the name
		let playerName = "Player " + (p + 1).toString();
		if (this.playerNames) {
			let name = this.playerNames[p];
			if (name.length > 15) name = name.slice(0, 12) + "...";
			playerName = name;
		}
		head.push(C('th', [], {text: playerName}));
	}

	let firstrow = C('tr', [ C('th', [], {text: '0.'}), C('td', [], {text: self.redFirstMove}), C('td', [], {text: self.whiteFirstMove}) ]);
	firstrow.onclick = function() { self.setMove(0); };
	let rows = [firstrow];

	for (let i = 0; i < self.moves.length; i++) {
		let row = [ C('th', [], {text: (i + 1) + '.'}) ], curp = i % 2;
		for (let p = 0; p < nplayers; p++) {
			if (p == curp) row.push(C('td', [], {text: self.moves[i]}));
			else row.push(C('td'));
		}
		let tr = C('tr', row, {class: i % 2 ? 'zebra' : ''});
		tr.onclick = (function(move) { return function() { self.setMove(move); } })(i + 1);
		rows.push(tr);
	}

	this.moveTableBody = C('tbody', rows);
	this.moveTable.onkeydown = function(event) {
		if (event.keyCode == 38 || event.keyCode == 37) {
			self.prevMove();
		} else if (event.keyCode == 40 || event.keyCode == 39) {
			self.nextMove();
		} else {
			return;
		}
		event.preventDefault();
		event.stopImmediatePropagation();
	};

	this.moveTable.appendChild(C('table', [C('thead', [C('tr', head)]), this.moveTableBody]));
};

Tumbleweed.prototype.updateSelectedRow = function() {
	if (this.onlyShowBoard) return;

	let lastCurMove = document.getElementById('cur-move');
	let tbl = this.moveTable;

	// reset the id.
	if (lastCurMove) lastCurMove.id = '';

	if (this.curMove >= 0) {
		let curRow = this.moveTableBody.children[this.curMove];
		curRow.id = 'cur-move';

		let topCoord = curRow.offsetTop;
		let bottomCoord = curRow.offsetTop + curRow.offsetHeight - tbl.offsetHeight;
		if (tbl.scrollTop < bottomCoord) {
			tbl.scrollTop = bottomCoord;
		} else if (tbl.scrollTop > topCoord) {
			tbl.scrollTop = topCoord;
		}
	} else {
		tbl.scrollTop = 0;
	}
};
