summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@chimrod.com>2014-08-24 12:52:10 +0200
committerSébastien Dailly <sebastien@chimrod.com>2014-08-24 12:52:10 +0200
commit6c2cc134abf3f32d1d6ec172c6201f8d990c88ab (patch)
tree98b03151505f8fc058977f906e93e9a799b02217
Initial commit
-rw-r--r--.gitignore3
-rw-r--r--qml/actions.js114
-rw-r--r--qml/content/gfx/black.pngbin0 -> 1557 bytes
-rw-r--r--qml/content/gfx/board.pngbin0 -> 78033 bytes
-rw-r--r--qml/content/gfx/pieces.svg139
-rw-r--r--qml/content/gfx/white.pngbin0 -> 1469 bytes
-rw-r--r--qml/content/sgf/easy.sgf299
-rw-r--r--qml/content/sgf/hard.sgf356
-rw-r--r--qml/cover/CoverPage.qml54
-rw-r--r--qml/pages/Board.qml92
-rw-r--r--qml/pages/Goban.qml262
-rw-r--r--qml/pages/Point.qml29
-rw-r--r--qml/python/__init__.py3
-rw-r--r--qml/python/board.py57
-rw-r--r--qml/python/game.py145
-rw-r--r--qml/python/sgfparser.py579
-rw-r--r--qml/python/tests/__init__.py3
-rw-r--r--qml/python/tests/test.sgf13
-rw-r--r--qml/python/tests/test2.sgf6
-rw-r--r--qml/python/tests/test_game.py101
-rw-r--r--qml/python/tests/test_transformations.py134
-rw-r--r--qml/python/transformations.py125
-rw-r--r--qml/tsumego.qml49
-rw-r--r--rpm/qtgo.changes.in15
-rw-r--r--rpm/qtgo.spec71
-rw-r--r--rpm/qtgo.yaml46
-rw-r--r--rpm/tsumego.changes.in15
-rw-r--r--rpm/tsumego.spec71
-rw-r--r--rpm/tsumego.yaml46
-rw-r--r--src/tsumego.cpp51
-rw-r--r--translations/qtgo-de.ts41
-rw-r--r--translations/qtgo.ts37
-rw-r--r--translations/tsumego-de.ts43
-rw-r--r--translations/tsumego.ts43
-rw-r--r--tsumego.desktop10
-rw-r--r--tsumego.pngbin0 -> 2965 bytes
-rw-r--r--tsumego.pro47
37 files changed, 3099 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2966941
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.pyc
+*.swp
+*.pro.user
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
--- /dev/null
+++ b/qml/content/gfx/black.png
Binary files differ
diff --git a/qml/content/gfx/board.png b/qml/content/gfx/board.png
new file mode 100644
index 0000000..2bba363
--- /dev/null
+++ b/qml/content/gfx/board.png
Binary files 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="210mm"
+ height="297mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="pieces.svg">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3773">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3775" />
+ <stop
+ id="stop3781"
+ offset="0.38297874"
+ style="stop-color:#ffffff;stop-opacity:1;" />
+ <stop
+ style="stop-color:#dcdcdc;stop-opacity:1;"
+ offset="1"
+ id="stop3777" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3773"
+ id="radialGradient3779"
+ cx="259.6062"
+ cy="382.87701"
+ fx="259.6062"
+ fy="382.87701"
+ r="232.85715"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3773-6"
+ id="radialGradient3779-3"
+ cx="259.6062"
+ cy="382.87701"
+ fx="259.6062"
+ fy="382.87701"
+ r="232.85715"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3773-6">
+ <stop
+ style="stop-color:#686868;stop-opacity:1;"
+ offset="0"
+ id="stop3775-1" />
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="1"
+ id="stop3777-5" />
+ </linearGradient>
+ <radialGradient
+ r="232.85715"
+ fy="382.87701"
+ fx="259.6062"
+ cy="382.87701"
+ cx="259.6062"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient3818"
+ xlink:href="#linearGradient3773-6"
+ inkscape:collect="always" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.49497475"
+ inkscape:cx="-453.07944"
+ inkscape:cy="670.73625"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1680"
+ inkscape:window-height="995"
+ inkscape:window-x="0"
+ inkscape:window-y="33"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:url(#radialGradient3779);fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path2985"
+ sodipodi:cx="341.42856"
+ sodipodi:cy="473.79074"
+ sodipodi:rx="232.85715"
+ sodipodi:ry="232.85715"
+ d="m 574.28571,473.79074 a 232.85715,232.85715 0 1 1 -465.7143,0 232.85715,232.85715 0 1 1 465.7143,0 z"
+ transform="translate(16.162441,-226.27417)"
+ inkscape:export-filename="/home/sebastien/Projets/sailfish/go/qtgo/qml/content/gfx/white.png"
+ inkscape:export-xdpi="9.6599998"
+ inkscape:export-ydpi="9.6599998" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:url(#radialGradient3818);fill-opacity:1.0;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path2985-6"
+ sodipodi:cx="341.42856"
+ sodipodi:cy="473.79074"
+ sodipodi:rx="232.85715"
+ sodipodi:ry="232.85715"
+ d="m 574.28571,473.79074 a 232.85715,232.85715 0 1 1 -465.7143,0 232.85715,232.85715 0 1 1 465.7143,0 z"
+ transform="translate(40.409104,279.56629)"
+ inkscape:export-filename="/home/sebastien/Projets/sailfish/go/qtgo/qml/content/gfx/black.png"
+ inkscape:export-xdpi="9.6599998"
+ inkscape:export-ydpi="9.6599998" />
+ </g>
+</svg>
diff --git a/qml/content/gfx/white.png b/qml/content/gfx/white.png
new file mode 100644
index 0000000..8de1fab
--- /dev/null
+++ b/qml/content/gfx/white.png
Binary files 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 <thomas.perl@jollamobile.com>
+ 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 <thomas.perl@jollamobile.com>
+ 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")
+
+}
diff --git a/rpm/qtgo.changes.in b/rpm/qtgo.changes.in
new file mode 100644
index 0000000..9e16d7f
--- /dev/null
+++ b/rpm/qtgo.changes.in
@@ -0,0 +1,15 @@
+# Rename this file as qtgo.changes to include changelog
+# entries in your RPM file.
+#
+# Add new changelog entries following the format below.
+# Add newest entries to the top of the list.
+# Separate entries from eachother with a blank line.
+
+# * date Author's Name <author's email> version-release
+# - Summary of changes
+
+* Sun Apr 13 2014 Jack Tar <jack.tar@example.com> 0.0.1-1
+- Scrubbed the deck
+- Hoisted the sails
+
+
diff --git a/rpm/qtgo.spec b/rpm/qtgo.spec
new file mode 100644
index 0000000..38a1466
--- /dev/null
+++ b/rpm/qtgo.spec
@@ -0,0 +1,71 @@
+#
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.27
+#
+
+Name: qtgo
+
+# >> macros
+# << macros
+
+%{!?qtc_qmake:%define qtc_qmake %qmake}
+%{!?qtc_qmake5:%define qtc_qmake5 %qmake5}
+%{!?qtc_make:%define qtc_make make}
+%{?qtc_builddir:%define _builddir %qtc_builddir}
+Summary: Tsumego game
+Version: 0.1
+Release: 1
+Group: Qt/Qt
+License: LICENSE
+URL: http://example.org/
+Source0: %{name}-%{version}.tar.bz2
+Source100: qtgo.yaml
+Requires: sailfishsilica-qt5 >= 0.10.9
+BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
+BuildRequires: pkgconfig(Qt5Core)
+BuildRequires: pkgconfig(Qt5Qml)
+BuildRequires: pkgconfig(Qt5Quick)
+BuildRequires: desktop-file-utils
+
+%description
+Short description of my SailfishOS Application
+
+
+%prep
+%setup -q -n %{name}-%{version}
+
+# >> setup
+# << setup
+
+%build
+# >> build pre
+# << build pre
+
+%qtc_qmake5
+
+%qtc_make %{?_smp_mflags}
+
+# >> build post
+# << build post
+
+%install
+rm -rf %{buildroot}
+# >> install pre
+# << install pre
+%qmake5_install
+
+# >> install post
+# << install post
+
+desktop-file-install --delete-original \
+ --dir %{buildroot}%{_datadir}/applications \
+ %{buildroot}%{_datadir}/applications/*.desktop
+
+%files
+%defattr(-,root,root,-)
+%{_bindir}
+%{_datadir}/%{name}
+%{_datadir}/applications/%{name}.desktop
+%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
+# >> files
+# << files
diff --git a/rpm/qtgo.yaml b/rpm/qtgo.yaml
new file mode 100644
index 0000000..d86fe1a
--- /dev/null
+++ b/rpm/qtgo.yaml
@@ -0,0 +1,46 @@
+Name: qtgo
+Summary: Tsumego game
+Version: 0.1
+Release: 1
+# The contents of the Group field should be one of the groups listed here:
+# http://gitorious.org/meego-developer-tools/spectacle/blobs/master/data/GROUPS
+Group: Qt/Qt
+URL: http://example.org/
+License: LICENSE
+# This must be generated before uploading a package to a remote build service.
+# Usually this line does not need to be modified.
+Sources:
+- '%{name}-%{version}.tar.bz2'
+Description: |
+ Short description of my SailfishOS Application
+Configure: none
+# The qtc5 builder inserts macros to allow QtCreator to have fine
+# control over qmake/make execution
+Builder: qtc5
+
+# This section specifies build dependencies that are resolved using pkgconfig.
+# This is the preferred way of specifying build dependencies for your package.
+PkgConfigBR:
+ - sailfishapp >= 1.0.2
+ - Qt5Core
+ - Qt5Qml
+ - Qt5Quick
+
+# Build dependencies without a pkgconfig setup can be listed here
+# PkgBR:
+# - package-needed-to-build
+
+# Runtime dependencies which are not automatically detected
+Requires:
+ - sailfishsilica-qt5 >= 0.10.9
+
+# All installed files
+Files:
+ - '%{_bindir}'
+ - '%{_datadir}/%{name}'
+ - '%{_datadir}/applications/%{name}.desktop'
+ - '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png'
+
+# For more information about yaml and what's supported in Sailfish OS
+# build system, please see https://wiki.merproject.org/wiki/Spectacle
+
diff --git a/rpm/tsumego.changes.in b/rpm/tsumego.changes.in
new file mode 100644
index 0000000..add0453
--- /dev/null
+++ b/rpm/tsumego.changes.in
@@ -0,0 +1,15 @@
+# Rename this file as tsumego.changes to include changelog
+# entries in your RPM file.
+#
+# Add new changelog entries following the format below.
+# Add newest entries to the top of the list.
+# Separate entries from eachother with a blank line.
+
+# * date Author's Name <author's email> version-release
+# - Summary of changes
+
+* Sun Apr 13 2014 Jack Tar <jack.tar@example.com> 0.0.1-1
+- Scrubbed the deck
+- Hoisted the sails
+
+
diff --git a/rpm/tsumego.spec b/rpm/tsumego.spec
new file mode 100644
index 0000000..a29c727
--- /dev/null
+++ b/rpm/tsumego.spec
@@ -0,0 +1,71 @@
+#
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.27
+#
+
+Name: tsumego
+
+# >> macros
+# << macros
+
+%{!?qtc_qmake:%define qtc_qmake %qmake}
+%{!?qtc_qmake5:%define qtc_qmake5 %qmake5}
+%{!?qtc_make:%define qtc_make make}
+%{?qtc_builddir:%define _builddir %qtc_builddir}
+Summary: Tsumego game
+Version: 0.1
+Release: 1
+Group: Qt/Qt
+License: LICENSE
+URL: http://example.org/
+Source0: %{name}-%{version}.tar.bz2
+Source100: tsumego.yaml
+Requires: sailfishsilica-qt5 >= 0.10.9
+BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
+BuildRequires: pkgconfig(Qt5Core)
+BuildRequires: pkgconfig(Qt5Qml)
+BuildRequires: pkgconfig(Qt5Quick)
+BuildRequires: desktop-file-utils
+
+%description
+Short description of my SailfishOS Application
+
+
+%prep
+%setup -q -n %{name}-%{version}
+
+# >> setup
+# << setup
+
+%build
+# >> build pre
+# << build pre
+
+%qtc_qmake5
+
+%qtc_make %{?_smp_mflags}
+
+# >> build post
+# << build post
+
+%install
+rm -rf %{buildroot}
+# >> install pre
+# << install pre
+%qmake5_install
+
+# >> install post
+# << install post
+
+desktop-file-install --delete-original \
+ --dir %{buildroot}%{_datadir}/applications \
+ %{buildroot}%{_datadir}/applications/*.desktop
+
+%files
+%defattr(-,root,root,-)
+%{_bindir}
+%{_datadir}/%{name}
+%{_datadir}/applications/%{name}.desktop
+%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
+# >> files
+# << files
diff --git a/rpm/tsumego.yaml b/rpm/tsumego.yaml
new file mode 100644
index 0000000..bd6a4ae
--- /dev/null
+++ b/rpm/tsumego.yaml
@@ -0,0 +1,46 @@
+Name: tsumego
+Summary: Tsumego game
+Version: 0.1
+Release: 1
+# The contents of the Group field should be one of the groups listed here:
+# http://gitorious.org/meego-developer-tools/spectacle/blobs/master/data/GROUPS
+Group: Qt/Qt
+URL: http://example.org/
+License: LICENSE
+# This must be generated before uploading a package to a remote build service.
+# Usually this line does not need to be modified.
+Sources:
+- '%{name}-%{version}.tar.bz2'
+Description: |
+ Short description of my SailfishOS Application
+Configure: none
+# The qtc5 builder inserts macros to allow QtCreator to have fine
+# control over qmake/make execution
+Builder: qtc5
+
+# This section specifies build dependencies that are resolved using pkgconfig.
+# This is the preferred way of specifying build dependencies for your package.
+PkgConfigBR:
+ - sailfishapp >= 1.0.2
+ - Qt5Core
+ - Qt5Qml
+ - Qt5Quick
+
+# Build dependencies without a pkgconfig setup can be listed here
+# PkgBR:
+# - package-needed-to-build
+
+# Runtime dependencies which are not automatically detected
+Requires:
+ - sailfishsilica-qt5 >= 0.10.9
+
+# All installed files
+Files:
+ - '%{_bindir}'
+ - '%{_datadir}/%{name}'
+ - '%{_datadir}/applications/%{name}.desktop'
+ - '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png'
+
+# For more information about yaml and what's supported in Sailfish OS
+# build system, please see https://wiki.merproject.org/wiki/Spectacle
+
diff --git a/src/tsumego.cpp b/src/tsumego.cpp
new file mode 100644
index 0000000..dfbb6d6
--- /dev/null
+++ b/src/tsumego.cpp
@@ -0,0 +1,51 @@
+/*
+ Copyright (C) 2013 Jolla Ltd.
+ Contact: Thomas Perl <thomas.perl@jollamobile.com>
+ 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.
+*/
+
+#ifdef QT_QML_DEBUG
+#include <QtQuick>
+#endif
+
+#include <sailfishapp.h>
+
+
+int main(int argc, char *argv[])
+{
+ // SailfishApp::main() will display "qml/template.qml", if you need more
+ // control over initialization, you can use:
+ //
+ // - SailfishApp::application(int, char *[]) to get the QGuiApplication *
+ // - SailfishApp::createView() to get a new QQuickView * instance
+ // - SailfishApp::pathTo(QString) to get a QUrl to a resource file
+ //
+ // To display the view, call "show()" (will show fullscreen on device).
+
+ return SailfishApp::main(argc, argv);
+}
+
diff --git a/translations/qtgo-de.ts b/translations/qtgo-de.ts
new file mode 100644
index 0000000..6b95304
--- /dev/null
+++ b/translations/qtgo-de.ts
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.0">
+<context>
+ <name>CoverPage</name>
+ <message>
+ <source>My Cover</source>
+ <translation>Mein Cover</translation>
+ </message>
+</context>
+<context>
+ <name>FirstPage</name>
+ <message>
+ <source>Show Page 2</source>
+ <translation type="obsolete">Zur Seite 2</translation>
+ </message>
+ <message>
+ <source>UI Template</source>
+ <translation>UI-Vorlage</translation>
+ </message>
+ <message>
+ <source>Hello Sailors</source>
+ <translation>Hallo Matrosen</translation>
+ </message>
+ <message>
+ <source>Board</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SecondPage</name>
+ <message>
+ <source>Nested Page</source>
+ <translation>Unterseite</translation>
+ </message>
+ <message>
+ <source>Item</source>
+ <translation>Element</translation>
+ </message>
+</context>
+</TS>
diff --git a/translations/qtgo.ts b/translations/qtgo.ts
new file mode 100644
index 0000000..79b0928
--- /dev/null
+++ b/translations/qtgo.ts
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.0">
+<context>
+ <name>CoverPage</name>
+ <message>
+ <source>My Cover</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FirstPage</name>
+ <message>
+ <source>UI Template</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Hello Sailors</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Board</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SecondPage</name>
+ <message>
+ <source>Nested Page</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Item</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/translations/tsumego-de.ts b/translations/tsumego-de.ts
new file mode 100644
index 0000000..ba4e6f9
--- /dev/null
+++ b/translations/tsumego-de.ts
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.0">
+<context>
+ <name>CoverPage</name>
+ <message>
+ <location filename="../qml/cover/CoverPage.qml" line="38"/>
+ <source>My Cover</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FirstPage</name>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="45"/>
+ <source>Board</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="61"/>
+ <source>UI Template</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="65"/>
+ <source>Hello Sailors</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SecondPage</name>
+ <message>
+ <location filename="../qml/pages/SecondPage.qml" line="42"/>
+ <source>Nested Page</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/SecondPage.qml" line="49"/>
+ <source>Item</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/translations/tsumego.ts b/translations/tsumego.ts
new file mode 100644
index 0000000..ba4e6f9
--- /dev/null
+++ b/translations/tsumego.ts
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.0">
+<context>
+ <name>CoverPage</name>
+ <message>
+ <location filename="../qml/cover/CoverPage.qml" line="38"/>
+ <source>My Cover</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FirstPage</name>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="45"/>
+ <source>Board</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="61"/>
+ <source>UI Template</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/FirstPage.qml" line="65"/>
+ <source>Hello Sailors</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SecondPage</name>
+ <message>
+ <location filename="../qml/pages/SecondPage.qml" line="42"/>
+ <source>Nested Page</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../qml/pages/SecondPage.qml" line="49"/>
+ <source>Item</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tsumego.desktop b/tsumego.desktop
new file mode 100644
index 0000000..6086437
--- /dev/null
+++ b/tsumego.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+X-Nemo-Application-Type=silica-qt5
+Icon=tsumego
+Exec=tsumego
+Name=tsumego
+# translation example:
+# your app name in French locale (fr)
+Name[fr]=tsumego
+
diff --git a/tsumego.png b/tsumego.png
new file mode 100644
index 0000000..4c883a0
--- /dev/null
+++ b/tsumego.png
Binary files differ
diff --git a/tsumego.pro b/tsumego.pro
new file mode 100644
index 0000000..e063572
--- /dev/null
+++ b/tsumego.pro
@@ -0,0 +1,47 @@
+# NOTICE:
+#
+# Application name defined in TARGET has a corresponding QML filename.
+# If name defined in TARGET is changed, the following needs to be done
+# to match new name:
+# - corresponding QML filename must be changed
+# - desktop icon filename must be changed
+# - desktop filename must be changed
+# - icon definition filename in desktop file must be changed
+# - translation filenames have to be changed
+
+# The name of your application
+TARGET = tsumego
+
+CONFIG += sailfishapp
+
+SOURCES += src/tsumego.cpp
+
+OTHER_FILES += qml/tsumego.qml \
+ qml/cover/CoverPage.qml \
+ qml/pages/FirstPage.qml \
+ qml/content/gfx/*.png \
+ rpm/tsumego.changes.in \
+ rpm/tsumego.spec \
+ rpm/tsumego.yaml \
+ translations/*.ts \
+ tsumego.desktop \
+ qml/pages/Board.qml \
+ qml/python/board.py \
+ qml/python/sgfparser.py \
+ qml/python/goban/sgfparser.py \
+ qml/python/goban/game.py \
+ qml/python/goban/board.py \
+ qml/actions.js \
+ qml/pages/Point.qml \
+ qml/python/transformations.py \
+ qml/python/__init__.py \
+ qml/python/game.py \
+ qml/pages/Goban.qml \
+ qml/content/sgf/hard.sgf \
+ qml/content/sgf/easy.sgf
+
+# to disable building translations every time, comment out the
+# following CONFIG line
+CONFIG += sailfishapp_i18n
+TRANSLATIONS += translations/tsumego-de.ts
+