mirror of https://github.com/espruino/BangleApps
302 lines
9.8 KiB
JavaScript
302 lines
9.8 KiB
JavaScript
// Using p4wn chess engine: https://p4wn.sourceforge.net/ | https://github.com/douglasbagnall/p4wn
|
|
const engine = require("chessengine");
|
|
|
|
Bangle.loadWidgets(); // load before first appRect call
|
|
|
|
const FIELD_WIDTH = Bangle.appRect.w/8;
|
|
const FIELD_HEIGHT = Bangle.appRect.h/8;
|
|
const SETTINGS_FILE = "chess.json";
|
|
const DEFAULT_TIMEOUT = Bangle.getOptions().lockTimeout;
|
|
const ICON_SIZE=45;
|
|
const ICON_BISHOP = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA="));
|
|
const ICON_PAWN = require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA="));
|
|
const ICON_KING = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA=="));
|
|
const ICON_QUEEN = require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA="));
|
|
const ICON_ROOK = require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA="));
|
|
const ICON_KNIGHT = require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA"));
|
|
|
|
const settings = Object.assign({
|
|
state: engine.P4_INITIAL_BOARD,
|
|
computer_level: 0, // default to "stupid" which is the fastest
|
|
}, require("Storage").readJSON(SETTINGS_FILE,1) || {});
|
|
|
|
const ovr = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,2,{msb:true});
|
|
const curfield = [4*FIELD_WIDTH, 6*FIELD_HEIGHT]; // e2
|
|
const startfield = Array(2);
|
|
let piece_sel = 0;
|
|
let showmenu = false;
|
|
let finished = false;
|
|
|
|
const writeSettings = () => {
|
|
settings.state = engine.p4_state2fen(state);
|
|
require('Storage').writeJSON(SETTINGS_FILE, settings);
|
|
};
|
|
|
|
const generateBgImage = () => {
|
|
let buf = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,1,{msb:true});
|
|
for(let idxrow=0; idxrow<8; idxrow++) {
|
|
for(let idxcol=0; idxcol<8; idxcol++) {
|
|
const bgCol = idxrow % 2 != idxcol % 2 ? 0 : 1;
|
|
const x = idxcol*FIELD_WIDTH;
|
|
const y = idxrow*FIELD_HEIGHT;
|
|
buf.setColor(bgCol).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT});
|
|
}
|
|
}
|
|
return {width:buf.getWidth(), height:buf.getHeight(),
|
|
buffer:buf.buffer
|
|
};
|
|
};
|
|
|
|
const idx2Pos = (idxcol, idxrow) => {
|
|
"ram"
|
|
return 2*(1+8+1) + (7-idxrow)*(1+8+1) + idxcol + 1;
|
|
};
|
|
|
|
const drawPiece = (buf, x, y, piece) => {
|
|
let icon;
|
|
|
|
switch(piece & ~0x1) {
|
|
case engine.P4_PAWN:
|
|
icon = ICON_PAWN;
|
|
break;
|
|
case engine.P4_BISHOP:
|
|
icon = ICON_BISHOP;
|
|
break;
|
|
case engine.P4_KING:
|
|
icon = ICON_KING;
|
|
break;
|
|
case engine.P4_QUEEN:
|
|
icon = ICON_QUEEN;
|
|
break;
|
|
case engine.P4_ROOK:
|
|
icon = ICON_ROOK;
|
|
break;
|
|
case engine.P4_KNIGHT:
|
|
icon = ICON_KNIGHT;
|
|
break;
|
|
}
|
|
|
|
if (icon) {
|
|
const scale = FIELD_HEIGHT/ICON_SIZE;
|
|
buf.drawImage(icon, x+(FIELD_WIDTH-(ICON_SIZE*scale))/2, y, {scale: scale});
|
|
}
|
|
return buf;
|
|
};
|
|
|
|
const drawBoard = () => {
|
|
//console.log("Free: " + process.memory().free);
|
|
|
|
g.setBgColor("#555").setColor("#aaa").drawImage(bgImage, Bangle.appRect.x, Bangle.appRect.y);
|
|
for(let idxrow=0; idxrow<8; idxrow++) {
|
|
for(let idxcol=0; idxcol<8; idxcol++) {
|
|
const x = idxcol*FIELD_WIDTH+Bangle.appRect.x;
|
|
const y = idxrow*FIELD_HEIGHT+Bangle.appRect.y;
|
|
|
|
const pos = idx2Pos(idxcol, idxrow);
|
|
const field = state.board[pos];
|
|
|
|
if (field) {
|
|
const fgCol = field & 0x1 ? "#000" : "#fff";
|
|
drawPiece(g.setBgColor(fgCol), x, y, field);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const roundX = (x) => {
|
|
return Math.round(x/FIELD_WIDTH)*FIELD_WIDTH;
|
|
};
|
|
|
|
const roundY = (y) => {
|
|
return Math.round(y/FIELD_HEIGHT)*FIELD_HEIGHT;
|
|
};
|
|
|
|
const drawSelectedField = () => {
|
|
ovr.clear();
|
|
if (!showmenu && !finished) {
|
|
if (startfield[0] !== undefined && startfield[1] !== undefined) {
|
|
// remove piece from startfield
|
|
const x = startfield[0];
|
|
const y = startfield[1];
|
|
ovr.setColor(2).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT});
|
|
}
|
|
|
|
const x = roundX(curfield[0]);
|
|
const y = roundY(curfield[1]);
|
|
ovr.setColor(piece_sel ? 1 : 2)
|
|
.drawRect({x:x+1, y:y, w:FIELD_WIDTH-2, h:FIELD_HEIGHT})
|
|
.drawRect({x:x+2, y:y+1, w:FIELD_WIDTH-4, h:FIELD_HEIGHT-2})
|
|
.drawRect({x:x+3, y:y+2, w:FIELD_WIDTH-6, h:FIELD_HEIGHT-4});
|
|
if (piece_sel) {
|
|
drawPiece(ovr.setBgColor(1), x, y, piece_sel);
|
|
ovr.setBgColor(0); // back to transparent
|
|
}
|
|
}
|
|
Bangle.setLCDOverlay({width:ovr.getWidth(), height:ovr.getHeight(),
|
|
bpp:2, transparent:0,
|
|
palette:new Uint16Array([0, g.toColor("#F00"), g.toColor("#0F0"), 0]),
|
|
buffer:ovr.buffer
|
|
},Bangle.appRect.x,Bangle.appRect.y);
|
|
};
|
|
|
|
const isInside = (rect, e) => {
|
|
return e.x>=rect.x && e.x<rect.x+rect.w
|
|
&& e.y>=rect.y && e.y<=rect.y+rect.h;
|
|
};
|
|
|
|
const showAlert = (msg, cb) => {
|
|
showmenu = true;
|
|
drawSelectedField();
|
|
E.showAlert(msg).then(function() {
|
|
showmenu = false;
|
|
drawBoard();
|
|
drawSelectedField();
|
|
if (cb) {
|
|
cb();
|
|
}
|
|
});
|
|
};
|
|
|
|
const move = (from,to,cbok) => {
|
|
const res = state.move(from, to);
|
|
//console.log(res);
|
|
if (!res.ok) {
|
|
showAlert("Illegal move");
|
|
} else {
|
|
if (res.flags & engine.P4_MOVE_FLAG_MATE) {
|
|
finished = true;
|
|
showAlert("Checkmate or stalemate", cbok);
|
|
} else if (res.flags & engine.P4_MOVE_FLAG_CHECK) {
|
|
showAlert("A king is in check", cbok);
|
|
} else if (res.flags & engine.P4_MOVE_FLAG_DRAW) {
|
|
showAlert("A draw is available", cbok);
|
|
} else if (cbok) {
|
|
cbok();
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
const showMessage = (msg) => {
|
|
g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10);
|
|
};
|
|
|
|
// Run
|
|
|
|
g.reset();
|
|
const bgImage = generateBgImage();
|
|
let state = engine.p4_fen2state(settings.state);
|
|
drawBoard();
|
|
drawSelectedField();
|
|
Bangle.drawWidgets();
|
|
|
|
// drag selected field
|
|
Bangle.on('drag', (ev) => {
|
|
const newx = curfield[0]+ev.dx;
|
|
const newy = curfield[1]+ev.dy;
|
|
if (newx >= 0 && newx <= 7*FIELD_WIDTH) {
|
|
curfield[0] = newx;
|
|
}
|
|
if (newy >= 0 && newy <= 7*FIELD_HEIGHT) {
|
|
curfield[1] = newy;
|
|
}
|
|
drawSelectedField();
|
|
});
|
|
|
|
// touch to start/stop moving a piece
|
|
Bangle.on('touch', (button, xy) => {
|
|
if (isInside(Bangle.appRect, xy) && !showmenu) {
|
|
if (piece_sel === 0) {
|
|
startfield[0] = roundX(curfield[0]);
|
|
startfield[1] = roundY(curfield[1]);
|
|
const startpos = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT);
|
|
piece_sel = state.board[startpos];
|
|
if (piece_sel === 0) {
|
|
startfield[0] = startfield[1] = undefined;
|
|
// nothing here, do nothing
|
|
return;
|
|
}
|
|
} else { // piece_sel === 0
|
|
const colTo = roundX(curfield[0]);
|
|
const rowTo = roundY(curfield[1]);
|
|
if (startfield[0] !== colTo || startfield[1] !== rowTo) {
|
|
showMessage(/*LANG*/"Moving..");
|
|
const posFrom = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT);
|
|
const posTo = idx2Pos(colTo/FIELD_WIDTH, rowTo/FIELD_HEIGHT);
|
|
setTimeout(() => {
|
|
const cb = () => {
|
|
// human move ok, update
|
|
drawBoard();
|
|
drawSelectedField();
|
|
if (!finished) {
|
|
// do computer move
|
|
Bangle.setLCDTimeout(0.1); // this can take some time, turn off to save power
|
|
showMessage(/*LANG*/"Calculating..");
|
|
setTimeout(() => {
|
|
const compMove = state.findmove(settings.computer_level+1);
|
|
const result = move(compMove[0], compMove[1]);
|
|
if (result.ok) {
|
|
writeSettings();
|
|
}
|
|
Bangle.setLCDPower(true);
|
|
Bangle.setLocked(false);
|
|
Bangle.setLCDTimeout(DEFAULT_TIMEOUT/1000); // restore
|
|
if (!showmenu) {
|
|
showAlert(result.string);
|
|
}
|
|
}, 200); // execute after display update
|
|
}
|
|
};
|
|
move(posFrom, posTo,cb);
|
|
}, 200); // execute after display update
|
|
} // piece_sel === 0
|
|
startfield[0] = startfield[1] = undefined;
|
|
piece_sel = 0;
|
|
}
|
|
drawSelectedField();
|
|
}
|
|
});
|
|
|
|
// show menu on button
|
|
setWatch(() => {
|
|
if (showmenu) {
|
|
return;
|
|
}
|
|
showmenu = true;
|
|
piece_sel = 0;
|
|
startfield[0] = startfield[1] = undefined;
|
|
drawSelectedField();
|
|
|
|
const closeMenu = () => {
|
|
showmenu = false;
|
|
E.showMenu();
|
|
drawBoard();
|
|
drawSelectedField();
|
|
};
|
|
|
|
E.showMenu({
|
|
"" : { title : /*LANG*/"Chess settings" },
|
|
"< Back" : () => closeMenu(),
|
|
/*LANG*/"New Game" : () => {
|
|
state = engine.p4_fen2state(engine.P4_INITIAL_BOARD);
|
|
writeSettings();
|
|
closeMenu();
|
|
},
|
|
/*LANG*/"Undo Move" : () => {
|
|
state.jump_to_moveno(-2);
|
|
writeSettings();
|
|
closeMenu();
|
|
},
|
|
/*LANG*/'Level': {
|
|
value: settings.computer_level,
|
|
min: 0, max: 4,
|
|
format: v => [/*LANG*/'stupid', /*LANG*/'middling', /*LANG*/'default', /*LANG*/'slow', /*LANG*/'slowest'][v],
|
|
onchange: v => {
|
|
settings.computer_level = v;
|
|
writeSettings();
|
|
}
|
|
},
|
|
/*LANG*/"Exit" : () => load(),
|
|
});
|
|
}, BTN, { repeat: true, edge: "falling" });
|