diff --git a/doc/pieces_states.drawio b/doc/pieces_states.drawio
new file mode 100644
index 0000000000000000000000000000000000000000..6ce0fb5e1c55bfe4573b331a1d7be491856a6633
--- /dev/null
+++ b/doc/pieces_states.drawio
@@ -0,0 +1 @@
+<mxfile host="app.diagrams.net" modified="2021-10-01T17:00:24.120Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.83 Safari/537.36" etag="X7S_uXiCLWks2JBqEszE" version="15.4.1" type="device"><diagram name="Page-1" id="58cdce13-f638-feb5-8d6f-7d28b1aa9fa0">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</diagram></mxfile>
\ No newline at end of file
diff --git a/doc/pieces_states.drawio.svg b/doc/pieces_states.drawio.svg
new file mode 100644
index 0000000000000000000000000000000000000000..120cd2f06f7036f3503e2ef4720c91fcd103bbbc
--- /dev/null
+++ b/doc/pieces_states.drawio.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Do not edit this file with editors other than diagrams.net -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="501px" height="362px" viewBox="-0.5 -0.5 501 362" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2021-10-01T17:01:49.302Z&quot; agent=&quot;5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.83 Safari/537.36&quot; etag=&quot;2gnUP50G2iihb7g0QLF6&quot; version=&quot;15.4.1&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;58cdce13-f638-feb5-8d6f-7d28b1aa9fa0&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);"><defs/><g><ellipse cx="15" cy="70" rx="11" ry="11" fill="#000000" stroke="#ff0000" transform="rotate(90,15,70)" pointer-events="all"/><path d="M 215 70 L 393.63 70" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 398.88 70 L 391.88 73.5 L 393.63 70 L 391.88 66.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 70px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="231" y="74" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><path d="M 30 70 L 158.63 70" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 163.88 70 L 156.88 73.5 L 158.63 70 L 156.88 66.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 70px; margin-left: 98px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Init</div></div></div></foreignObject><text x="98" y="74" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">Init</text></switch></g><ellipse cx="190" cy="70" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 70px; margin-left: 166px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S0</div></div></div></foreignObject><text x="190" y="74" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S0</text></switch></g><ellipse cx="425" cy="70" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 70px; margin-left: 401px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S2</div></div></div></foreignObject><text x="425" y="74" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S2</text></switch></g><path d="M 190 95 L 190 173.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 190 178.88 L 186.5 171.88 L 190 173.63 L 193.5 171.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 110px; margin-left: 190px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="190" y="113" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><ellipse cx="425" cy="205" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 205px; margin-left: 401px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S4</div></div></div></foreignObject><text x="425" y="209" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S4</text></switch></g><ellipse cx="315" cy="295" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 295px; margin-left: 291px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S3</div></div></div></foreignObject><text x="315" y="299" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S3</text></switch></g><path d="M 207.68 87.68 L 397.39 190.1" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 402.01 192.6 L 394.19 192.35 L 397.39 190.1 L 397.52 186.19 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 105px; margin-left: 224px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF</div></div></div></foreignObject><text x="224" y="108" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF</text></switch></g><path d="M 425 95 L 425 173.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 425 178.88 L 421.5 171.88 L 425 173.63 L 428.5 171.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 110px; margin-left: 425px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF</div></div></div></foreignObject><text x="425" y="113" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF</text></switch></g><path d="M 403.33 82.46 L 217.2 189.38" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 212.65 192 L 216.98 185.48 L 217.2 189.38 L 220.46 191.55 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 96px; margin-left: 380px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="380" y="99" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 334.37 279.2 L 400.72 224.87" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 404.79 221.54 L 401.59 228.68 L 400.72 224.87 L 397.15 223.27 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 270px; margin-left: 350px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF<font color="#ff0000">/1</font></div></div></div></foreignObject><text x="350" y="273" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF/1</text></switch></g><path d="M 440 50 Q 440 0 425 0 Q 410 0 410 43.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 410 48.88 L 406.5 41.88 L 410 43.63 L 413.5 41.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 34px; margin-left: 441px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="441" y="37" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><ellipse cx="190" cy="205" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 205px; margin-left: 166px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S1</div></div></div></foreignObject><text x="190" y="209" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S1</text></switch></g><path d="M 170 190 Q 120 190 120 205 Q 120 220 163.63 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 168.88 220 L 161.88 223.5 L 163.63 220 L 161.88 216.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 189px; margin-left: 154px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="154" y="193" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 300 315 Q 300 360 315 360 Q 330 360 330 321.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 330 316.12 L 333.5 323.12 L 330 321.37 L 326.5 323.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 330px; margin-left: 299px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="299" y="334" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><path d="M 291.65 286.07 Q 250 270 235 260 Q 220 250 207.44 231.07" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 204.54 226.7 L 211.32 230.6 L 207.44 231.07 L 205.49 234.47 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 282px; margin-left: 276px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">ON</font></div></div></div></foreignObject><text x="276" y="286" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 445 190 Q 500 190 500 205 Q 500 220 451.37 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 446.12 220 L 453.12 216.5 L 451.37 220 L 453.12 223.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 470px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF,U</div></div></div></foreignObject><text x="470" y="195" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF,U</text></switch></g><path d="M 400.63 210.56 Q 360 220 325 220 Q 290 220 221.03 209.6" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 215.84 208.82 L 223.28 206.4 L 221.03 209.6 L 222.24 213.32 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 215px; margin-left: 384px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="384" y="218" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 214.67 200.95 Q 280 190 315 190 Q 350 190 394.23 198.9" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 399.38 199.94 L 391.82 201.99 L 394.23 198.9 L 393.21 195.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 198px; margin-left: 234px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">OFF</font>/1</div></div></div></foreignObject><text x="234" y="201" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF/1</text></switch></g><path d="M 212.38 216.13 Q 280 250 295.75 270.23" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 298.98 274.37 L 291.92 271 L 295.75 270.23 L 297.44 266.7 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 231px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">U</font></div></div></div></foreignObject><text x="231" y="234" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg>
\ No newline at end of file
diff --git a/memory_lib/__init__.py b/memory_lib/__init__.py
index 106adc018de1241a8e58914a4346b2c338cbdd2b..bf6ae896df9a13a83cf2eda8a2d4b81a5fef2201 100644
--- a/memory_lib/__init__.py
+++ b/memory_lib/__init__.py
@@ -1 +1 @@
-from .memory import MemoryABM, MemoryAruco
+from .memory import MemoryABM, MemoryArucoHalf, MemoryArucoFull
diff --git a/memory_lib/board.py b/memory_lib/board.py
index b44294b70aff05c9f0e50336e7a1f35de9d49045..70b4e7288b4f4b59a6535430145a419eeb9dba8e 100644
--- a/memory_lib/board.py
+++ b/memory_lib/board.py
@@ -1,441 +1,75 @@
-import cv2 as cv
-import numpy as np
-
-from abc import ABC, abstractmethod
+import sys
 from typing import Tuple, Optional, List, Callable
-from .cv_utils import (
-    draw_line,
-    draw_point,
-    average_color,
-    get_roi_around_point,
-    MotionDetector,
-)
-from .geometry import (
-    get_segment_size,
-    line_eq_from_points,
-    get_parallel_eq,
-    get_line_intersection_point,
-    get_rect_center,
-    get_segment_center,
-    get_segement_point_at_dist,
-)
-from .model import Board, Point, LineEq, Piece, Rect
-
-
-class BoardFactory(ABC):
-    @abstractmethod
-    def get_board(self) -> Board:
-        ...
-
-
-class ABMExtractor(ABC):
-    @abstractmethod
-    def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]:
-        ...
-
-
-class PieceVisibilityDetector(ABC):
-    @abstractmethod
-    def is_visible(self, img, piece_pos: Point, piece_index) -> bool:
-        ...
-
-    @abstractmethod
-    def train(self, board: Board, img: np.ndarray) -> None:
-        ...
-
-
-class ABMBoardFactory(BoardFactory):
-    def __init__(self, a: Point, b: Point, m: Point, img: np.ndarray):
-        self.a = a
-        self.b = b
-        self.m = m
-        # TODO: no need img here, only for debug
-        self.img = img
+from .model import Board, Point, LineEq, Piece, Rect, PieceState
+from .piece_state import PiecesStatesTracker
 
-    def get_board(self) -> Board:
-        """ Create a Board object from from A, B and M borad points """
-        a = self.a
-        b = self.b
-        m = self.m
 
-        # F is [AB] center
-        f = Point((a.x + b.x) // 2, (a.y + b.y) // 2)
-        ab_eq = line_eq_from_points(a, b)
+class PieceTakenTrigger:
+    """Call 'trigger' function when after a piece is taken"""
 
-        # TODO: check
-        # if ab_m == math.nan or ab_b == math.nan:
-        #     return
-
-        # TODO: debug only
-        # draw_line(self.img, ab_eq)
-
-        # FM line
-        fm_eq = line_eq_from_points(f, m)
-
-        # TODO: check
-        # if fm_m == math.inf or fm_b == math.inf:
-        #     return
-
-        # TODO: debug only
-        # draw_line(self.img, fm_eq)
-        fm_s = get_segment_size(f, m)
-
-        # Approximate E, based on FM size.
-        # First approximate DC line, based on FM size
-        dc_eq = get_parallel_eq(ab_eq, Point(0, int(a.y - fm_s * 1.5)))
-
-        # TODO: debug only
-        # draw_line(self.img, dc_eq)
-
-        # Then e is on DC, FM intersection
-        e = get_line_intersection_point(fm_eq, dc_eq)
+    def __init__(self, trigger: Callable[[int], None]):
+        self.trigger = trigger
+        self.averager = BoardStateAverager()
+        self.piece_state_tracker = PiecesStatesTracker(trigger)
 
-        # Compute the 4 parallels: A1B1, A2B2, A3B3, A4B4
-        a1, b1 = self._compute_parallel(a, b, ab_eq, e, f, 0.75, 1 * (fm_s / 10))
-        a2, b2 = self._compute_parallel(a, b, ab_eq, e, f, 0.48, 3 * (fm_s / 10))
-        a3, b3 = self._compute_parallel(a, b, ab_eq, e, f, 0.25, 4.5 * (fm_s / 10))
-        a4, b4 = self._compute_parallel(a, b, ab_eq, e, f, 0.07, 6 * (fm_s / 10))
+    def add_board(self, board: Board) -> Board:
+        new_board = self.averager.add_board(board)
+        self.piece_state_tracker.update(new_board)
+        return new_board
 
-        # Compoute the pieces position from the above lines
-        pieces = (
-            *self._compute_piece(a1, b1),
-            *self._compute_piece(a2, b2),
-            *self._compute_piece(a3, b3),
-            *self._compute_piece(a4, b4),
-        )
 
-        # TODO: debug only
-        debut_print = (
-            (a, "A"),
-            (b, "B"),
-            (f, "F"),
-            (e, "E"),
-            (a1, "A1"),
-            (b1, "B1"),
-            (a2, "A2"),
-            (b2, "B2"),
-            (a3, "A3"),
-            (b3, "B3"),
-            (a4, "A4"),
-            (b4, "B4"),
-        )
-        # for point, name in debut_print:
-        #     draw_point(self.img, point, name, (0, 255, 0))
-        # for idx, piece in enumerate(pieces):
-        #     draw_point(img, piece, f"P{idx}", (0, 0, 255))
-        # cv.imshow("IMG", img)
+class BoardStateAverager:
+    """Moving average for board states"""
+    def __init__(self, window_size: int = 15) -> None:
+        self.window_size = window_size
+        self.last_boards: List[Board] = []
 
-        return Board(pieces)
+    def add_board(self, board: Board) -> Board:
+        if len(self.last_boards) > self.window_size:
+            self.last_boards.pop(0)
+        self.last_boards.append(board)
+        states_by_pieces = self._states_by_pieces(self.last_boards)
+        average_state_by_pieces = [self._average_state(states) for states in states_by_pieces]
+        out_board = Board.from_board(board)
+        for idx, _ in enumerate(out_board.pieces):
+            out_board.pieces[idx].state = average_state_by_pieces[idx]
+        return out_board
 
     @staticmethod
-    def _compute_parallel(
-        a: Point,
-        b: Point,
-        ab_eq: LineEq,
-        e: Point,
-        f: Point,
-        height_reduce_ratio: float,
-        width_reduce_ratio: float,
-    ) -> Tuple[Point, Point]:
-        """ used to find A1B1, A2B2, A3B3, A4B4 from AB and EF"""
-        m1 = Point(
-            int(e.x + height_reduce_ratio * (f.x - e.x)),
-            int(e.y + height_reduce_ratio * (f.y - e.y)),
-        )
-        a1b1_eq = get_parallel_eq(ab_eq, m1)
-        left_point = Point(
-            int(a.x + width_reduce_ratio),
-            int(a1b1_eq.m * (a.x + width_reduce_ratio) + a1b1_eq.b),
-        )
-        right_point = Point(
-            int(b.x - width_reduce_ratio),
-            int(a1b1_eq.m * (b.x - width_reduce_ratio) + a1b1_eq.b),
-        )
-        return left_point, right_point
+    def _average_state(states: List[PieceState]) -> PieceState:
+        """Get the most present state in a list"""
+        u = states.count(PieceState.UNKNOWN), PieceState.UNKNOWN
+        on = states.count(PieceState.ON_BOARD), PieceState.ON_BOARD
+        off = states.count(PieceState.OFF_BOARD), PieceState.OFF_BOARD
+        return max(u, on, off, key=lambda t: t[0])[1]
 
     @staticmethod
-    def _compute_piece(left_point: Point, right_point: Point) -> Tuple[Piece, ...]:
-        """ find the 4 pieces from a horizontal line """
-        return (
-            Piece(
-                Point(
-                    int(left_point.x + 0.1 * (right_point.x - left_point.x)),
-                    int(left_point.y + 0.1 * (right_point.y - left_point.y)),
-                )
-            ),
-            Piece(
-                Point(
-                    int(left_point.x + 0.35 * (right_point.x - left_point.x)),
-                    int(left_point.y + 0.35 * (right_point.y - left_point.y)),
-                )
-            ),
-            Piece(
-                Point(
-                    int(left_point.x + 0.65 * (right_point.x - left_point.x)),
-                    int(left_point.y + 0.65 * (right_point.y - left_point.y)),
-                )
-            ),
-            Piece(
-                Point(
-                    int(left_point.x + 0.9 * (right_point.x - left_point.x)),
-                    int(left_point.y + 0.9 * (right_point.y - left_point.y)),
-                )
-            ),
-        )
-
-
-class ArucoABMExtractor(ABMExtractor):
-    """Uses Aruco marker to extract the board A, B and M points
-
-    Aruco marker ids:
-    0 for point A
-    1 for point B
-    2 for point M
-    """
-
-    def __init__(self):
-        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_50)
-        self.aruco_params = cv.aruco.DetectorParameters_create()
-
-    def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]:
-        """ If all 3 markers are found, returns the 3 Points A, B and M """
-        corners, ids, rejected = cv.aruco.detectMarkers(
-            img, self.aruco_dict, parameters=self.aruco_params
-        )
-
-        # TODO: debug
-        # cv.aruco.drawDetectedMarkers(img, corners)
-
-        markers = {}
-        if ids is None:
-            return None
-
-        for i in range(len(ids)):
-            rect_val = ((int(x), int(y)) for x, y in corners[i][0].tolist())
-            rect = Rect.from_corners(*(Point.from_tuple(t) for t in rect_val))
-            markers[ids[i][0]] = get_rect_center(rect)
-
-        a = markers.get(0)
-        b = markers.get(1)
-        m = markers.get(2)
-
-        if a and b and m and (a != b):
-            return a, b, m
-        return None
-
-
-class LightnessPieceVisibilityDetector(PieceVisibilityDetector):
-    """ Primitive detector based on lightness of average pixels around the piece"""
-
-    def __init__(self, threshold=80):
-        self.threshold = threshold
-
-    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index) -> bool:
-        roi = get_roi_around_point(img, piece_pos, 3)
-        if roi.shape != (6, 6, 3):
-            return False
-        avg_color = average_color(roi)
-        return avg_color[2] > self.threshold
-
-    def train(self, board: Board, img: np.ndarray) -> None:
-        ...
-
-
-class AverageColorPieceVisibilityDetector(PieceVisibilityDetector):
-    """ Piece detector based on average pixels color around the piece"""
-
-    def __init__(self):
-        self.pieces_colors: List[Tuple[int, int, int]] = []
-        self.board_color: Tuple[int, int, int] = (0, 0, 0)
-
-    def train(self, board: Board, img: np.ndarray) -> None:
-        """ Store an avraged color value for every pieces in the board """
-        if not self.pieces_colors:
-            self.pieces_colors = [
-                average_color(get_roi_around_point(img, piece.postion))
-                for piece in board.pieces
-            ]
-            # get the socle color from a point below M
-            middle = get_segment_center(
-                board.pieces[5].postion, board.pieces[6].postion
-            )
-            self.board_color = average_color(get_roi_around_point(img, middle))
-        else:
-            for idx, piece in enumerate(board.pieces):
-                c1 = self.pieces_colors[idx]
-                c2 = average_color(get_roi_around_point(img, piece.postion))
-                self.pieces_colors[idx] = self._avg_colors(c1, c2)
-            middle = get_segment_center(
-                board.pieces[5].postion, board.pieces[6].postion
-            )
-            self.board_color = self._avg_colors(
-                self.board_color, average_color(get_roi_around_point(img, middle))
-            )
-
-    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool:
-        # naive color distance detection, TODO: uses device independent color spaces? in_range?
-
-        # Test if piece color is far from piece and close to board_color
-        c1 = self.pieces_colors[piece_index]
-        c2 = average_color(get_roi_around_point(img, piece_pos))
-        m1 = self.board_color
-        m2 = average_color(get_roi_around_point(img, piece_pos))
-        return (
-            abs(m1[0] - m2[0]) < 100
-            and abs(m1[1] - m2[1]) < 100
-            and abs(m1[2] - m2[2]) < 100
-        ) and (
-            abs(c1[0] - c2[0]) > 60
-            or abs(c1[1] - c2[1]) > 60
-            or abs(c1[2] - c2[2]) > 60
-        )
-
-    @staticmethod
-    def _avg_colors(
-        c1: Tuple[int, int, int], c2: Tuple[int, int, int]
-    ) -> Tuple[int, int, int]:
-        return (c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2, (c1[2] + c2[2]) // 2
-
-
-class AverageColorAndMovementPieceVisibilityDetector(PieceVisibilityDetector):
-    """ Piece detector based on average pixels color around the piece, compute only no moving objects"""
-
-    def __init__(self, same_frame_counter_max: int = 5):
-        """
-        :param same_frame_counter_max: how many different frames before considering a movement
+    def _states_by_pieces(boards: List[Board]) -> list[list[PieceState]]:
+        # TODO: Improve perf by storing directly the states, not the boards in the fifo
+        """For every pieces, get the last X states
+
+        res = [
+            [...state_at_t-3, state_at_t-2, state_at_t-1], # piece 0
+            [...state_at_t-3, state_at_t-2, state_at_t-1], # piece 1
+            ...
+        ]
         """
-        self.average_color_detector = AverageColorPieceVisibilityDetector()
-        self.same_frame_counter_max = same_frame_counter_max
-        self.motion_detectors = [MotionDetector() for _ in range(16)]
-
-    def train(self, board: Board, img: np.ndarray) -> None:
-        self.average_color_detector.train(board, img)
-
-    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool:
-        roi = cv.cvtColor(
-            get_roi_around_point(img, piece_pos, radius=10), cv.COLOR_BGR2GRAY
-        )
-
-        # TODO: debug only
-        # if piece_index == 5:
-        #     print(self.motion_detectors[piece_index].same_frame_counter)
-        #     cv.imshow("roi", roi)
-        #     if (self.motion_detectors[piece_index]._last_frame is not None):
-        #         cv.imshow("last", self.motion_detectors[piece_index]._last_frame)
-
-        self.motion_detectors[piece_index].add(roi)
-        if (
-            self.motion_detectors[piece_index].same_frame_counter
-            > self.same_frame_counter_max
-        ):
-            return self.average_color_detector.is_visible(img, piece_pos, piece_index)
-        return False
-
-
-class ArucoPieceVisibilityDetector:
-    """ Piece detector based Aruco marker
-
-    TODO:
-    for now this detector does not work as a PieceVisibilityDetector
-    This detector directly send a board with updated visibility
-    """
-
-    def __init__(self):
-        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
-        self.aruco_params = cv.aruco.DetectorParameters_create()
-        self.top_left: Point = Point(-1, -1)
-        self.top_right: Point = Point(-1, -1)
-        self.bottom_left: Point = Point(-1, -1)
-        self.bottom_right: Point = Point(-1, -1)
-
-    def train(self, img: np.ndarray) -> None:
-        """ Store the position of the 4 markers below the the 4 pieces in the corners"""
-        corners, ids, rejected = cv.aruco.detectMarkers(
-            img, self.aruco_dict, parameters=self.aruco_params
-        )
-
-        if ids is not None:
-            all_markers = []
-            for i in range(len(ids)):
-                r = ((int(x), int(y)) for x, y in corners[i][0].tolist())
-                rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
-                center = get_rect_center(rect)
-                all_markers.append(center)
-
-            # we have the 4 corners
-            if len(all_markers) == 4:
-                vertical_sort = sorted(all_markers, key=lambda point: point.y)
-                top_markers = vertical_sort[:2]
-                bottom_markers = vertical_sort[2:]
-                self.top_left = min(top_markers, key=lambda point: point.x)
-                self.top_right = max(top_markers, key=lambda point: point.x)
-                self.bottom_left = min(bottom_markers, key=lambda point: point.x)
-                self.bottom_right = max(bottom_markers, key=lambda point: point.x)
-
-    def get_board_with_visibility(self, img: np.ndarray) -> Board:
-        """Compute a new board from the 4 corners stored during training, with all pieces visibility"""
-        board = self._board_from_corners()
-
-        corners, ids, rejected = cv.aruco.detectMarkers(
-            img, self.aruco_dict, parameters=self.aruco_params
-        )
-
-        if ids is not None:
-            all_markers = []
-            for i in range(len(ids)):
-                r = ((int(x), int(y)) for x, y in corners[i][0].tolist())
-                rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
-                center = get_rect_center(rect)
-                all_markers.append(center)
-
-            for marker in all_markers:
-                closest_piece_idx = np.argmin(
-                    [get_segment_size(marker, piece.postion) for piece in board.pieces]
-                )
-                board.pieces[closest_piece_idx].is_visible = True
-
-        return board
-
-    def _board_from_corners(self) -> Board:
-        left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left)
-        right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right)
-        row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion)
-        row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion)
-        row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion)
-        row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion)
-        return Board(row_0 + row_1 + row_2 + row_3)
-
-    @staticmethod
-    def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]:
-        """Get 4 pieces from 2 ends pieces"""
-        return (
-            Piece(p1),
-            Piece(get_segement_point_at_dist(p1, p2, 0.45)),
-            Piece(get_segement_point_at_dist(p2, p1, 0.20)),
-            Piece(p2),
-        )
-
-    @staticmethod
-    def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]:
-        """Get 4 pieces from 2 ends pieces"""
-        return (
-            Piece(p1),
-            Piece(get_segement_point_at_dist(p1, p2, 0.33)),
-            Piece(get_segement_point_at_dist(p2, p1, 0.33)),
-            Piece(p2),
-        )
+        boards_pieces = [board.pieces for board in boards]
+        transposed = [elem for elem in zip(*boards_pieces)]
+        res = []
+        for last_pieces_for_one_piece in transposed:
+            construct = []
+            for piece in last_pieces_for_one_piece:
+                construct.append(piece.state)
+            res.append(construct)
+        return res
 
 
 class PieceTakenDetectionEdgesTrigger:
-    """Call 'trigger' function when after a piece is taken
-
-    It waits for 'rising_count' times the same piece detection state before triggering when we take a piece
-    It waits for 'falling_count' times the same piece detection state before triggering when we put back a piece
-
-    TODO: replace this average impl with a counter to increase perf
-    """
+    """ WARNING: deprectated: replaced with PieceTakenTrigger"""
 
     def __init__(self, trigger: Callable[[int], None], rising_count: int = 5, falling_count: int = 5):
+        print(self.__doc__, file=sys.stderr)
         self.trigger = trigger
         self.rising_count = rising_count
         self.falling_count = falling_count
@@ -477,3 +111,5 @@ class PieceTakenDetectionEdgesTrigger:
             #     print([int(b.pieces[idx].is_visible) for b in self.last_boards])
 
         return next_out_board
+
+
diff --git a/memory_lib/cv_utils.py b/memory_lib/cv_utils.py
index afcf5fd418d10feaf91af46a02222f41a99f6012..b6ec89386b457acd5b3c10610d549d12b655642f 100644
--- a/memory_lib/cv_utils.py
+++ b/memory_lib/cv_utils.py
@@ -93,32 +93,3 @@ def draw_rect(img: np.ndarray, rect: Rect, color=(255, 0, 0)):
     """ Draw rectangle rect on img """
     x, y, w, h = rect.to_tuple()
     cv.rectangle(img, (x, y), (x + w, y + h), color)
-
-
-class MotionDetector:
-    """ Compute how many frames are the same """
-
-    def __init__(self, pixels_count_to_diff: int = 3):
-        """
-        :param pixels_count_to_diff: When you diff to frame, how many pixels can change to consider same/different frame
-        """
-        self.pixels_count_to_diff = pixels_count_to_diff
-        self._same_frame_counter = 0
-        self._last_frame = None
-        self._dbg_last_frame_img = None
-
-    def add(self, img: np.ndarray) -> None:
-        self._dbg_last_frame_img = img.copy()
-
-        canny = cv.Canny(img, 0, 150)
-        if self._last_frame is not None:
-            diff = cv.absdiff(canny, self._last_frame)
-            if cv.countNonZero(diff) < self.pixels_count_to_diff:
-                self._same_frame_counter += 1
-            else:
-                self._same_frame_counter = 0
-        self._last_frame = canny.copy()
-
-    @property
-    def same_frame_counter(self):
-        return self._same_frame_counter
diff --git a/memory_lib/detectors/board_detectors.py b/memory_lib/detectors/board_detectors.py
new file mode 100644
index 0000000000000000000000000000000000000000..f09ab23e7cfad75e093ea1bfd08d23129624ff36
--- /dev/null
+++ b/memory_lib/detectors/board_detectors.py
@@ -0,0 +1,54 @@
+import cv2 as cv
+import numpy as np
+
+from abc import ABC, abstractmethod
+from typing import Tuple, Optional
+
+from memory_lib.geometry import get_rect_center
+from memory_lib.model import Point, Rect
+
+
+class ABMExtractor(ABC):
+    @abstractmethod
+    def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]:
+        ...
+
+
+class ArucoABMExtractor(ABMExtractor):
+    """Uses Aruco marker to extract the board A, B and M points
+
+    Aruco marker ids:
+    0 for point A
+    1 for point B
+    2 for point M
+    """
+
+    def __init__(self):
+        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_50)
+        self.aruco_params = cv.aruco.DetectorParameters_create()
+
+    def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]:
+        """ If all 3 markers are found, returns the 3 Points A, B and M """
+        corners, ids, rejected = cv.aruco.detectMarkers(
+            img, self.aruco_dict, parameters=self.aruco_params
+        )
+
+        # TODO: debug
+        # cv.aruco.drawDetectedMarkers(img, corners)
+
+        markers = {}
+        if ids is None:
+            return None
+
+        for i in range(len(ids)):
+            rect_val = ((int(x), int(y)) for x, y in corners[i][0].tolist())
+            rect = Rect.from_corners(*(Point.from_tuple(t) for t in rect_val))
+            markers[ids[i][0]] = get_rect_center(rect)
+
+        a = markers.get(0)
+        b = markers.get(1)
+        m = markers.get(2)
+
+        if a and b and m and (a != b):
+            return a, b, m
+        return None
diff --git a/memory_lib/detectors/board_factory.py b/memory_lib/detectors/board_factory.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc4ca5e50529a955afeeb8b170827d4b8a1656f3
--- /dev/null
+++ b/memory_lib/detectors/board_factory.py
@@ -0,0 +1,153 @@
+from abc import ABC, abstractmethod
+from typing import Tuple
+
+import numpy as np
+
+from memory_lib.geometry import line_eq_from_points, get_segment_size, get_parallel_eq, get_line_intersection_point
+from memory_lib.model import Board, Point, LineEq, Piece
+
+
+class BoardFactory(ABC):
+    @abstractmethod
+    def get_board(self) -> Board:
+        ...
+
+
+class ABMBoardFactory(BoardFactory):
+    def __init__(self, a: Point, b: Point, m: Point, img: np.ndarray):
+        self.a = a
+        self.b = b
+        self.m = m
+        # TODO: no need img here, only for debug
+        self.img = img
+
+    def get_board(self) -> Board:
+        """ Create a Board object from from A, B and M borad points """
+        a = self.a
+        b = self.b
+        m = self.m
+
+        # F is [AB] center
+        f = Point((a.x + b.x) // 2, (a.y + b.y) // 2)
+        ab_eq = line_eq_from_points(a, b)
+
+        # TODO: check
+        # if ab_m == math.nan or ab_b == math.nan:
+        #     return
+
+        # TODO: debug only
+        # draw_line(self.img, ab_eq)
+
+        # FM line
+        fm_eq = line_eq_from_points(f, m)
+
+        # TODO: check
+        # if fm_m == math.inf or fm_b == math.inf:
+        #     return
+
+        # TODO: debug only
+        # draw_line(self.img, fm_eq)
+        fm_s = get_segment_size(f, m)
+
+        # Approximate E, based on FM size.
+        # First approximate DC line, based on FM size
+        dc_eq = get_parallel_eq(ab_eq, Point(0, int(a.y - fm_s * 1.5)))
+
+        # TODO: debug only
+        # draw_line(self.img, dc_eq)
+
+        # Then e is on DC, FM intersection
+        e = get_line_intersection_point(fm_eq, dc_eq)
+
+        # Compute the 4 parallels: A1B1, A2B2, A3B3, A4B4
+        a1, b1 = self._compute_parallel(a, b, ab_eq, e, f, 0.75, 1 * (fm_s / 10))
+        a2, b2 = self._compute_parallel(a, b, ab_eq, e, f, 0.48, 3 * (fm_s / 10))
+        a3, b3 = self._compute_parallel(a, b, ab_eq, e, f, 0.25, 4.5 * (fm_s / 10))
+        a4, b4 = self._compute_parallel(a, b, ab_eq, e, f, 0.07, 6 * (fm_s / 10))
+
+        # Compoute the pieces position from the above lines
+        pieces = (
+            *self._compute_piece(a1, b1),
+            *self._compute_piece(a2, b2),
+            *self._compute_piece(a3, b3),
+            *self._compute_piece(a4, b4),
+        )
+
+        # TODO: debug only
+        debut_print = (
+            (a, "A"),
+            (b, "B"),
+            (f, "F"),
+            (e, "E"),
+            (a1, "A1"),
+            (b1, "B1"),
+            (a2, "A2"),
+            (b2, "B2"),
+            (a3, "A3"),
+            (b3, "B3"),
+            (a4, "A4"),
+            (b4, "B4"),
+        )
+        # for point, name in debut_print:
+        #     draw_point(self.img, point, name, (0, 255, 0))
+        # for idx, piece in enumerate(pieces):
+        #     draw_point(img, piece, f"P{idx}", (0, 0, 255))
+        # cv.imshow("IMG", img)
+
+        return Board(pieces)
+
+    @staticmethod
+    def _compute_parallel(
+            a: Point,
+            b: Point,
+            ab_eq: LineEq,
+            e: Point,
+            f: Point,
+            height_reduce_ratio: float,
+            width_reduce_ratio: float,
+    ) -> Tuple[Point, Point]:
+        """ used to find A1B1, A2B2, A3B3, A4B4 from AB and EF"""
+        m1 = Point(
+            int(e.x + height_reduce_ratio * (f.x - e.x)),
+            int(e.y + height_reduce_ratio * (f.y - e.y)),
+        )
+        a1b1_eq = get_parallel_eq(ab_eq, m1)
+        left_point = Point(
+            int(a.x + width_reduce_ratio),
+            int(a1b1_eq.m * (a.x + width_reduce_ratio) + a1b1_eq.b),
+        )
+        right_point = Point(
+            int(b.x - width_reduce_ratio),
+            int(a1b1_eq.m * (b.x - width_reduce_ratio) + a1b1_eq.b),
+        )
+        return left_point, right_point
+
+    @staticmethod
+    def _compute_piece(left_point: Point, right_point: Point) -> Tuple[Piece, ...]:
+        """ find the 4 pieces from a horizontal line """
+        return (
+            Piece(
+                Point(
+                    int(left_point.x + 0.1 * (right_point.x - left_point.x)),
+                    int(left_point.y + 0.1 * (right_point.y - left_point.y)),
+                )
+            ),
+            Piece(
+                Point(
+                    int(left_point.x + 0.35 * (right_point.x - left_point.x)),
+                    int(left_point.y + 0.35 * (right_point.y - left_point.y)),
+                )
+            ),
+            Piece(
+                Point(
+                    int(left_point.x + 0.65 * (right_point.x - left_point.x)),
+                    int(left_point.y + 0.65 * (right_point.y - left_point.y)),
+                )
+            ),
+            Piece(
+                Point(
+                    int(left_point.x + 0.9 * (right_point.x - left_point.x)),
+                    int(left_point.y + 0.9 * (right_point.y - left_point.y)),
+                )
+            ),
+        )
diff --git a/memory_lib/detectors/misc_detectors.py b/memory_lib/detectors/misc_detectors.py
new file mode 100644
index 0000000000000000000000000000000000000000..b017bed0115c8a3d4a0b4bb14ef464d26cc4faf8
--- /dev/null
+++ b/memory_lib/detectors/misc_detectors.py
@@ -0,0 +1,31 @@
+import cv2 as cv
+import numpy as np
+
+
+class MotionDetector:
+    """ Compute how many frames are the same """
+
+    def __init__(self, pixels_count_to_diff: int = 3):
+        """
+        :param pixels_count_to_diff: When you diff to frame, how many pixels can change to consider same/different frame
+        """
+        self.pixels_count_to_diff = pixels_count_to_diff
+        self._same_frame_counter = 0
+        self._last_frame = None
+        self._dbg_last_frame_img = None
+
+    def add(self, img: np.ndarray) -> None:
+        self._dbg_last_frame_img = img.copy()
+
+        canny = cv.Canny(img, 0, 150)
+        if self._last_frame is not None:
+            diff = cv.absdiff(canny, self._last_frame)
+            if cv.countNonZero(diff) < self.pixels_count_to_diff:
+                self._same_frame_counter += 1
+            else:
+                self._same_frame_counter = 0
+        self._last_frame = canny.copy()
+
+    @property
+    def same_frame_counter(self):
+        return self._same_frame_counter
diff --git a/memory_lib/detectors/piece_state_detectors.py b/memory_lib/detectors/piece_state_detectors.py
new file mode 100644
index 0000000000000000000000000000000000000000..5479bef6d9b8ecc7c469e53ce01864d8bc24154c
--- /dev/null
+++ b/memory_lib/detectors/piece_state_detectors.py
@@ -0,0 +1,352 @@
+from abc import ABC, abstractmethod
+from typing import List, Tuple
+import cv2 as cv
+
+import numpy as np
+
+from memory_lib.cv_utils import get_roi_around_point, average_color
+from memory_lib.detectors.misc_detectors import MotionDetector
+from memory_lib.geometry import get_segment_center, get_rect_center, get_segement_point_at_dist, get_segment_size
+from memory_lib.model import Point, Board, Rect, Piece, PieceState
+
+
+class PieceStateDetector(ABC):
+    """ Use to extract if a piece is visible or not"""
+    @abstractmethod
+    def is_visible(self, img, piece_pos: Point, piece_index) -> bool:
+        ...
+
+    @abstractmethod
+    def train(self, board: Board, img: np.ndarray) -> None:
+        ...
+
+
+class PieceStateExtractor(ABC):
+    """ Use to get a board with updated pieces states"""
+    @abstractmethod
+    def get_board_with_visibility(self, img: np.ndarray) -> Board:
+        ...
+
+    @abstractmethod
+    def train(self, img: np.ndarray) -> None:
+        ...
+
+    @abstractmethod
+    def is_ready(self) -> bool:
+        ...
+
+
+class LightnessPieceStateDetector(PieceStateDetector):
+    """ Primitive detector based on lightness of average pixels around the piece"""
+
+    def __init__(self, threshold=80):
+        self.threshold = threshold
+
+    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index) -> bool:
+        roi = get_roi_around_point(img, piece_pos, 3)
+        if roi.shape != (6, 6, 3):
+            return False
+        avg_color = average_color(roi)
+        return avg_color[2] > self.threshold
+
+    def train(self, board: Board, img: np.ndarray) -> None:
+        ...
+
+
+class AverageColorPieceStateDetector(PieceStateDetector):
+    """ Piece detector based on average pixels color around the piece"""
+
+    def __init__(self):
+        self.pieces_colors: List[Tuple[int, int, int]] = []
+        self.board_color: Tuple[int, int, int] = (0, 0, 0)
+
+    def train(self, board: Board, img: np.ndarray) -> None:
+        """ Store an avraged color value for every pieces in the board """
+        if not self.pieces_colors:
+            self.pieces_colors = [
+                average_color(get_roi_around_point(img, piece.postion))
+                for piece in board.pieces
+            ]
+            # get the socle color from a point below M
+            middle = get_segment_center(
+                board.pieces[5].postion, board.pieces[6].postion
+            )
+            self.board_color = average_color(get_roi_around_point(img, middle))
+        else:
+            for idx, piece in enumerate(board.pieces):
+                c1 = self.pieces_colors[idx]
+                c2 = average_color(get_roi_around_point(img, piece.postion))
+                self.pieces_colors[idx] = self._avg_colors(c1, c2)
+            middle = get_segment_center(
+                board.pieces[5].postion, board.pieces[6].postion
+            )
+            self.board_color = self._avg_colors(
+                self.board_color, average_color(get_roi_around_point(img, middle))
+            )
+
+    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool:
+        # naive color distance detection, TODO: uses device independent color spaces? in_range?
+
+        # Test if piece color is far from piece and close to board_color
+        c1 = self.pieces_colors[piece_index]
+        c2 = average_color(get_roi_around_point(img, piece_pos))
+        m1 = self.board_color
+        m2 = average_color(get_roi_around_point(img, piece_pos))
+        return (
+                       abs(m1[0] - m2[0]) < 100
+                       and abs(m1[1] - m2[1]) < 100
+                       and abs(m1[2] - m2[2]) < 100
+               ) and (
+                       abs(c1[0] - c2[0]) > 60
+                       or abs(c1[1] - c2[1]) > 60
+                       or abs(c1[2] - c2[2]) > 60
+               )
+
+    @staticmethod
+    def _avg_colors(
+            c1: Tuple[int, int, int], c2: Tuple[int, int, int]
+    ) -> Tuple[int, int, int]:
+        return (c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2, (c1[2] + c2[2]) // 2
+
+
+class AverageColorAndMovementPieceVisibilityDetector(PieceStateDetector):
+    """ Piece detector based on average pixels color around the piece, compute only no moving objects"""
+
+    def __init__(self, same_frame_counter_max: int = 5):
+        """
+        :param same_frame_counter_max: how many different frames before considering a movement
+        """
+        self.average_color_detector = AverageColorPieceStateDetector()
+        self.same_frame_counter_max = same_frame_counter_max
+        self.motion_detectors = [MotionDetector() for _ in range(16)]
+
+    def train(self, board: Board, img: np.ndarray) -> None:
+        self.average_color_detector.train(board, img)
+
+    def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool:
+        roi = cv.cvtColor(
+            get_roi_around_point(img, piece_pos, radius=10), cv.COLOR_BGR2GRAY
+        )
+
+        # TODO: debug only
+        # if piece_index == 5:
+        #     print(self.motion_detectors[piece_index].same_frame_counter)
+        #     cv.imshow("roi", roi)
+        #     if (self.motion_detectors[piece_index]._last_frame is not None):
+        #         cv.imshow("last", self.motion_detectors[piece_index]._last_frame)
+
+        self.motion_detectors[piece_index].add(roi)
+        if (
+                self.motion_detectors[piece_index].same_frame_counter
+                > self.same_frame_counter_max
+        ):
+            return self.average_color_detector.is_visible(img, piece_pos, piece_index)
+        return False
+
+
+class ArucoHalfPieceStateExtractor(PieceStateExtractor):
+    """ Piece detector based on Aruco marker below every piece
+
+    TODO:
+    for now this detector does not work as a PieceVisibilityDetector
+    This detector directly send a board with updated visibility
+    """
+
+    def __init__(self):
+        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
+        self.aruco_params = cv.aruco.DetectorParameters_create()
+        self.top_left: Point = Point(-1, -1)
+        self.top_right: Point = Point(-1, -1)
+        self.bottom_left: Point = Point(-1, -1)
+        self.bottom_right: Point = Point(-1, -1)
+
+    def train(self, img: np.ndarray) -> None:
+        """ Store the position of the 4 markers below the the 4 pieces in the corners"""
+        corners, ids, rejected = cv.aruco.detectMarkers(
+            img, self.aruco_dict, parameters=self.aruco_params
+        )
+
+        if ids is not None:
+            all_markers = []
+            for i in range(len(ids)):
+                r = ((int(x), int(y)) for x, y in corners[i][0].tolist())
+                rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
+                center = get_rect_center(rect)
+                all_markers.append(center)
+
+            # we have the 4 corners
+            if len(all_markers) == 4:
+                vertical_sort = sorted(all_markers, key=lambda point: point.y)
+                top_markers = vertical_sort[:2]
+                bottom_markers = vertical_sort[2:]
+                self.top_left = min(top_markers, key=lambda point: point.x)
+                self.top_right = max(top_markers, key=lambda point: point.x)
+                self.bottom_left = min(bottom_markers, key=lambda point: point.x)
+                self.bottom_right = max(bottom_markers, key=lambda point: point.x)
+
+    def get_board_with_visibility(self, img: np.ndarray) -> Board:
+        """Compute a new board from the 4 corners stored during training, with all pieces visibility"""
+        board = self._board_from_corners()
+
+        corners, ids, rejected = cv.aruco.detectMarkers(
+            img, self.aruco_dict, parameters=self.aruco_params
+        )
+
+        if ids is not None:
+            all_markers = []
+            for i in range(len(ids)):
+                r = ((int(x), int(y)) for x, y in corners[i][0].tolist())
+                rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
+                center = get_rect_center(rect)
+                all_markers.append(center)
+
+            for marker in all_markers:
+                closest_piece_idx = np.argmin(
+                    [get_segment_size(marker, piece.postion) for piece in board.pieces]
+                )
+                board.pieces[closest_piece_idx].is_visible = True
+
+        return board
+
+    def _board_from_corners(self) -> Board:
+        left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left)
+        right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right)
+        row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion)
+        row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion)
+        row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion)
+        row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion)
+        return Board(row_0 + row_1 + row_2 + row_3)
+
+    @staticmethod
+    def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]:
+        """Get 4 pieces from 2 ends pieces"""
+        return (
+            Piece(p1),
+            Piece(get_segement_point_at_dist(p1, p2, 0.45)),
+            Piece(get_segement_point_at_dist(p2, p1, 0.20)),
+            Piece(p2),
+        )
+
+    @staticmethod
+    def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]:
+        """Get 4 pieces from 2 ends pieces"""
+        return (
+            Piece(p1),
+            Piece(get_segement_point_at_dist(p1, p2, 0.33)),
+            Piece(get_segement_point_at_dist(p2, p1, 0.33)),
+            Piece(p2),
+        )
+
+
+class ArucoFullPieceStateExtractor(PieceStateExtractor):
+    """ Piece detector based on Aruco markers above and below every piece"""
+
+    MARKER_ON_BOARD_ID = 930
+    MARKER_OFF_BOARD_ID = 190
+
+    def __init__(self):
+        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000)
+        self.aruco_params = cv.aruco.DetectorParameters_create()
+
+        # TODO: to PieceStateExtractor property
+        self.ready = False
+
+        self.top_left: Point = Point(-1, -1)
+        self.top_right: Point = Point(-1, -1)
+        self.bottom_left: Point = Point(-1, -1)
+        self.bottom_right: Point = Point(-1, -1)
+        self.board = None
+
+    def train(self, img: np.ndarray) -> None:
+        """ Store the position of the 4 markers below the the 4 pieces in the corners"""
+        corners, ids, rejected = cv.aruco.detectMarkers(
+            img, self.aruco_dict, parameters=self.aruco_params
+        )
+
+        if ids is not None:
+            all_markers = []
+            for idx, marker_id in enumerate(ids):
+                if marker_id in (self.MARKER_ON_BOARD_ID, self.MARKER_OFF_BOARD_ID):
+                    r = ((int(x), int(y)) for x, y in corners[idx][0].tolist())
+                    rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
+                    # TODO
+                    # BUG: the center is only correct if the marker is in the right orientation
+                    # otherwise, we get complute the center on a corner
+                    center = get_rect_center(rect)
+                    cv.circle(img, (center.x, center.y), 10, (123,0,0))
+                    cv.rectangle(img, (rect.top_left.x, rect.top_left.y), (rect.top_left.x+rect.width, rect.top_left.y+rect.height), (255, 0 ,0))
+                    all_markers.append(center)
+
+            if len(all_markers) == 16:
+                vertical_sort = sorted(all_markers, key=lambda point: point.y)
+                top_markers = vertical_sort[:4]
+                bottom_markers = vertical_sort[-4:]
+                self.top_left = min(top_markers, key=lambda point: point.x)
+                self.top_right = max(top_markers, key=lambda point: point.x)
+                self.bottom_left = min(bottom_markers, key=lambda point: point.x)
+                self.bottom_right = max(bottom_markers, key=lambda point: point.x)
+                self.ready = True
+
+    def get_board_with_visibility(self, img: np.ndarray) -> Board:
+        """Compute a new board from the 4 corners stored during training, with all pieces visibility"""
+        board = self._board_from_corners()
+
+        corners, ids, rejected = cv.aruco.detectMarkers(
+            img, self.aruco_dict, parameters=self.aruco_params
+        )
+
+        # DEBUG
+        # cv.aruco.drawDetectedMarkers(img, corners)
+
+        if ids is not None:
+            all_markers = []
+            for idx, marker_ids in enumerate(ids):
+                r = ((int(x), int(y)) for x, y in corners[idx][0].tolist())
+                rect = Rect.from_corners(*(Point.from_tuple(t) for t in r))
+                center = get_rect_center(rect)
+                all_markers.append((marker_ids[0], center))
+
+            for marker_id, center in all_markers:
+                closest_piece_idx = np.argmin(  # argmin to get the index or the min
+                    [get_segment_size(center, piece.postion) for piece in board.pieces]
+                )
+                if marker_id == self.MARKER_ON_BOARD_ID:
+                    board.pieces[closest_piece_idx].state = PieceState.ON_BOARD
+                elif marker_id == self.MARKER_OFF_BOARD_ID:
+                    board.pieces[closest_piece_idx].state = PieceState.OFF_BOARD
+                else:
+                    board.pieces[closest_piece_idx].state = PieceState.UNKNOWN
+
+        return board
+
+    def is_ready(self) -> bool:
+        return self.ready
+
+    def _board_from_corners(self) -> Board:
+        left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left)
+        right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right)
+        row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion)
+        row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion)
+        row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion)
+        row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion)
+        return Board(row_0 + row_1 + row_2 + row_3)
+
+    @staticmethod
+    def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]:
+        """Get 4 pieces from 2 ends pieces"""
+        return (
+            Piece(p1),
+            Piece(get_segement_point_at_dist(p1, p2, 0.45)),
+            Piece(get_segement_point_at_dist(p2, p1, 0.20)),
+            Piece(p2),
+        )
+
+    @staticmethod
+    def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]:
+        """Get 4 pieces from 2 ends pieces"""
+        return (
+            Piece(p1),
+            Piece(get_segement_point_at_dist(p1, p2, 0.33)),
+            Piece(get_segement_point_at_dist(p2, p1, 0.33)),
+            Piece(p2),
+        )
diff --git a/memory_lib/memory.py b/memory_lib/memory.py
index 1324d84cd3215a9672ac1612e77ceb51fcb95597..3375a968e3b1b2b9a53d2fd89d6b135fef906fc9 100644
--- a/memory_lib/memory.py
+++ b/memory_lib/memory.py
@@ -5,20 +5,13 @@ import numpy as np
 
 from abc import ABC, abstractmethod
 
-from memory_lib.geometry import get_rect_center
-
-from memory_lib.cv_utils import draw_rect, draw_point
+from memory_lib.detectors.board_detectors import ABMExtractor, ArucoABMExtractor
+from memory_lib.detectors.board_factory import ABMBoardFactory
+from memory_lib.detectors.piece_state_detectors import ArucoFullPieceStateExtractor, ArucoHalfPieceStateExtractor, \
+    AverageColorAndMovementPieceVisibilityDetector, PieceStateDetector, PieceStateExtractor
 
 from .board import (
-    ArucoABMExtractor,
-    ABMBoardFactory,
-    PieceVisibilityDetector,
-    LightnessPieceVisibilityDetector,
-    AverageColorPieceVisibilityDetector,
-    ArucoPieceVisibilityDetector,
-    ABMExtractor,
-    AverageColorAndMovementPieceVisibilityDetector,
-    PieceTakenDetectionEdgesTrigger,
+    PieceTakenDetectionEdgesTrigger, PieceTakenTrigger,
 )
 from .model import Board, PiecesObserver, Rect, Point
 
@@ -41,20 +34,27 @@ class Memory(ABC, threading.Thread, PiecesObserver):
         threading.Thread.__init__(self)
         PiecesObserver.__init__(self)
         self.video_caputre = video_capture
+        self.running = False
+        self.frame = None
 
     def run(self):
-        while True:
+        self.running = True
+        while self.running:
             ret, frame = self.video_caputre.read()
             if ret:
                 time.sleep(0.04)
-                cv.imshow("vid", frame)
+                # cv.imshow("vid", frame)
                 self.process(frame)
-            key = cv.waitKey(1) & 0xFF
-            if key == ord("r"):
-                self.reset()
-            if key == ord("q"):
-                self.stopped_trigger()
-                break
+                self.frame = frame
+                # key = cv.waitKey(1) & 0xFF
+                # if key == ord("r"):
+                #     self.reset()
+                # if key == ord("q"):
+                #     self.stopped_trigger()
+                #     break
+
+    def stop_game(self):
+        self.running = False
 
     @abstractmethod
     def process(self, img: np.ndarray) -> None:
@@ -65,39 +65,78 @@ class Memory(ABC, threading.Thread, PiecesObserver):
         ...
 
 
-class MemoryAruco(Memory):
+class MemoryArucoFull(Memory):
+    """Memory pieces detection based on Aruco above and below every piece"""
+
+    def __init__(
+            self,
+            video_capture: cv.VideoCapture,
+    ):
+        Memory.__init__(self, video_capture)
+        self.piece_state_extractor = ArucoFullPieceStateExtractor()
+        self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000)
+        self.aruco_params = cv.aruco.DetectorParameters_create()
+        self.average_trigger = PieceTakenTrigger(self.piece_trigger)
+
+    def process(self, img: np.ndarray) -> None:
+        # For the 5 first frames, store the corners
+        if not self.piece_state_extractor.ready:
+            self.piece_state_extractor.train(img)
+            return
+
+        board = self.piece_state_extractor.get_board_with_visibility(img)
+        # self.board_trigger(board, img)
+        out_board = self.average_trigger.add_board(board)
+
+        # TODO: debug
+        # cv.aruco.drawDetectedMarkers(img, corners)
+        out_board.draw(img)
+        self.board_trigger(board, img)
+        # cv.imshow("marker", img)
+
+    def reset(self) -> None:
+        """Reset board position"""
+        print("postion reset")
+        self.piece_state_extractor = ArucoFullPieceStateExtractor()
+
+
+class MemoryArucoHalf(Memory):
     """Memory pieces detection based on Aruco below every piece"""
 
     def __init__(
         self,
         video_capture: cv.VideoCapture,
-        visibility_detector: ArucoPieceVisibilityDetector = ArucoPieceVisibilityDetector()
+        piece_state_extractor: PieceStateExtractor = ArucoFullPieceStateExtractor(),
     ):
         Memory.__init__(self, video_capture)
-        self.visibility_detector = visibility_detector
+        self.piece_extractor = piece_state_extractor
         self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
         self.aruco_params = cv.aruco.DetectorParameters_create()
-        self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 10, 40)
+        self.average_trigger = PieceTakenDetectionEdgesTrigger(  # Deprecated
+            self.piece_trigger, 10, 40
+        )
         self.frame_counter = 0
 
     def process(self, img: np.ndarray) -> None:
         # For the 5 first frames, store the corners
         if self.frame_counter < 10:
-            self.visibility_detector.train(img)
+            self.piece_extractor.train(img)
             self.frame_counter += 1
             return
 
-        board = self.visibility_detector.get_board_with_visibility(img)
-        self.board_trigger(board)
+        board = self.piece_extractor.get_board_with_visibility(img)
+        # self.board_trigger(board, img)
         self.average_trigger.add_board(board)
 
         # TODO: debug
         # cv.aruco.drawDetectedMarkers(img, corners)
         self.average_trigger.out_board.draw(img)
-        cv.imshow("marker", img)
+        self.board_trigger(board, img)
+        # cv.imshow("marker", img)
 
     def reset(self) -> None:
         """Reset board position"""
+        print("postion reset")
         self.frame_counter = 0
 
 
@@ -113,13 +152,13 @@ class MemoryABM(Memory):
         self,
         video_capture: cv.VideoCapture,
         abm_extractor: ABMExtractor = ArucoABMExtractor(),
-        visibility_detector: PieceVisibilityDetector = AverageColorAndMovementPieceVisibilityDetector(),
+        visibility_detector: PieceStateDetector = AverageColorAndMovementPieceVisibilityDetector(),
     ):
         Memory.__init__(self, video_capture)
         self.abm_extractor = abm_extractor
         self.visibility_detector = visibility_detector
         self.frame_counter = 0
-        self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 5)
+        self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 5)  # Deprecated
         self.last_board = None
 
     def process(self, img: np.ndarray) -> None:
diff --git a/memory_lib/model.py b/memory_lib/model.py
index 506981909d57a3fc132d0407643924a83ece4016..58f31dc2fefa2d4c4f1e3f6fe799905ba1eeb283 100644
--- a/memory_lib/model.py
+++ b/memory_lib/model.py
@@ -1,4 +1,5 @@
 from dataclasses import dataclass
+from enum import Enum
 from typing import Tuple, Callable
 import numpy as np
 import cv2 as cv
@@ -39,20 +40,32 @@ class LineEq:
     b: float
 
 
+class PieceState(Enum):
+    ON_BOARD = 1
+    OFF_BOARD = 2
+    UNKNOWN = 3
+
+
 @dataclass
 class Piece:
     postion: Point = Point(0, 0)
-    is_visible: bool = False
+    state: PieceState = PieceState.UNKNOWN
 
 
 @dataclass
 class Board:
     pieces: Tuple[Piece, ...]  # All 16 pieces
 
+    _draw_colors = {
+        PieceState.ON_BOARD: (255, 0, 0),
+        PieceState.OFF_BOARD: (0, 255, 0),
+        PieceState.UNKNOWN: (0, 0, 255),
+    }
+
     def draw(self, img: np.ndarray):
         """ Draw circle for each piece position. Green if the piece is visible, red otherwise"""
         for piece in self.pieces:
-            color = (0, 255, 0) if piece.is_visible else (0, 0, 255)
+            color = self._draw_colors[piece.state]
             cv.circle(img, piece.postion.to_tuple(), 10, color)
 
     def update_pieces(self, img: np.ndarray, piece_detector) -> None:
@@ -61,7 +74,7 @@ class Board:
 
     @classmethod
     def from_board(cls, board: "Board") -> "Board":
-        pieces = tuple(Piece(p.postion, p.is_visible) for p in board.pieces)
+        pieces = tuple(Piece(p.postion, p.state) for p in board.pieces)
         return cls(pieces)
 
 
@@ -85,12 +98,7 @@ class PiecesObserver:
         for callback in self._pieces_observers:
             callback(piece_id)
 
-    def board_trigger(self, board: Board):
+    def board_trigger(self, board: Board, img):
         """On every frame, send the full board"""
         for callback in self._board_observers:
-            callback(board)
-
-    def stopped_trigger(self):
-        """On every frame, send the full board"""
-        for callback in self._stopped_observers:
-            callback()
+            callback(board, img)
diff --git a/memory_lib/piece_state.py b/memory_lib/piece_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcb00cf81696f03d9c581f4b99bb47619a9c7855
--- /dev/null
+++ b/memory_lib/piece_state.py
@@ -0,0 +1,115 @@
+from abc import ABC, abstractmethod
+from enum import Enum
+from typing import Callable
+
+from memory_lib.model import PieceState, Board
+
+
+class StateMachineInput(ABC):
+    def __init__(self, enum: Enum) -> None:
+        self.enum = enum
+
+
+class StateMachineInputPiece(StateMachineInput):
+    def __init__(self, piece_state: PieceState):
+        super().__init__(piece_state)
+
+
+class State(ABC):
+
+    @staticmethod
+    @abstractmethod
+    def next(sm_input: StateMachineInput) -> tuple["State", bool]:
+        ...
+
+
+class StateMachine:
+    def __init__(self, inital_state: State) -> None:
+        self.current_state = inital_state
+
+    def next(self, sm_input: StateMachineInput) -> bool:
+        """Go to the next state according to current state and input. Return if the output needs to be triggered"""
+        self.current_state, trigger_output = self.current_state.next(sm_input)
+        return trigger_output
+
+
+class PieceStateStateMachine(StateMachine):
+    def __init__(self) -> None:
+        super().__init__(state_0)
+
+
+class State0(State):
+    @staticmethod
+    def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]:
+        if sm_input.enum == PieceState.UNKNOWN:
+            return state_2, False
+        if sm_input.enum == PieceState.ON_BOARD:
+            return state_1, False
+        if sm_input.enum == PieceState.OFF_BOARD:
+            return state_4, False
+
+
+class State1(State):
+    @staticmethod
+    def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]:
+        if sm_input.enum == PieceState.UNKNOWN:
+            return state_3, False
+        if sm_input.enum == PieceState.ON_BOARD:
+            return state_1, False
+        if sm_input.enum == PieceState.OFF_BOARD:
+            return state_4, True
+
+
+class State2(State):
+    @staticmethod
+    def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]:
+        if sm_input.enum == PieceState.UNKNOWN:
+            return state_2, False
+        if sm_input.enum == PieceState.ON_BOARD:
+            return state_1, False
+        if sm_input.enum == PieceState.OFF_BOARD:
+            return state_4, False
+
+
+class State3(State):
+    @staticmethod
+    def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]:
+        if sm_input.enum == PieceState.UNKNOWN:
+            return state_3, False
+        if sm_input.enum == PieceState.ON_BOARD:
+            return state_1, False
+        if sm_input.enum == PieceState.OFF_BOARD:
+            return state_4, True
+
+
+class State4(State):
+    @staticmethod
+    def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]:
+        if sm_input.enum == PieceState.UNKNOWN:
+            return state_4, False
+        if sm_input.enum == PieceState.ON_BOARD:
+            return state_1, False
+        if sm_input.enum == PieceState.OFF_BOARD:
+            return state_4, False
+
+
+state_0 = State0()
+state_1 = State1()
+state_2 = State2()
+state_3 = State3()
+state_4 = State4()
+
+
+class PiecesStatesTracker:
+    def __init__(self, output_trigger: Callable):
+        """Create a PieceStateMachine for every piece on the board
+        :param output_trigger: function to be called when state machine output is triggered
+        """
+        self.output_f = output_trigger
+        self.state_machines = [PieceStateStateMachine() for _ in range(16)]
+
+    def update(self, new_board: Board):
+        for idx, piece in enumerate(new_board.pieces):
+            if self.state_machines[idx].next(StateMachineInputPiece(piece.state)):
+                self.output_f(idx)
+
diff --git a/memory_lib/tests/aruco.py b/memory_lib/tests/aruco.py
index 1780e47a2f3391df35c023247f1a0d9b7ee6e337..f117c73113490d7f7be571e8ffee84592feed762 100644
--- a/memory_lib/tests/aruco.py
+++ b/memory_lib/tests/aruco.py
@@ -1,9 +1,9 @@
 import cv2 as cv
 import numpy as np
 
-# cap = cv.VideoCapture(0)
-cap = cv.VideoCapture("res/webcam_05_aruco.avi")
-aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
+cap = cv.VideoCapture(0)
+# cap = cv.VideoCapture("res/webcam_05_aruco.avi")
+aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000)
 aruco_params = cv.aruco.DetectorParameters_create()
 
 while True:
diff --git a/memory_lib/tests/res/marker_930.png b/memory_lib/tests/res/marker_930.png
new file mode 100644
index 0000000000000000000000000000000000000000..f07c8b48de338c5af5b1cea41d2b5707eac4f77a
Binary files /dev/null and b/memory_lib/tests/res/marker_930.png differ
diff --git a/memory_lib/tests/res/markers_930.png b/memory_lib/tests/res/markers_930.png
new file mode 100644
index 0000000000000000000000000000000000000000..af58fe07e79560c9b8d5b3d9cb8886f753b1ff65
Binary files /dev/null and b/memory_lib/tests/res/markers_930.png differ
diff --git a/memory_lib/tests/run_memory.py b/memory_lib/tests/run_memory.py
index 8a222c8b36d04653514db2f400b50acd79998116..6e149ee7cef481bb065bf1a890e4300eab550f1a 100644
--- a/memory_lib/tests/run_memory.py
+++ b/memory_lib/tests/run_memory.py
@@ -36,12 +36,19 @@ def main():
     # cap = cv.VideoCapture("res/webcam_05_aruco.avi")
 
     # cap = cv.VideoCapture(0)
-    cap = cv.VideoCapture("res/webcam_11_aruco.avi")
-    memory = memory_lib.MemoryAruco(cap)
+    cap = cv.VideoCapture("res/webcam_12_aruco_full.avi")
+    memory = memory_lib.MemoryArucoFull(cap)
 
-    memory.bind(hello_piece)
+    memory.bind_pieces(hello_piece)
     memory.start()
 
+    while True:
+        frame = memory.frame
+        if frame is not None:
+            cv.imshow('webcam', frame)
+        if cv.waitKey(1) == 27:
+            break
+
     # process_vid("res/webcam_03_shadow.avi", memory)
 
     # process_cam()
diff --git a/memory_lib/tests/test_pieces_states.py b/memory_lib/tests/test_pieces_states.py
new file mode 100644
index 0000000000000000000000000000000000000000..9573d53302074810c4cdd36aea69c23f1fe2a464
--- /dev/null
+++ b/memory_lib/tests/test_pieces_states.py
@@ -0,0 +1,58 @@
+
+from unittest import TestCase
+
+from memory_lib.model import PieceState
+from memory_lib.piece_state import PieceStateStateMachine, state_0, StateMachineInputPiece, state_1, state_2, state_3, state_4
+
+
+class TestPiecesStates(TestCase):
+
+    def test_state_1(self):
+        pieces_states = [PieceState.ON_BOARD, PieceState.ON_BOARD, PieceState.OFF_BOARD]
+        expected_states = [state_1, state_1, state_4]
+        expected_outputs = [False, False, True]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+        pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN]
+        expected_states = [state_1, state_3]
+        expected_outputs = [False, False]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+    def test_state_2(self):
+        pieces_states = [PieceState.UNKNOWN, PieceState.UNKNOWN, PieceState.OFF_BOARD]
+        expected_states = [state_2, state_2, state_4]
+        expected_outputs = [False, False, False]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+        pieces_states = [PieceState.UNKNOWN, PieceState.ON_BOARD]
+        expected_states = [state_2, state_1]
+        expected_outputs = [False, False]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+    def test_state_3(self):
+        pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN, PieceState.UNKNOWN, PieceState.OFF_BOARD]
+        expected_states = [state_1, state_3, state_3, state_4]
+        expected_outputs = [False, False, False, True]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+        pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN, PieceState.ON_BOARD]
+        expected_states = [state_1, state_3, state_1]
+        expected_outputs = [False, False, False]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+    def test_state_4(self):
+        pieces_states = [PieceState.OFF_BOARD, PieceState.UNKNOWN, PieceState.OFF_BOARD, PieceState.ON_BOARD]
+        expected_states = [state_4, state_4, state_4, state_1]
+        expected_outputs = [False, False, False, False]
+        self._test_state(pieces_states, expected_states, expected_outputs)
+
+    def _test_state(self, pieces_states: list[PieceState], expected_states: list[StateMachineInputPiece], expected_outputs: list[bool]) -> None:
+        expected_index = 0
+        state_machine = PieceStateStateMachine()
+        for piece_state in pieces_states:
+            state_machine_input = StateMachineInputPiece(piece_state)
+            need_to_trigger = state_machine.next(state_machine_input)
+            self.assertEqual(expected_states[expected_index], state_machine.current_state)
+            self.assertEqual(expected_outputs[expected_index], need_to_trigger)
+            expected_index += 1
+
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b055a1807504afa845874e1f3ec46daaeec0028a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.pyright]
+venvPath = "."
+venv = "venv"