From 6c2cc134abf3f32d1d6ec172c6201f8d990c88ab Mon Sep 17 00:00:00 2001 From: Sébastien Dailly Date: Sun, 24 Aug 2014 12:52:10 +0200 Subject: Initial commit --- qml/actions.js | 114 ++++++ qml/content/gfx/black.png | Bin 0 -> 1557 bytes qml/content/gfx/board.png | Bin 0 -> 78033 bytes qml/content/gfx/pieces.svg | 139 ++++++++ qml/content/gfx/white.png | Bin 0 -> 1469 bytes qml/content/sgf/easy.sgf | 299 ++++++++++++++++ qml/content/sgf/hard.sgf | 356 +++++++++++++++++++ qml/cover/CoverPage.qml | 54 +++ qml/pages/Board.qml | 92 +++++ qml/pages/Goban.qml | 262 ++++++++++++++ qml/pages/Point.qml | 29 ++ qml/python/__init__.py | 3 + qml/python/board.py | 57 +++ qml/python/game.py | 145 ++++++++ qml/python/sgfparser.py | 579 +++++++++++++++++++++++++++++++ qml/python/tests/__init__.py | 3 + qml/python/tests/test.sgf | 13 + qml/python/tests/test2.sgf | 6 + qml/python/tests/test_game.py | 101 ++++++ qml/python/tests/test_transformations.py | 134 +++++++ qml/python/transformations.py | 125 +++++++ qml/tsumego.qml | 49 +++ 22 files changed, 2560 insertions(+) create mode 100644 qml/actions.js create mode 100644 qml/content/gfx/black.png create mode 100644 qml/content/gfx/board.png create mode 100644 qml/content/gfx/pieces.svg create mode 100644 qml/content/gfx/white.png create mode 100644 qml/content/sgf/easy.sgf create mode 100644 qml/content/sgf/hard.sgf create mode 100644 qml/cover/CoverPage.qml create mode 100644 qml/pages/Board.qml create mode 100644 qml/pages/Goban.qml create mode 100644 qml/pages/Point.qml create mode 100644 qml/python/__init__.py create mode 100644 qml/python/board.py create mode 100644 qml/python/game.py create mode 100644 qml/python/sgfparser.py create mode 100644 qml/python/tests/__init__.py create mode 100644 qml/python/tests/test.sgf create mode 100644 qml/python/tests/test2.sgf create mode 100644 qml/python/tests/test_game.py create mode 100644 qml/python/tests/test_transformations.py create mode 100644 qml/python/transformations.py create mode 100644 qml/tsumego.qml (limited to 'qml') diff --git a/qml/actions.js b/qml/actions.js new file mode 100644 index 0000000..314015f --- /dev/null +++ b/qml/actions.js @@ -0,0 +1,114 @@ +.pragma library + +/** + * Check if the case on the grid belongs to the first column. + */ +function isFirstCol(index, cols) { + return index % cols == 0; +} + +/** + * Check if the case on the grid belongs to the last column. + */ +function isLastCol(index, cols) { + return index % cols == cols - 1; +} + +/** + * Check if the case on the grid belongs to the first row + */ +function isFirstRow(index, rows) { + return index < rows; +} + +/** + * Check if the case on the grid belongs to the last row. + */ +function isLastRow(index, cols, rows) { + return cols * (rows - 1) <= index; +} + +/** + * Get all the neighbors for a given position. + */ +function getNeighbors(index, cols, rows) { + + var neighbors = []; + if (!isFirstCol(index, cols)) { + neighbors.push(index - 1) + } + + if (!isLastCol(index, cols)) { + neighbors.push(index + 1) + } + + if (!isFirstRow(index, cols)) { + neighbors.push(index - cols) + } + + if (!isLastRow(index, cols, rows)) { + neighbors.push(index + cols) + } + + return neighbors; +} + +function getChainToRemove(index, datas, cols, rows, filter) { + + var piecesToCheck = []; + var piecesToRemove = []; + + /* + * filter wich keep only free places. + */ + function freePlaces(x) { + return datas.itemAt(x).getType() === ""; + } + + var piece = index; + while (piece !== undefined) { + + /* if the case has already been marked, do not check it again. + */ + if (!datas.itemAt(piece).mark) { + datas.itemAt(piece).mark = true; + piecesToRemove.push(piece); + + var neighbors = getNeighbors(piece, cols, rows); + + if (neighbors.length !== 0) { + /* + * If the place has liberty, return empty list. + */ + if (neighbors.some(freePlaces)) { + return []; + } + + /* + * Now update the check list. + */ + neighbors.filter(filter).forEach(function(x) { + piecesToCheck.push(x) + }); + + } + } else { + /* + * The piece may have been marked outside of this call. + * (We try to check chain in each direction, and return as soon as + * we find an empty place). + * If the piece is marked, but does not belongs to the piecesToRemove, + * we assume the piece is connected to a living chain, and + * subsequently this chain too. + */ + if (! piecesToRemove.some(function(x) { return x === piece})) { + return []; + } + } + + piece = piecesToCheck.pop(); + } + return piecesToRemove; + +} + diff --git a/qml/content/gfx/black.png b/qml/content/gfx/black.png new file mode 100644 index 0000000..f7b76fa Binary files /dev/null and b/qml/content/gfx/black.png differ diff --git a/qml/content/gfx/board.png b/qml/content/gfx/board.png new file mode 100644 index 0000000..2bba363 Binary files /dev/null and b/qml/content/gfx/board.png differ diff --git a/qml/content/gfx/pieces.svg b/qml/content/gfx/pieces.svg new file mode 100644 index 0000000..6d93be6 --- /dev/null +++ b/qml/content/gfx/pieces.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/qml/content/gfx/white.png b/qml/content/gfx/white.png new file mode 100644 index 0000000..8de1fab Binary files /dev/null and b/qml/content/gfx/white.png differ diff --git a/qml/content/sgf/easy.sgf b/qml/content/sgf/easy.sgf new file mode 100644 index 0000000..ad7f8fb --- /dev/null +++ b/qml/content/sgf/easy.sgf @@ -0,0 +1,299 @@ +A collection of 40 easy problems. +(;GM[1]FF[3] +;AW[oq][pq][qq][rq][sq][mr][or] +AB[pr][qr][rr][sr][ps];B[rs] +) +(;GM[1]FF[3] +;AW[qn][mp][qp][rp][kq][mq][oq][pq][nr][pr][rs] +AB[nn][mo][np][op][pp][nq][qq][rq][qr][sr][qs] +(;B[or];W[os];B[ps];W[or];B[mr]) +(;B[ps]WV[ps];W[or];B[mr];W[lr]) +) +(;GM[1]FF[3] +;AW[qo][qp][kq][nq][oq][pq] +AB[qh][ol][nm][qm][op][pp][qq][rq][qr][sr][qs][rs] +;W[no];B[oo];W[on];B[pn];W[po] +) +(;GM[1]FF[3] +;AW[ok][qk][rl][mm][ln][pn][op][pp][rp][qq][rq][pr][qr][sr] +AB[qn][qo][ro][np][qp][mq][oq][pq];B[on] +(;W[po];B[pm];W[no];B[oo]) +(;W[oo];B[no];W[pm];B[po]) +) +(;GM[1]FF[3] +;AW[pq][qq][rq][gr][hr][ir][jr][kr][pr][is][ps][rs] +AB[ql][op][pp][qp][rp][fq][gq][hq][iq][jq][kq][lq][mq][oq][dr][fr][gs] +(;W[ns] +(;B[nr] +(;W[ls]) +(;W[ms]) +) +(;B[ms];W[mr];B[lr];W[ls];B[nr];W[ms]) +(;B[lr];W[ls];B[nr];W[ms]) +) +(;W[ms]WV[ms];B[ls];W[lr];B[ns];W[mr];B[nr]) +(;W[lr]WV[lr];B[mr]) +) +(;GM[1]FF[3] +;AW[iq][kq][lq][mq][nq][jr][kr][nr][is][ks] +AB[mn][on][lo][hp][ip][jp][op][qp][hq][oq][gr][mr][or][ns] +;W[ms];B[ls];W[lr];B[ms];W[os] +) +(;GM[1]FF[3] +;AW[ip][jp][kp][lp][iq][lq][ir][kr][lr] +AB[ho][io][jo][ko][lo][hp][mp][hq][mq][hr][mr][or] +(;B[ls];W[is] +(;B[ks]) +(;B[jq]) +) +(;B[jq]WV[jq];W[ls];B[js];W[is];B[jr]) +(;B[is]WV[is];W[js]) +) +(;GM[1]FF[3] +;AW[jq][kq][gr][hr][ir][lr][gs][ls] +AB[jp][kp][lp][fq][gq][hq][iq][lq][nq][fr][mr][fs] +(;B[jr];W[kr];B[js]) +(;B[js]WV[js];W[jr];B[ks];W[is]) +) +(;GM[1]FF[3] +;AW[io][jo][ko][lo][jp][lp][jq][kq][ir][lr][js][ks] +AB[lm][hn][in][jn][ho][mo][mp][hq][iq][lq][mq][hr][mr][ms] +(;B[hs];W[is];B[kr];W[jr];B[ls]) +(;B[ls]WV[ls];W[kr]) +(;B[kr]WV[kr];W[ls]) +) +(;GM[1]FF[3] +;AW[jo][qp][fq][iq][jq][kq][mq][oq][gr][hr][lr][mr] +AB[hp][lp][hq][lq][ir][jr][kr] +;B[jp];W[ip];B[io];W[kp];B[ko] +(;W[jn];B[jp]) +(;W[jp];B[jn]) +) +(;GM[1]FF[3] +;AW[fo][cp][dp][ep][cq][gq][hq][kq][br][dr][hr][ds] +AB[dq][eq][cr][er][fr][gr][es] +;B[bs] +(;W[bq];B[gs]) +(;W[gs];B[bq];W[cs];B[cr];W[bp];B[ar]) +) +(;GM[1]FF[3] +;AW[ro][nq][oq][pq][qq][rq][mr] +AB[or][pr][qr][rr][sr] +(;W[os] +(;B[ps];W[rs];B[ns];W[nr]) +(;B[ns];W[nr];B[rs];W[ps];B[qs];W[os]) +) +(;W[rs] +(;B[os];W[qs]) +(;B[qs];W[os];B[nr];W[ns]) +) +) +(;GM[1]FF[3] +;AW[rk][ol][pl][rl][om][qm][sm][qn][rn][sn][qo][pp][pq][pr][qr] +AB[ri][qj][qk][sk][ql][pm][pn][oo][po][ro][so][qp][sp][qq][rq][rr]; +W[no];B[op];W[np];B[oq];W[or];B[nq];W[mq];B[nr];W[mr];B[os];W[on] +) +(;GM[1]FF[3] +;AW[dl][cm][bn][ao][bo][ap][cp][dp][dq][dr][bs][ds] +AB[cn][en][co][eo][bp][ep][bq][cq][eq][ar][cr][gr][cs] +(;B[er]WV[er];W[aq]) +(;B[bm];W[bl] +(;B[er]) +(;B[do]) +) +) +(;GM[1]FF[3] +;AW[qq][rq][sq][pr][rs] +AB[pp][qp][rp][nq][pq][or][rr][qs] +(;W[sr];B[ss]C[Ko.]) +(;W[qr]WV[qr];B[ss]) +(;W[ps]WV[ps];B[ss]) +) +(;GM[1]FF[3] +;AW[qq][rq][pr][sr] +AB[oo][qo][rp][sp][kq][oq][pq][sq][or] +(;W[rs] +(;B[qr];W[ps]) +(;B[ps];W[qr]) +) +(;W[qr]WV[qr];B[rs]) +(;W[qs]WV[qs];B[rr];W[rs];B[ss]C[Ko.]) +) +(;GM[1]FF[3] +;AW[oq][qq][nr][pr][rr][sr][ns][ps][rs][ss] +AB[ro][op][pp][qp][mq][nq][rq][sq][mr][qr] +(;W[pq];B[qs];W[rr]) +(;W[qs]WV[qs];B[pq]) +) +(;GM[1]FF[3] +;AW[rl][qn][rn][po][so][pp][rp][pq][sq][pr] +AB[qo][ro][qp][qq][rq][rr][sr];W[sp];B[sn];W[sp];B[so];W[rs] +) +(;GM[1]FF[3] +;AW[po][qp][rp][mq][oq][pq][qr] +AB[qq][rq][pr][rr] +(;W[qs];B[rs];W[sq];B[ps];W[sr]) +(;W[or]WV[or];B[qs]) +) +(;GM[1]FF[3] +;AW[hp][ip][jp][kp][lp][mp][np][gq][kq][oq][pq][jr][pr][ps] +AB[fp][gp][cq][fq][hq][iq][jq][lq][mq][nq][kr][mr][or][ls][ms][ns][os] +(;W[hr]WV[hr];B[gr];W[ir];B[gq];W[js];B[hs]) +(;W[is] +(;B[ks];W[gr]) +(;B[gr];W[ks]) +) +) +(;GM[1]FF[3] +;AW[fp][gp][dq][hq][iq][er][hr][jr][hs][is] +AB[hp][ip][jp][lp][gq][jq][gr][lr][gs] +(;B[kr]WV[kr];W[fr]) +(;B[ks] +(;W[fr];B[js]) +(;W[kr];B[ls];W[fq];B[js]) +) +) +(;GM[1]FF[3] +;AW[hp][jp][kp][lp][iq][mq][pq][hr][jr][mr][hs][js] +AB[co][ip][dq][gq][jq][kq][gr][kr][gs][ks] +(;B[ir]WV[ir];W[hq]) +(;B[hq];W[io];B[ir];W[is];B[ir]) +) +(;GM[1]FF[3] +;AW[op][pp][qp][rp][sp][oq][or][os] +AB[pq][qq][rq][sq][pr][ps] +(;W[rr];B[rs];W[sr]) +(;W[rs]WV[rs];B[rr];W[qs];B[ss];W[sr]C[Ko.]) +) +(;GM[1]FF[3] +;AW[bp][cq][dq][eq][jq][mq][br][fr][gr][hr][ir] +AB[fn][bo][co][ep][fp][hp][bq][fq][hq][cr][dr][er] +(;B[aq];W[ap];B[cp];W[ar];B[bq];W[aq];B[dp];W[bq] +(;B[ao]) +(;B[bs]) +) +(;B[cp]WV[cp];W[aq];B[dp];W[bq];B[bs];W[cs];B[ds];W[fs]) +) +(;GM[1]FF[3] +;AW[gp][hp][fq][hq][fr][hr][ir][jr][lr][fs][ls] +AB[go][ho][ko][ep][fp][ip][op][cq][gq][kq][lq][mq][dr][gr][kr][nr][gs][is][js] +(;W[hs]WV[hs];B[gr];W[ks];B[js]) +(;W[ks];B[mr];W[hs];B[js];W[gr]) +) +(;GM[1]FF[3] +;AW[rp][pq][qq][rq][or][ps] +AB[qn][ro][op][pp][qp][mq][oq][nr][ns] +(;B[pr];W[qr];B[rs] +(;W[sp];B[sr];W[os];B[qs]) +(;W[rr];B[os]) +) +(;B[sp]WV[sp];W[rs];B[sr];W[sq];B[qr];W[rr];B[pr];W[qs];B[pr];W[os]) +) +(;GM[1]FF[3] +;AW[hq][iq][jq][gr][kr][gs] +AB[hp][ip][jp][fq][gq][kq][mq][fr][lr];W[ks] +(;B[ir];W[is]) +(;B[is];W[ir];B[ls];W[hs]) +) +(;GM[1]FF[3] +;AW[iq][jq][kq][hr][jr][lr][hs][ls] +AB[ho][ko][ip][kp][gq][hq][lq][mq][gr][mr][gs][ms] +(;B[js]) +(;B[ks]WV[ks];W[kr];B[is];W[ir]) +) +(;GM[1]FF[3] +;AW[ob][oc][qc][pd][qe][pf] +AB[pb][qb][mc][pc][nd][od];W[rb];B[rc] +(;W[qd];B[ra];W[sb];B[sc];W[pa];B[sa];W[rb];B[sb];W[oa]) +(;W[qa]WV[qa];B[qd]) +) +(;GM[1]FF[3] +;AW[cl][cn][co][cp][dq][dr][er] +AB[do][fo][dp][cq][eq][fq][gq][cr];W[br];B[bq];W[cs] +) +(;GM[1]FF[3] +;AW[bm][bn][bo][cp][ep][bq][cq][eq][jq][mq][fr][gr][hr][ir] +AB[bl][cl][am][cm][in][co][do][eo][fo][ip][fq][ar][br][cr][dr][er][es] +;W[fp];B[gq];W[hp];B[gp];W[go] +) +(;GM[1]FF[3] +;AW[kj][lj][mj][nj][pj][jk][ok][jl][pl][mm][pm][in][jn][qn] +[ko][lo][no][oo][po][nq] +AB[lk][mk][nk][kl][ml][ol][km][om][kn][on];B[mn] +(;W[nm];B[nn];W[lm];B[ln]) +(;W[nn];B[nm];W[ln];B[lm]) +) +(;GM[1]FF[3] +;AW[pa][qa][ob][qb][sb][kc][lc][oc][rc][ld][md] +[nd][qd][ke][qe][kf][lf][mf][pf][og] +AB[ra][kb][pb][hc][jc][pc][qc][jd][kd][od][pd][je][le][me][ne][oe] +;B[nc] +(;W[nb] +(;B[mb];W[mc];B[lb];W[nc];B[na]) +(;B[lb];W[mc];B[mb];W[nc];B[na]) +) +(;W[mb];B[nb];W[oa];B[lb]) +) +(;GM[1]FF[3] +;AW[rp][pq][qq][rq][pr]AB[qn][ro][op][pp][qp][oq][or] +(;B[sp];W[rs];B[sr];W[sq] +(;B[rr]) +(;B[ps]) +) +(;B[rr] +(;W[rs];B[sr];W[sp] +(;B[ps]) +(;B[qs]) +) +(;W[sr];B[rs];W[sp];B[ps]) +(;W[sp];B[sr];W[ps];B[rs]) +) +(;B[rs] +(;W[ps] +(;B[sp]) +(;B[rr]) +(;B[sr]WV[sr];W[rr];B[sp];W[sq]) +) +(;W[qs];B[rr];W[sp];B[sr]) +) +) +(;GM[1]FF[3] +;AW[gp][gq][er][fr][hr][ir] +AB[fo][go][fp][hp][cq][dq][fq][hq][jq][kq] +(;W[gs];B[gr];W[gq]) +(;W[gr]WV[gr];B[dr];W[jr];B[kr];W[js];B[es]) +) +(;GM[1]FF[3] +;AW[rp][pq][qq][rq][pr] +AB[qn][ro][op][pp][qp][oq][or] +(;W[rs];B[sr];W[rr]) +(;W[rr]WV[rr];B[qs];W[ps];B[rs];W[sp];B[sr]C[Seki.]) +) +(;GM[1]FF[3] +;AW[qo][ro][qp][oq][qq][pr][ps] +AB[rp][rq][qr][sr][qs]PL[2];W[rs];B[rr];W[sp] +) +(;GM[1]FF[3] +;AW[lo][kp][gq][hq][iq][jq][lq][mq][nq][oq][gr][or] +AB[kq][hr][ir][jr][kr][lr][mr][nr] +(;W[hs];B[is] +(;W[ns];B[ms];W[ks]) +(;W[ks];B[ns];W[ls]) +) +(;W[ns];B[hs]PL[2] +(;W[ms];B[js];W[ls]) +(;W[js];B[ks];W[ms]) +) +(;W[ks]WV[ks];B[ns];W[ls];B[hs];W[js]) +) +(;GM[1]FF[3] +;AW[hq][iq][jq][kq][lq][mq][nq][pq][hr][or][js][ls] +AB[ir][jr][kr][lr][mr][nr][ms] +(;B[is]) +(;B[ks]WV[ks];W[is];B[hs];W[is]) +) +(;GM[1]FF[3] +;AW[ck][fn][do][go][cp][cq][gq][hq][br][hr][cs][ds] +AB[ep][dq][fq][cr][dr][er][fr][gr];B[gs];W[es];B[bs];W[as];B[fs];W[bs] +;B[bq] +) diff --git a/qml/content/sgf/hard.sgf b/qml/content/sgf/hard.sgf new file mode 100644 index 0000000..f628e66 --- /dev/null +++ b/qml/content/sgf/hard.sgf @@ -0,0 +1,356 @@ +A collection of 40 intermediate and hard problems. +(;GM[1]FF[3] +;AB[cb][dc][dd][ae][be][ce][bg] +AW[bb][fb][cc][ec][ad][bd][cd][de][ee][df] +(;B[db]WV[db];W[ab];B[ba];W[ca];B[da];W[ed]) +(;B[ac];W[bc];B[ab];W[aa];B[db];W[da] +(;B[ab];W[ac] +(;B[ea];W[eb];B[ba];W[ca];B[ba];W[ca];B[da];W[ed];B[ba]) +(;B[ba];W[ca];B[ea];W[eb];B[ba];W[ca];B[da];W[ed];B[ba]) +) +(;B[ea];W[eb];B[ab];W[ac];B[ba];W[ca];B[ba];W[ca];B[da];W[ed];B[ba]) +) +) +(;GM[1]FF[3] +;AW[ro][qp][rp][qq][pr][qr] +AB[qm][rn][po][qo][pp][nq][pq][or][os];B[rr];W[rs];B[sq];W[ps];B[ss] +) +(;GM[1]FF[3] +;AW[ob][nc][md][nd][od][qe][re] +AB[ma][lb][nb][lc][mc][oc][pc];W[pb];B[qc];W[qb];B[rb];W[rc];B[rd]; +W[sc];B[sd];W[qd];B[sb];W[rc];B[sc];W[pd] +) +(;GM[1]FF[3] +;AB[ao][bo][bn][bm][bl][cl][ck][dm][dn][fk][gl][fi] +AW[il][jj][cm][cn][co][bp][bq][cr][dl][dk][cj][bj][bk][bh][ch][fj]; +B[dj];W[ej];B[di];W[ek];B[gj];W[fl];B[gk];W[fm];B[en];W[fo];B[go]; +W[gn];B[hn];W[fn];B[cp];W[do];B[eo];W[dp];B[ep] +) +(;GM[1]FF[3] +;AW[dn][ao][bo][co][dp][ep][fp][fq][dr][er] +AB[ap][bp][cp][cq][dq][eq][hq][kq][fr][gr];W[cr];B[br];W[ar];B[bs]; +W[bq] +) +(;GM[1]FF[3] +;AW[lb][nb][nc][sc][od][sd][oe][se][of][sf][pg][qg][rg] +AB[ob][sb][oc][rc][pd][qd][rd][pe][re];W[qb];B[rb];W[pa];B[ra] +(;W[qf];B[oa];W[pc]) +(;W[rf];B[oa];W[pc]) +) +(;GM[1]FF[3] +;AW[mb][rb][mc][qc][nd][qd][ne][qe][re][of][qf][og][pg] +AB[nb][ob][pb][qb][pc][pd][pe][pf][rf][qg][rg][qi][qk] +(;W[sd];B[ra];W[rc];B[sb];W[qa];B[pa];W[na]) +(;W[ra]WV[ra];B[sd];W[rc];B[se]) +(;W[sc]WV[sc];B[ra]) +) +(;GM[1]FF[3] +;AW[ra][mb][rb][mc][pc][qc][nd][od][qd][pe][qe] +AB[qa][nb][qb][sb][lc][nc][rc][sc][ld][md][rd][re][mf][of][pf][qf] +(;W[pa]WV[pa];B[sa];W[na];B[lb];W[ma];B[la];W[oa];B[pb];W[ob]) +(;W[na];B[lb];W[ma] +(;B[la];W[pb];B[sa];W[oa]) +(;B[oa];W[pa];B[pb];W[ob]) +) +) +(;GM[1]FF[3] +;AW[ja][ib][ob][pb][hc][jc][qc][jd][od][pd][je][qe][kf][lf][of][pf][mg][ng] +AB[ka][jb][kb][lb][mb][nc][oc][pc][md][nd][le][oe][mf][nf] +(;W[lc];B[kc];W[ld];B[kd];W[ke];B[mc];W[ld];B[lc];W[ne]) +(;W[ld];B[kd];W[lc];B[kc];W[ke];B[mc];W[ld];B[lc];W[ne]) +) +(;GM[1]FF[3] +;AW[na][lb][nb][rb][oc][pc][qc][kd][le][qf]AB[ob][pb][kc][mc][nc][ne] +;B[mb];W[ma];B[ka];W[kb];B[oa];W[la];B[jb] +) +(;GM[1]FF[3] +;AW[dm][en][fo][ho][dp][ep][fp][hp][eq][gq][iq][er][gr] +AB[em][fm][dn][gn][hn][co][go][cp][gp][dq][fq][dr][fr][es][fs] +(;W[cq];B[cr] +(;W[ds];B[cs];W[gs];B[ds];W[bq]) +(;W[bq];B[br];W[ds];B[cs];W[gs];B[ds];W[ar]) +) +(;W[gs]WV[gs];B[ds];W[cq];B[cr];W[bq];B[br]) +) +(;GM[1]FF[3] +;AW[bh][bi][cj][dk][cm][dm][bn][dn][dp][cq][dq] +AB[ai][bj][bk][bl][bm][cn][co][cp] +(;W[aq]WV[aq];B[ao];W[an];B[am]) +(;W[ao] +(;B[bq];W[bp]) +(;B[bo];W[aq];B[an] +(;W[ap]WV[ap];B[bq];W[br];B[ar];W[as];B[bp];W[ar];B[cr]) +(;W[bp];B[ap]C[Ko.]) +) +) +) +(;GM[1]FF[3] +;AW[da][db][dc][dd][ce][cf][ag][bg][cg] +AB[ab][cb][cc][cd][be][bf] +(;W[ba] +(;B[ca] +(;W[bd]WV[bd];B[bc];W[ad];B[ac]) +(;W[bb] +(;B[bc];W[ad] +(;B[ae];W[af]) +(;B[bd];W[ae]) +) +(;B[bd];W[af] +(;B[ae] +(;W[bc]) +(;W[ac]) +) +(;B[bc] +(;W[ad]) +(;W[ae]) +) +) +) +) +(;B[bb];W[ca];B[ad];W[af]) +) +(;W[bd]WV[bd];B[bc]) +) +(;GM[1]FF[3] +;AB[qj][pk][qk][ol][nm][qn][no][po][pp][qp][rp][pr] +AW[ql][pm][pn][qo][ro][sp][rq][rr][rs] +(;B[sk];W[rl];B[sl] +(;W[sm] +(;B[sn];W[so];B[rm]) +(;B[rm]WV[rm];W[sn]) +) +(;W[rn];B[rm];W[qm];B[sm]) +) +(;B[rn]WV[rn];W[so] +(;B[sq];W[sr];B[sn];W[sq];B[rl];W[rm]) +(;B[sn];W[sq];B[rl];W[rm]) +) +(;B[rl]WV[rl];W[rm];B[rn];W[so];B[sn];W[sq];B[sm];W[qm]) +) +(;GM[1]FF[3] +;AW[qj][pl][ql][nm][om][nn][no][op][jq][lq][oq][pq][mr][or] +AB[ml][nl][ol][pm][qm][ln][oo][po][lp][mp][np][nq][qq][pr][qr] +;W[pn];B[qn];W[qo];B[ro];W[qp];B[on];W[pp];B[pn] +(;W[sn];B[rl];W[sm]) +(;W[rp]WV[rp];B[rl];W[rk];B[mm]) +) +(;GM[1]FF[3] +;AW[pa][pb][kc][mc][nc][oc][sc][pd][qd][rd][qg] +AB[nb][ob][qb][pc][qc][rc] +(;B[oa];W[mb];B[sb];W[na];B[ra]) +(;B[qa]WV[qa];W[sb]) +(;B[sb]WV[sb];W[ra];B[oa];W[qa];B[sd];W[mb]) +) +(;GM[1]FF[3] +;AW[qb][rc][sc][qd][qe][pf][pg][ph][rh] +AB[rd][re][sd][rg][qi][pi][ri][qf] +(;W[sf] +(;B[qh];W[rf]) +(;B[rf];W[qh];B[sh];W[sg]) +) +(;W[qg]WV[qg];B[rf]) +(;W[sg]WV[sg];B[qh]) +(;W[sh]WV[sh];B[qh];W[qg];B[rf]) +) +(;GM[1]FF[3] +;AW[bb][cc][cd][de][df][cg][ch] +AB[bd][ce][cf][dg][dh][ci][di] +(;W[bf]WV[bf];B[be];W[bg];B[af];W[bi];B[ag]) +(;W[bg] +(;B[bf];W[af];B[be];W[bi];B[ah];W[ai];B[bj];W[bc]) +(;B[af];W[ae];B[be];W[bi];B[bj];W[ah];B[aj];W[bf];B[ai];W[bc]) +) +) +(;GM[1]FF[3] +;AW[lk][el][kl][ll][fm][jm][mm][fn][jn][go][ho][io][no] +AB[ii][jk][jl][ml][im][km][lm][gn][hn][fo][ko][dp][fp][gp][ip][jp]; +W[hm];B[gm];W[gl];B[hl];W[in];B[hm];W[hk];B[il];W[jj];B[gk] +(;W[kk];B[fl];W[ik];B[gl];W[gj]) +(;W[ik];B[fl];W[kk];B[gl];W[gj]) +) +(;GM[1]FF[3] +;AW[ib][mb][sb][ic][jc][mc][oc][pc][qc][rc][fd][md][od][le][me][oe][nf][of] +AB[jb][nb][ob][pb][qb][rb][kc][lc][nc][jd][nd][ke][ne][kf][mf][mg] +(;W[ja] +(;B[la];W[lb];B[kb];W[ma];B[ka];W[kd];B[ia];W[ld]) +(;B[kb];W[ma] +(;B[ka] +(;W[ra]) +(;W[na]) +) +(;B[la];W[lb];B[ka];W[kd]) +) +) +(;W[ma]WV[ma];B[lb]) +(;W[la]WV[la];B[ka]) +) +(;GM[1]FF[3] +;AW[sp][pq][qq][rq][or][os] +AB[no][po][ro][pp][rp][nq][oq][sq][nr] +(;W[sr]WV[sr];B[rs];W[qs];B[qr];W[pr];B[ps]) +(;W[qs] +(;B[sr];W[rr]) +(;B[pr];W[sr]) +(;B[rr];W[sr]) +) +(;W[rr]WV[rr];B[qs]) +(;W[rs]WV[rs];B[sr]) +) +(;GM[1]FF[3] +;AW[nb][mc][nc][oc][rd][ne][pe][qe][re] +AB[ob][pb][pc][rc][pd][qd] +(;W[sb];B[sc];W[rb];B[qb];W[sd];B[qc];W[oa]) +(;W[rb]WV[rb];B[sb]) +) +(;GM[1]FF[3] +;AW[na][kb][mb][lc][rc][ld][le][me][re][nf][pf][qf][rf][pj] +AB[ob][oc][qc][md][nd][rd][pe][qe] +(;W[rb];B[sd];W[oa];B[pb];W[pa];B[qa] +(;W[pd];B[qd];W[od];B[oe];W[ne]) +(;W[qb]WV[qb];B[nb];W[ra];B[mc]) +) +(;W[sd]WV[sd];B[rb];W[qd];B[pd];W[qb]) +) +(;GM[1]FF[3] +;AW[cb][db][bc][cd][dd][de][df][dg][dh] +AB[eb][dc][ed][be][fe][cf][fg][ch][ci][ei][fj][ck];W[bf];B[bg];W[af]; +B[ag];W[ad];B[bd];W[ac];B[ae];W[ba] +) +(;GM[1]FF[3] +;AW[pd][oc][ob][nc][md][ld][kd][kc][if][hf][hh][fg][eg][df] +[dd][dc][cb][bc][hc][gb] +AB[ib][kb][lc][mc][mb][nb][he][gf][ff][ee][ec][fc][gc][eb][db] +;W[jb];B[ja];W[hb];B[id];W[ic];B[jc];W[jd] +) +(;GM[1]FF[3] +;AW[bo][do][ap][cp][dp][dq][dr][fr][bs][cs] +AB[cn][dn][en][co][fo][bp][bq][cq][fq][gq][ar][br][cr][hr];W[bn]; +B[bm];W[ao];B[am];W[ds];B[an];W[as];B[aq];W[bo] +) +(;GM[1]FF[3] +;AW[lm][mm][kn][mn][lo][mo][lp][np][mq][lr][nr][or] +AB[mj][kk][nk][il][kl][ol][im][on][io][jo][no][oo][pp] +[iq][kq][nq][oq][kr][pr][ps] +(;W[os];B[ms];W[ls];B[mr];W[ns];B[mr];W[lq];B[ms];W[nr]) +(;W[ms]WV[ms];B[os]) +(;W[ns]WV[ns];B[ls]) +(;W[lq]WV[lq];B[ms];W[ls];B[ns]) +) +(;GM[1]FF[3] +;AW[mb][mc][md][nd][rd][oe][qe] +AB[nc][oc][pc][qc][rc] +(;W[oa] +(;B[ra];W[sb]) +(;B[na];W[nb];B[pa];W[ob]) +(;B[pa];W[na];B[ob];W[qb] +(;B[ra];W[sb];B[sc];W[rb]) +(;B[rb];W[sc];B[sb];W[ra]) +) +) +(;W[sb]WV[sb];B[sc];W[oa];B[pa];W[na];B[ob]) +) +(;GM[1]FF[3] +;AW[fq][gq][hq][iq][jq][fr][kr][lr][fs][gs] +AB[go][io][cp][ep][fp][jp][eq][kq][lq][mq][er][gr][hr][mr][hs][ls] +(;W[ks];B[jr];W[ms];B[js];W[ir];B[ls];W[is];B[hr];W[kr]) +(;W[js]WV[js];B[ir]) +) +(;GM[1]FF[3] +;AB[be][bf][cf][bh][bi] +AW[bb][cc][cd][ce][df][dg][ch][ci][cj][cl][cn] +;B[ag];W[cg];B[bg];W[ae] +;B[bd];W[ad];B[bc];W[ac];B[ab];W[aa];B[af];W[ai];B[bj] +) +(;GM[1]FF[3] +;AW[rb][qb][qc][od][oc][ne][me][md][lc][mb][kb][jb][ia][hb] +AB[lb][kc][jc][ic][ib][le][mf][nf][of][oe][pd][qd][rc][re][sb][nd] +;B[ld];W[mc];B[pc] +(;W[pb];B[ob];W[nc];B[pa];W[nb];B[la] +(;W[ma];B[oa]) +(;W[ja];B[na]) +) +(;W[ob];B[pb];W[pa];B[ra]) +) +(;GM[1]FF[3] +;AW[nb][pc][qc][rc][qg][rg][qh][qi][rj][rk] +AB[pd][rd][pf][rf][oh][nj][pj][qk][pl][rl][rm];W[sg];B[si];W[re] +(;B[se];W[qe];B[qf];W[sf]) +(;B[qe];W[se];B[qf];W[sf]) +(;B[qf];W[sf] +(;B[qd];W[sd]) +(;B[qe];W[se] +(;B[qd];W[sd]) +(;B[sd];W[qd]) +) +) +(;B[sf];W[qf]) +) +(;GM[1]FF[3] +;AW[rd][rc][qb][oc][nc][mc][lc][jb][kb][ic][hc][gc] +AB[ib][hb][jc][kc][ob][ld][md][nd][od][qc][qe][re];B[pc];W[pb];B[lb]; +W[mb];B[oa] +(;W[rb];B[ma];W[la];B[nb]) +(;W[la];B[rb];W[ra];B[sb];W[sc];B[qa]) +(;W[na];B[la]) +) +(;GM[1]FF[3] +;AW[qb][mb][qc][mc][qd][md][me][rf][qf][lf][pg][lg][ph][oh][nh][mh] +AB[pb][nb][pc][nc][pd][nd][pe][ne][pf][mf][og][ng] +(;W[oa] +(;B[ob];W[od];B[oe];W[nf]) +(;B[pa];W[na];B[mg];W[of];B[nf];W[oe]) +(;B[nf];W[oe]) +) +(;W[od]WV[od];B[nf];W[oe];B[oa];W[oc]) +(;W[mg]WV[mg];B[nf];W[oa];B[ob]) +) +(;GM[1]FF[3] +;AB[ka][db][jb][cc][hc][ic][kc][cd][hd][de][he][ef][gf][gg] +AW[ja][eb][hb][ib][ec][gc][ed][gd][fe] +;B[ga];W[fa];B[fb];W[ea];B[gb]; +W[fc];B[ha];W[ia];B[gb] +) +(;GM[1]FF[3] +;AW[bp][cp][dp][ep][fp][bq][fq][gr] +AB[cq][dq][eq][ar][br][fr];W[ds] +(;B[er];W[es];B[fs];W[cr]) +(;B[es];W[cr];B[bs];W[aq]) +) +(;GM[1]FF[3] +;AW[ap][bp][cp][aq][dq][eq][cr] +AB[bo][co][do][ep][gp][bq][fq][hq][ar][br] +(;B[dp];W[bs];B[er];W[dr];B[es] +(;W[fr];B[gr];W[ds];B[ao]) +(;W[ds];B[ao]) +) +(;B[er]WV[er];W[dr];B[dp];W[fr]) +) +(;GM[1]FF[3] +;AW[kb][ob][rb][jc][lc][mc][rc][nd][rd][ne][qe][of][qf] +AB[lb][mb][nb][nc][oc][pc][qc][od][qd][pe] +(;W[oa]WV[oa];B[na]) +(;W[na]WV[na];B[pb]) +(;W[qb]WV[qb];B[pb];W[oa];B[na]) +(;W[la]WV[la];B[oe]) +(;W[qa]WV[qa];B[oa]) +(;W[pa] +(;B[pb];W[oa];B[qa];W[oe];B[na];W[la]) +(;B[qa] +(;W[oe];B[pb];W[oa]) +(;W[qb]WV[qb];B[pb]) +) +) +) +(;GM[1]FF[3] +;AW[pb][qc][rc][rd]AB[oa][nc][oc][pc][qd][qe][re] +(;W[ob];B[nb];W[qa];B[sb];W[rb];B[na];W[sa];B[pa];W[sc];B[qb];W[pb]) +(;W[ra]WV[ra];B[sb];W[ob];B[nb];W[pa];B[qb]) +(;W[qa];B[ob]) +) +(;GM[1]FF[3] +;AB[qm][qo][ro][so][pp][pq][pr][ps][ss] +AW[oo][po][op][qp][rp][nq][qq][sq][nr][rr][ns] +(;B[rs]WV[rs];W[qr];B[qs];W[oq];B[sp]) +(;B[sp]WV[sp];W[qr]) +(;B[qr];W[sp];B[rs];W[oq];B[sr];W[qs];B[sr];W[ss];B[rs]) +) diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml new file mode 100644 index 0000000..786b78f --- /dev/null +++ b/qml/cover/CoverPage.qml @@ -0,0 +1,54 @@ +/* + Copyright (C) 2013 Jolla Ltd. + Contact: Thomas Perl + All rights reserved. + + You may use this file under the terms of BSD license as follows: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Jolla Ltd nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CoverBackground { + Label { + id: label + anchors.centerIn: parent + text: qsTr("My Cover") + } + + CoverActionList { + id: coverAction + + CoverAction { + iconSource: "image://theme/icon-cover-next" + } + + CoverAction { + iconSource: "image://theme/icon-cover-pause" + } + } +} + + diff --git a/qml/pages/Board.qml b/qml/pages/Board.qml new file mode 100644 index 0000000..37a0e89 --- /dev/null +++ b/qml/pages/Board.qml @@ -0,0 +1,92 @@ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + + +import io.thp.pyotherside 1.2 + + + +Page { + + width: Screen.width; height: Screen.height; + + + anchors.fill: parent + + Column { + + width:parent.width + height: parent.height + + spacing: 2 +// Row { + +// height: 60 + +// //anchors.horizontalCenter: parent.horizontalCenter +// Repeater { +// model: 3 +// Rectangle { +// width: 100; height: 40 +// border.width: 1 +// color: "yellow" +// } +// } +// } + + + Goban { + id:goban + width: parent.width + height: 650 + } + + + SlideshowView { + id: view + width: parent.width + height: 100 + itemWidth: width / 2 + onCurrentIndexChanged: {py.call('board.getGame', [view.currentIndex], goban.setGoban)} + + model: 5 + delegate: Text { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + width: view.itemWidth + height: view.height + text: "Level " + index + color: "white" + + } + } + } + + Python { + id:py + Component.onCompleted: { + var pythonpath = Qt.resolvedUrl('../python').substr('file://'.length); + addImportPath(pythonpath); + console.log(pythonpath); + + importModule('board', function() { + console.log('module loaded'); + console.log('Python version: ' + pythonVersion()); + }) + + setHandler('log', function (content) { + console.log(content); + }); + + call('board.setPath', [pythonpath]); + call('board.loadBoard', ["easy.sgf"], function (result) { + console.log(result + " problems found in the file") + view.model = result + call('board.getGame', [0], goban.setGoban); + }); + + } + } + +} diff --git a/qml/pages/Goban.qml b/qml/pages/Goban.qml new file mode 100644 index 0000000..64475ec --- /dev/null +++ b/qml/pages/Goban.qml @@ -0,0 +1,262 @@ +import QtQuick 2.0 + +import "../actions.js" as Actions + +Item { + + /** + * This property represent a case size on the board. + * The value is calculated at initialization, and depends on the goban size. + */ + property int caseSize + + /** + * Booleans flags telling if the board is limited in each directions + */ + property bool limitTop: true; + property bool limitBottom: true; + property bool limitLeft: true; + property bool limitRight: true; + + /* + * The current color to play : + * - true for white + * - false for black + */ + property bool currentPlayer: true; + + function setGoban(ret) { + + limitTop = ret.side.TOP; + limitBottom = ret.side.BOTTOM; + limitLeft = ret.side.LEFT; + limitRight = ret.side.RIGHT; + + goban.columns = ret.size[0] + goban.rows = ret.size[1] + + currentPlayer = true; + + var maxWidth = width / ret.size[0] + var maxHeight = height / ret.size[1] + + if (maxWidth > maxHeight) { + caseSize = maxHeight; + } else { + caseSize = maxWidth; + } + + /* + * Put the initials stones + */ + var initial = ret.tree[0] + + var aw = initial.AW; + if (aw !== undefined) { + aw.forEach(function (pos) { + goban.getItemAt(pos[0], pos[1]).setColor(currentPlayer); + }); + } + + var ab = initial.AB; + if (ab !== undefined) { + ab.forEach(function (pos) { + goban.getItemAt(pos[0], pos[1]).setColor(!currentPlayer); + + }); + } + } + + /** + * Handle a click on the goban. + */ + function clickHandler(index) { + + + if ( !limitLeft && Actions.isFirstCol(index, goban.columns) + || !limitRight && Actions.isLastCol(index, goban.columns) + || !limitTop && Actions.isFirstRow(index, goban.rows) + || !limitBottom && Actions.isLastRow(index, goban.columns, goban.rows) ) { + return; + } + + var point = repeater.itemAt(index); + var elementType = point.getType(); + + if (elementType !== "") { + return; + } + + var neighbors = Actions.getNeighbors(index, goban.columns, goban.rows); + + function isPlayer(x) { + return repeater.itemAt(x).getType() === (currentPlayer ? "white" : "black"); + } + + function isOponnent(x) { + return repeater.itemAt(x).getType() === (currentPlayer ? "black" : "white"); + } + + /* + * Check for pieces to remove. + */ + var toRemove = neighbors.filter(isOponnent); + +// function freeOrChain(x) { +// var pointType = repeater.itemAt(x).getType(); +// return pointType === "" || pointType === (currentPlayer ? "white" : "black"); +// } +// /* +// * Single suicide is not allowed… +// */ +// if (neighbors.length !== 0 && toRemove.length === 0 && !neighbors.some(freeOrChain)) { +// return; +// } + + point.setColor(currentPlayer); + + if (neighbors.length !== 0) { + + toRemove.forEach(function(neighbor) { + Actions.getChainToRemove(neighbor, repeater, goban.columns, goban.rows, isOponnent). + forEach(function(x) { + repeater.itemAt(x).remove(); + }) + }); + + /* + * Check for suicide. + */ + Actions.getChainToRemove(index, repeater, goban.columns, goban.rows, isPlayer). + forEach(function(x) { + repeater.itemAt(x).remove(); + }); + + /* + * Remove the marks in the cases. + * + * The call to getChainToRemove add marks on the cases in order to + * prevent infinite looping. We need to clean the cases before any new + * click. + * + * We do not need to remove them before as we are not filtering the + * same pieces. + */ + for (var i = 0; i < goban.columns * goban.rows; i++) { + repeater.itemAt(i).mark = false; + } + + } + currentPlayer = !currentPlayer; + + } + + /** + * Background + */ + Image { + width: goban.width + (caseSize / 2); height: goban.height + (caseSize / 2); + source: "../content/gfx/board.png" + anchors.centerIn: goban + } + + /* + * Horizontal lines + */ + Repeater { + model: goban.rows + + Rectangle { + + function isOpen(index) { + if ( (index === goban.rows - 1 && !limitBottom) || (index === 0 && !limitTop)) { + return "transparent" + } + return "black" + } + + x: goban.x + (caseSize / 2) + + y: goban.y + (caseSize / 2) + (index * caseSize) + + width: goban.width - caseSize; + + color: isOpen(index) + height: 1 + + } + } + + /* + * Verticals lines + */ + Repeater { + model: goban.columns + + Rectangle { + + function isOpen(index) { + if ( (index === goban.columns - 1 && !limitRight) || (index === 0 && !limitLeft)) { + return "transparent" + } + return "black" + } + + x: goban.x + (caseSize / 2) + (index * caseSize) + + y: goban.y + (caseSize / 2) + + height: goban.height - caseSize; + + color: isOpen(index) + width: 1 + } + + + } + + /* + * The grid for the game. + */ + Grid { + id: goban + anchors.centerIn: parent + columns: 0 + rows : 0 + spacing: 0 + + function getItemAt(x, y) { + return repeater.itemAt(x + y * columns) + } + + Repeater { + model: goban.columns * goban.rows + id : repeater + + Item { + + function setColor(isWhite) { + piece.type = isWhite ? "white" : "black" + } + + function remove() { + piece.type = ""; + } + + function getType() { + return piece.type; + } + + width: caseSize; height: caseSize + + property bool mark: false + + Point{ + id : piece + width: caseSize; height: caseSize + + } + } + } + } +} diff --git a/qml/pages/Point.qml b/qml/pages/Point.qml new file mode 100644 index 0000000..f610461 --- /dev/null +++ b/qml/pages/Point.qml @@ -0,0 +1,29 @@ +import QtQuick 2.0 + +import "../actions.js" as Actions + +Item { + + property string type: ""; + + function getImageForType() { + if ("" === type) { + return "" + } + + return "../content/gfx/" + type + ".png" + } + + MouseArea { + + id: interactiveArea + anchors.fill: parent + onClicked: clickHandler(index); + } + + Image { + id: piece + anchors.fill: parent + source: getImageForType(); + } +} diff --git a/qml/python/__init__.py b/qml/python/__init__.py new file mode 100644 index 0000000..7847780 --- /dev/null +++ b/qml/python/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + diff --git a/qml/python/board.py b/qml/python/board.py new file mode 100644 index 0000000..48f3ff0 --- /dev/null +++ b/qml/python/board.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +try: + import pyotherside +except: + print("no pyotherside module loaded") + +import sgfparser +from game import Game + +counter = 0 + +path = "" +cursor = None + +def setPath(qtPath): + global path + path = qtPath + +def loadBoard(filename): + global cursor + + sgfPath = os.path.join(path,"../content","sgf",filename); + pyotherside.send('log', sgfPath) + try: + f = open(sgfPath) + s = f.read() + f.close() + except IOError: + pyotherside.send('log', "Cannot open %s" % filename) + return + + try: + cursor = sgfparser.Cursor(s) + except sgfparser.SGFError: + pyotherside.send('log', 'Error in SGF file!') + return + pyotherside.send('log', 'File %s loaded' % filename) + pyotherside.send('log', 'Found %d problems' % cursor.root.numChildren) + return cursor.root.numChildren + +def getGame(n): + global cursor + + cursor.game(n) + + game = Game(cursor) + game.normalize() + pyotherside.send('log', "Game loaded !!") + + return { + "tree": game.tree, + "size": game.get_size(), + "side": game.side, + } diff --git a/qml/python/game.py b/qml/python/game.py new file mode 100644 index 0000000..7a91ae7 --- /dev/null +++ b/qml/python/game.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import random +from transformations import * + + +class Game(object): + """" A game loaded from a sgf data source. + """ + + def __init__(self, cursor): + """ Create a new Game on the current cursor position. + :cursor: The cursor opened at the game. + """ + + node = cursor.currentNode() + + # display problem name + name = '' + if 'GN' in node: + name = node['GN'][0][:15] + self.name = name + + while not ('AG' in node or 'AW' in node \ + or 'B' in node or 'W' in node): + node = cursor.next() + + self.min_x, self.min_y = 19, 19 + self.max_x, self.max_y = 0, 0 + + # Get the board size from the whole possibles positions and create the + # game tree + self.tree = Game.create_tree(cursor, self.extend_board_size, []) + + x_space = 2 + y_space = 2 + + if self.min_y > y_space: + self.min_y -= y_space + + if self.min_x > x_space: + self.min_x -= x_space + + if self.max_y < 19 - y_space: + self.max_y += y_space + + if self.max_x < 19 - x_space: + self.max_x += x_space + + self.side = { + "TOP": self.min_y != 0, + "LEFT": self.min_x != 0, + "RIGHT": self.max_x != 19, + "BOTTOM": self.max_y != 19, + } + + + def extend_board_size(self, pos): + """ Extend the board size to include the position given. + """ + x, y = Game.conv_coord(pos) + self.min_x = min(x, self.min_x) + self.max_x = max(x, self.max_x) + self.min_y = min(y, self.min_y) + self.max_y = max(y, self.max_y) + return (x, y) + + @staticmethod + def create_tree(cursor, fun, acc=None): + """ Walk over the whole node in the game and call fun for each of them. + :cursor: The cursor in the sgf parser. + :fun: Function called for each position read + """ + + if acc is None: + acc = [] + + node = cursor.currentNode().copy() + for key in ['AB', 'AW', 'B', 'W']: + if key in node: + node[key] = [fun(pos) for pos in node[key]] + + acc.append(node) + childs = cursor.noChildren() + + if childs == 1: + # When there is only one child, we just add it to the current path + cursor.next() + Game.create_tree(cursor, fun, acc) + cursor.previous() + elif childs > 1: + # Create a new list containing each subtree + sub_nodes = [] + for i in range(childs): + cursor.next(i) + sub_nodes.append(Game.create_tree(cursor, fun)) + cursor.previous() + acc.append(sub_nodes) + return acc + + def get_size(self): + #return self.max_x, self.max_y + x_size = self.max_x - self.min_x + y_size = self.max_y - self.min_y + return min(19, x_size + 1), min(19, y_size + 1) + + @staticmethod + def conv_coord(x): + """ This takes coordinates in SGF style (aa - qq) and returns the + corresponding integer coordinates (between 1 and 19). """ + + print(x) + + return tuple([ord(c) - 96 for c in x]) + + def parse_tree(self, fun, elements=None): + """" Parse the current tree, and apply fun to each element. + """ + + if elements is None: + elements = self.tree + + for elem in elements: + if isinstance(elem, dict): + for key in ['AB', 'AW', 'B', 'W']: + if key in elem: + elem[key] = [fun(pos) for pos in elem[key]] +# for type, values in elem.items(): +# elem[type] = [fun(coord) for coord in values] + else: + for l in elem: + self.parse_tree(fun, l) + + def normalize(self): + """ Create a normalized board, translated on lower coord. + """ + + for transformation in [Translation(self), Rotation(self), Translation(self), Symmetry(self)]: + if not transformation.is_valid(): + continue + + self.parse_tree(transformation.apply_points) + self.min_x, self.min_y, self.max_x, self.max_y = transformation.get_new_size() + self.side = transformation.get_new_side() diff --git a/qml/python/sgfparser.py b/qml/python/sgfparser.py new file mode 100644 index 0000000..2ad91c9 --- /dev/null +++ b/qml/python/sgfparser.py @@ -0,0 +1,579 @@ +# File: sgfparser.py + +## This is part of uliGo 0.4, a program for practicing +## go problems. For more information, see http://www.g0ertz.de/uligo/ + +## Copyright (C) 2001-12 Ulrich Goertz (uligo@g0ertz.de) + +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. + +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +## You should have received a copy of the GNU General Public License +## along with this program (gpl.txt); if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## The GNU GPL is also currently available at +## http://www.gnu.org/copyleft/gpl.html + +import re +import string + +class SGFError(Exception): pass + +reGameStart = re.compile(r'\(\s*;') +reRelevant = re.compile(r'[\[\]\(\);]') +reStartOfNode = re.compile(r'\s*;\s*') + +def SGFescape(s): + t = s.replace('\\', '\\\\') + t = t.replace(']', '\\]') + return t + +class Node: + def __init__(self, previous=None, SGFstring = '', level=0): + self.previous = previous + self.next = None + self.up = None + self.down = None + self.level = level # self == self.previous.next[self.level] + + self.numChildren = 0 + + self.SGFstring = SGFstring + self.parsed = 0 + if self.SGFstring: + self.parseNode() + else: + self.data = {} + + self.posyD = 0 + + + def getData(self): + if not self.parsed: self.parseNode() + return self.data + + def pathToNode(self): + l = [] + n = self + + while n.previous: + l.append(n.level) + n = n.previous + + l.reverse() + return l + + + def parseNode(self): + + if self.parsed: return + + s = self.SGFstring + i = 0 + + match = reStartOfNode.search(s, i) + if not match: + raise SGFError('No node found') + i = match.end() + + node = {} + while i < len(s): + + while i < len(s) and s[i] in string.whitespace: i += 1 + if i >= len(s): break + + ID = [] + + while not s[i] == '[': + if s[i] in string.ascii_uppercase: + ID.append(s[i]) + elif not s[i] in string.ascii_lowercase + string.whitespace: + raise SGFError('Invalid Property ID') + + i += 1 + + if i >= len(s): + raise SGFError('Property ID does not have any value') + + i += 1 + + key = ''.join(ID) + + if key == '': raise SGFError('Property does not have a correct ID') + + + if key in node: + if not Node.sloppy: + raise SGFError('Multiple occurrence of SGF tag') + else: + node[key] = [] + + propertyValueList = [] + while 1: + propValue = [] + while s[i] != ']': + if s[i] == '\t': # convert whitespace to ' ' + propValue.append(' ') + i += 1 + continue + if s[i] == '\\': + i += 1 # ignore escaped characters, throw away backslash + if s[i:i+2] in ['\n\r', '\r\n']: + i += 2 + continue + elif s[i] in ['\n', '\r']: + i += 1 + continue + propValue.append(s[i]) + i += 1 + + if i >= len(s): + raise SGFError('Property value does not end') + + propertyValueList.append(''.join(propValue)) + + i += 1 + + while i < len(s) and s[i] in string.whitespace: + i += 1 + + if i >= len(s) or s[i] != '[': break + else: i += 1 + + if key in ['B', 'W', 'AB', 'AW']: + for N in range(len(propertyValueList)): + en = propertyValueList[N] + if Node.sloppy: + en = en.replace('\n', '') + en = en.replace('\r', '') + if not (len(en) == 2 or (len(en) == 0 and key in ['B', 'W'])): + raise SGFError('') + propertyValueList[N] = en + + node[key].extend(propertyValueList) + + self.data = node + self.parsed = 1 + + +Node.sloppy = 1 + +# ------------------------------------------------------------------------------------ + +class Cursor: + + """ Initialized with an SGF file. Then use game(n); next(n), previous to navigate. + self.collection is list of Nodes, namely of the root nodes of the game trees. + + self.currentN is the current Node + self.currentNode() returns self.currentN.data + + The sloppy option for __init__ determines if the following things, which are not allowed + according to the SGF spec, are accepted nevertheless: + - multiple occurrences of a tag in one node + - line breaks in AB[]/AW[]/B[]/W[] tags (e.g. "B[a\nb]") + """ + + + def __init__(self, sgf, sloppy = 1): + Node.sloppy = sloppy + + self.height = 0 + self.width = 0 + self.posx = 0 + self.posy = 0 + + self.root = Node(None, '', 0) + + self.parse(sgf) + self.currentN = self.root.next + self.setFlags() + + def setFlags(self): + if self.currentN.next: self.atEnd = 0 + else: self.atEnd = 1 + if self.currentN.previous: self.atStart = 0 + else: self.atStart = 1 + + def noChildren(self): + return self.currentN.numChildren + + def currentNode(self): + if not self.currentN.parsed: + self.currentN.parseNode() + return self.currentN.data + + def parse(self, sgf): + + curr = self.root + + p = -1 # start of the currently parsed node + c = [] # list of nodes from which variations started + last = ')' # type of last aprsed bracked ('(' or ')') + inbrackets = 0 # are the currently parsed characters in []'s? + + height_previous = 0 + width_currentVar = 0 + + i = 0 # current parser position + + # skip everything before first (; : + + match = reGameStart.search(sgf, i) + if not match: + raise SGFError('No game found') + + i = match.start() + + while i < len(sgf): + + match = reRelevant.search(sgf, i) + if not match: + break + i = match.end() - 1 + + if inbrackets: + if sgf[i]==']': + numberBackslashes = 0 + j = i-1 + while sgf[j] == '\\': + numberBackslashes += 1 + j -= 1 + if not (numberBackslashes % 2): + inbrackets = 0 + i = i + 1 + continue + + if sgf[i] == '[': + inbrackets = 1 + + if sgf[i] == '(': + if last != ')': # start of first variation of previous node + if p != -1: curr.SGFstring = sgf[p:i] + + nn = Node() + nn.previous = curr + + width_currentVar += 1 + if width_currentVar > self.width: self.width = width_currentVar + + if curr.next: + last = curr.next + while last.down: last = last.down + nn.up = last + last.down = nn + nn.level = last.level + 1 + self.height += 1 + nn.posyD = self.height - height_previous + else: + curr.next = nn + nn.posyD = 0 + height_previous = self.height + + curr.numChildren += 1 + + c.append((curr, width_currentVar-1, self.height)) + + curr = nn + + p = -1 + last = '(' + + if sgf[i] == ')': + if last != ')' and p != -1: + curr.SGFstring = sgf[p:i] + try: + curr, width_currentVar, height_previous = c.pop() + except IndexError: + raise SGFError('Game tree parse error') + last = ')' + + if sgf[i] == ';': + if p != -1: + curr.SGFstring = sgf[p:i] + nn = Node() + nn.previous = curr + width_currentVar += 1 + if width_currentVar > self.width: self.width = width_currentVar + nn.posyD = 0 + curr.next = nn + curr.numChildren = 1 + curr = nn + p = i + + i = i + 1 + + if inbrackets or c: + raise SGFError('Game tree parse error') + + n = curr.next + n.previous = None + n.up = None + + while n.down: + n = n.down + n.previous = None + + + def game(self, n): + if n < self.root.numChildren: + self.posx = 0 + self.posy = 0 + self.currentN = self.root.next + for i in range(n): self.currentN = self.currentN.down + self.setFlags() + else: + raise SGFError('Game not found') + + + def delVariation(self, c): + + if c.previous: + self.delVar(c) + else: + if c.next: + node = c.next + while node.down: + node = node.down + self.delVar(node.up) + + self.delVar(node) + + c.next = None + + self.setFlags() + + + def delVar(self, node): + if node.up: node.up.down = node.down + else: node.previous.next = node.down + + if node.down: + node.down.up = node.up + node.down.posyD = node.posyD + n = node.down + while n: + n.level -= 1 + n = n.down + + h = 0 + n = node + while n.next: + n = n.next + while n.down: + n = n.down + h += n.posyD + + if node.up or node.down: h += 1 + + p = node.previous + p.numChildren -= 1 + + while p: + if p.down: p.down.posyD -= h + p = p.previous + + + self.height -= h + + + + + def add(self, st): + node = Node(self.currentN,st,0) + + node.down = None + node.next = None + node.numChildren = 0 + + if not self.currentN.next: + node.level = 0 + node.posyD = 0 + node.up = 0 + + self.currentN.next = node + self.currentN.numChildren = 1 + else: + n = self.currentN.next + while n.down: + n = n.down + self.posy += n.posyD + + + n.down = node + node.up = n + node.level = n.level + 1 + node.next = None + self.currentN.numChildren += 1 + + node.posyD = 1 + while n.next: + n = n.next + while n.down: + n = n.down + node.posyD += n.posyD + + self.posy += node.posyD + + self.height += 1 + + n = node + while n.previous: + n = n.previous + if n.down: n.down.posyD += 1 + + self.currentN = node + + self.posx += 1 + self.setFlags() + + if self.posx > self.width: self.width += 1 + + + + + + + + def next(self, n=0): + if n >= self.noChildren(): + raise SGFError('Variation not found') + + self.posx += 1 + + self.currentN = self.currentN.next + for i in range(n): + self.currentN = self.currentN.down + self.posy += self.currentN.posyD + self.setFlags() + return self.currentNode() + + def previous(self): + if self.currentN.previous: + while self.currentN.up: + self.posy -= self.currentN.posyD + self.currentN = self.currentN.up + self.currentN = self.currentN.previous + self.posx -= 1 + else: raise SGFError('No previous node') + self.setFlags() + return self.currentNode() + + def getRootNode(self, n): + if not self.root: return + if n >= self.root.numChildren: raise SGFError('Game not found') + + nn = self.root.next + for i in range(n): nn = nn.down + + if not nn.parsed: nn.parseNode() + + return nn.data + + + def updateCurrentNode(self): + """ Put the data in self.currentNode into the corresponding string in self.collection. + This will be called from an application which may have modified self.currentNode.""" + + self.currentN.SGFstring = self.nodeToString(self.currentN.data) + + + + + def updateRootNode(self, data, n=0): + if n >= self.root.numChildren: + raise SGFError('Game not found') + + nn = self.root.next + for i in range(n): nn = nn.down + + nn.SGFstring = self.rootNodeToString(data) + nn.parsed = 0 + nn.parseNode() + + + def rootNodeToString(self, node): + + result = [';'] + keylist = ['GM', 'FF', 'SZ', 'PW', 'WR', 'PB', 'BR', + 'EV', 'RO', 'DT', 'PC', 'KM', 'RE', 'US', 'GC'] + for key in keylist: + if key in node: + result.append(key) + result.append('[' + SGFescape(node[key][0]) + ']\n') + + l = 0 + for key in node.keys(): + if not key in keylist: + result.append(key) + l += len(key) + for item in node[key]: + result.append('[' + SGFescape(item) + ']\n') + l += len(item) + 2 + if l > 72: + result.append('\n') + l = 0 + + return ''.join(result) + + def nodeToString(self, node): + l = 0 + result = [';'] + for k in node.keys(): + if l + len(k) > 72: + result.append('\n') + l = 0 + if not node[k]: continue + result.append(k) + l += len(k) + for item in node[k]: + if l + len(item) > 72: + result.append('\n') + l = 0 + l += len(item) + 2 + result.append('[' + SGFescape(item) + ']') + + return ''.join(result) + + + def outputVar(self, node): + + result = [] + + result.append(node.SGFstring) + + while node.next: + node = node.next + + if node.down: + while node.down: + result.append('(' + self.outputVar(node) + ')' ) + node = node.down + + result.append('(' + self.outputVar(node) + ')' ) + return ''.join(result) + + else: + result.append(node.SGFstring) + + return ''.join(result) + + + + def output(self): + result = [] + + n = self.root.next + + while n: + result.append('(' + self.outputVar(n)+ ')\n') + n = n.down + + return ''.join(result) diff --git a/qml/python/tests/__init__.py b/qml/python/tests/__init__.py new file mode 100644 index 0000000..7847780 --- /dev/null +++ b/qml/python/tests/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + diff --git a/qml/python/tests/test.sgf b/qml/python/tests/test.sgf new file mode 100644 index 0000000..bdbe898 --- /dev/null +++ b/qml/python/tests/test.sgf @@ -0,0 +1,13 @@ +(;GM[1]FF[3] +;AW[ro][nq][oq][pq][qq][rq][mr] +AB[or][pr][qr][rr][sr] + (;W[os] + (;B[ps];W[rs];B[ns];W[nr]) + (;B[ns];W[nr];B[rs];W[ps];B[qs];W[os]) + ) +(;W[rs] + (;B[os];W[qs]) + (;B[qs];W[os];B[nr];W[ns]) +) +) + diff --git a/qml/python/tests/test2.sgf b/qml/python/tests/test2.sgf new file mode 100644 index 0000000..a11356c --- /dev/null +++ b/qml/python/tests/test2.sgf @@ -0,0 +1,6 @@ +(;GM[1]FF[3] +;AW[qn][mp][qp][rp][kq][mq][oq][pq][nr][pr][rs] +AB[nn][mo][np][op][pp][nq][qq][rq][qr][sr][qs] +(;B[or];W[os];B[ps];W[or];B[mr]) +(;B[ps]WV[ps];W[or];B[mr];W[lr]) +) diff --git a/qml/python/tests/test_game.py b/qml/python/tests/test_game.py new file mode 100644 index 0000000..de43909 --- /dev/null +++ b/qml/python/tests/test_game.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import unittest +from python import sgfparser, game + +def create_board(fic): + with open("python/tests/%s" % fic) as f: + cursor = sgfparser.Cursor(f.read()) + + return game.Game(cursor) + +class TestBoard(unittest.TestCase): + """ Test for the board management. + """ + + def testConvCoord(self): + """ Test the conversion in the coordinates system. + """ + + self.assertEqual((1,2),game.Game.conv_coord("ab")) + self.assertEqual((1,1),game.Game.conv_coord("aa")) + + def test_createTree(self): + + def fun(pos): + return pos + + with open("python/tests/test.sgf") as f: + cursor = sgfparser.Cursor(f.read()) + + cursor.next() + + expected_tee = \ + [{'AB': ['or', 'pr', 'qr', 'rr', 'sr'], 'AW': ['ro', 'nq', 'oq', 'pq', 'qq', 'rq', 'mr']}, + [ + [{'W': ['os']}, + [ + [{'B': ['ps']}, {'W': ['rs']}, {'B': ['ns']}, {'W': ['nr']}], + [{'B': ['ns']}, {'W': ['nr']}, {'B': ['rs']}, {'W': ['ps']}, {'B': ['qs']}, {'W': ['os']}] + ] + ], + [{'W': ['rs']}, + [ + [{'B': ['os']}, {'W': ['qs']}], + [{'B': ['qs']}, {'W': ['os']}, {'B': ['nr']}, {'W': ['ns']}] + ] + ] + ] + ] + self.assertEqual(expected_tee, game.Game.create_tree(cursor, fun)) + + def test_init(self): + + def fun(pos): + return (0,0) + currentgame = create_board("test.sgf") + + self.assertEqual(11, currentgame.min_x) + self.assertEqual(13, currentgame.min_y) + self.assertEqual(19, currentgame.max_x) + self.assertEqual(19, currentgame.max_y) + + self.assertEqual((9, 7), currentgame.get_size()) + + # There is only 2 state : initial board, and 2 possibilities. + self.assertEqual(2, len(currentgame.tree)) + + self.assertTrue(currentgame.side['TOP']) + self.assertTrue(currentgame.side['LEFT']) + self.assertFalse(currentgame.side['BOTTOM']) + self.assertFalse(currentgame.side['RIGHT']) + + currentgame.normalize() + + def test_level2(self): + + def fun(pos): + return (0,0) + currentgame = create_board("test2.sgf") + + print(currentgame.tree) + + currentgame.normalize() + + self.assertEqual(11, currentgame.min_x) + self.assertEqual(13, currentgame.min_y) + self.assertEqual(19, currentgame.max_x) + self.assertEqual(19, currentgame.max_y) + + self.assertEqual((9, 7), currentgame.get_size()) + + # There is only 2 state : initial board, and 2 possibilities. + self.assertEqual(2, len(currentgame.tree)) + + self.assertTrue(currentgame.side['TOP']) + self.assertTrue(currentgame.side['LEFT']) + self.assertFalse(currentgame.side['BOTTOM']) + self.assertFalse(currentgame.side['RIGHT']) + + currentgame.normalize() diff --git a/qml/python/tests/test_transformations.py b/qml/python/tests/test_transformations.py new file mode 100644 index 0000000..b802b9f --- /dev/null +++ b/qml/python/tests/test_transformations.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import unittest + +from python.transformations import Rotation, Translation, Symmetry + +class FakeBoard(): + + def __init__(self, min_x, min_y, max_x, max_y): + self.min_x = min_x + self.min_y = min_y + self.max_x = max_x + self.max_y = max_y + + def get_size(self): + x_size = self.max_x - self.min_x + y_size = self.max_y - self.min_y + return min(19, x_size), min(19, y_size) + +class TestRotation(unittest.TestCase): + """ Test the rotation transformation + """ + + def test_apply_points(self): + """ Test the points rotation. + """ + rotation = Rotation(FakeBoard(2, 1, 8, 5)) + self.assertEqual((4, 2), rotation.apply_points((2, 1))) + + rotation = Rotation(FakeBoard(0, 0, 6, 4)) + self.assertEqual((4, 0), rotation.apply_points((0, 0))) + self.assertEqual((4, 3), rotation.apply_points((3, 0))) + self.assertEqual((1, 0), rotation.apply_points((0, 3))) + self.assertEqual((1, 3), rotation.apply_points((3, 3))) + + rotation = Rotation(FakeBoard(1, 1, 6, 4)) + self.assertEqual((4, 1), rotation.apply_points((1, 0))) + + + + def test_get_new_size(self): + """ Test the goban size rotation. + """ + rotation = Rotation(FakeBoard(2, 1, 8, 5)) + self.assertEqual((0, 2, 4, 8), rotation.get_new_size()) + + rotation = Rotation(FakeBoard(0, 0, 6, 4)) + self.assertEqual((0, 0, 4, 6), rotation.get_new_size()) + + def test_is_valid(self): + """ Test the is_valid method. + """ + + # Do not rotate if height > width + rotation = Rotation(FakeBoard(0, 0, 6, 4)) + self.assertTrue(rotation.is_valid()) + + # Always rotate if width > height + rotation = Rotation(FakeBoard(0, 0, 4, 6)) + self.assertFalse(rotation.is_valid()) + + # May rotate if width = height (not tested here…) + +class TestTranslation(unittest.TestCase): + """ Test the translation transformation. + """ + + def test_apply_points(self): + """ Test the points translation. + """ + translation = Translation(FakeBoard(2, 1, 8, 5)) + self.assertEqual((0, 0), translation.apply_points((2, 1))) + self.assertEqual((6, 4), translation.apply_points((8, 5))) + + def test_get_new_size(self): + """ Test the goban size translation. + """ + translation = Translation(FakeBoard(2, 1, 8, 5)) + + self.assertEqual((0, 0, 6, 4), translation.get_new_size()) + + def test_is_valid(self): + """ Test the is_valid method. + """ + + translation = Translation(FakeBoard(1, 0, 6, 4)) + self.assertTrue(translation.is_valid()) + + translation = Translation(FakeBoard(0, 1, 6, 4)) + self.assertTrue(translation.is_valid()) + + translation = Translation(FakeBoard(0, 0, 4, 6)) + self.assertFalse(translation.is_valid()) + +class TestRotation(unittest.TestCase): + """ Test the simetry transformation. + """ + + def test_apply_points(self): + """ Test the points Symmetry. + """ + symmetry = Symmetry(FakeBoard(2, 1, 8, 5)) + symmetry.x_flip = True + symmetry.y_flip = False + self.assertEqual((8, 1), symmetry.apply_points((2, 1))) + self.assertEqual((2, 5), symmetry.apply_points((8, 5))) + + symmetry.x_flip = False + symmetry.y_flip = True + self.assertEqual((2, 5), symmetry.apply_points((2, 1))) + self.assertEqual((8, 1), symmetry.apply_points((8, 5))) + + def test_get_new_size(self): + """ Test the goban size Symmetry. + """ + symmetry = Symmetry(FakeBoard(2, 1, 8, 5)) + + self.assertEqual((2, 1, 8, 5), symmetry.get_new_size()) + + def test_is_valid(self): + """ Test the is_valid method. + """ + + symmetry = Symmetry(FakeBoard(1, 0, 6, 4)) + symmetry.x_flip = True + self.assertTrue(symmetry.is_valid()) + + + symmetry = Symmetry(FakeBoard(0, 0, 4, 6)) + symmetry.x_flip = False + symmetry.y_flip = False + self.assertFalse(symmetry.is_valid()) + diff --git a/qml/python/transformations.py b/qml/python/transformations.py new file mode 100644 index 0000000..380c30c --- /dev/null +++ b/qml/python/transformations.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import random +random.seed() + +class Translation(object): + """ A translation transformation. + """ + + def __init__(self, board): + """ Create a new translation from the board. + """ + self.board = board + + def is_valid(self): + """ Apply if a translation if the goban is not maximized. + """ + return self.board.min_x != 0 or self.board.min_y != 0 + + def apply_points(self, coord): + """ Move the points to the lower position. + """ + x, y = coord + return (x - self.board.min_x, y - self.board.min_y) + + def get_new_size(self): + """ The goban size does not change. + """ + return 0, 0, self.board.max_x - self.board.min_x, self.board.max_y - self.board.min_y + + def get_new_side(self): + """ There is no changes on the sides. + """ + return self.board.side + +class Rotation(object): + """ A rotation transformation. + """ + + def __init__(self, board): + """ Create a new roation from the board. + """ + self.board = board + + def is_valid(self): + """ Apply the rotation in following cases : + * if the board is widther than heigther + * randomly is width == heigth + * never if the board is heigther than widther + """ + + width, heigth = self.board.get_size() + should = heigth - width + if should == 0: + return random.randint(0, 1) == 1 + return should < 0 + + def apply_points(self, coord): + """ Apply the transformation on a point. + """ + x, y = coord + return (self.board.max_y - y, x) + + def get_new_size(self): + """ Apply the transformation on the goban size. + """ + max_x = self.board.max_y - self.board.min_y + return 0, self.board.min_x, max_x, self.board.max_x + + def get_new_side(self): + """ Apply the transformations on the sides. + """ + return { + "TOP": self.board.side["RIGHT"], + "LEFT": self.board.side["TOP"], + "RIGHT": self.board.side["BOTTOM"], + "BOTTOM":self.board.side["LEFT"] + } + +class Symmetry(object): + """ A translation transformation. + """ + + def __init__(self, board): + self.board = board + + self.x_flip = random.randint(0, 1) == 1 + self.y_flip = random.randint(0, 1) == 1 + + def is_valid(self): + """ The transformation is valid if one flip is required in one + direction. """ + return self.x_flip or self.y_flip + + def apply_points(self, coord): + """ Flip in both directions. + """ + x, y = coord + if self.x_flip: + new_x = self.board.max_x - x + self.board.min_x + else: + new_x = x + + if self.y_flip: + new_y = self.board.max_y - y + self.board.min_y + else: + new_y = y + + return (new_x, new_y) + + def get_new_size(self): + """ The size is not changed. + """ + return self.board.min_x, self.board.min_y, self.board.max_x, self.board.max_y + + def get_new_side(self): + """ There is no changes on the sides. + """ + return { + "TOP": self.board.side["BOTTOM" if self.y_flip else "TOP"], + "LEFT": self.board.side["RIGHT" if self.x_flip else "LEFT"], + "RIGHT": self.board.side["LEFT" if self.x_flip else "RIGHT"], + "BOTTOM":self.board.side["TOP" if self.y_flip else "BOTTOM"] + } diff --git a/qml/tsumego.qml b/qml/tsumego.qml new file mode 100644 index 0000000..6b61613 --- /dev/null +++ b/qml/tsumego.qml @@ -0,0 +1,49 @@ +/* + Copyright (C) 2013 Jolla Ltd. + Contact: Thomas Perl + All rights reserved. + + You may use this file under the terms of BSD license as follows: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Jolla Ltd nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "pages" + +ApplicationWindow +{ + + + initialPage: Component { + + id:app + + + Board {id:board} + } + + cover: Qt.resolvedUrl("cover/CoverPage.qml") + +} -- cgit v1.2.3