diff --git a/game_manager.js b/game_manager.js
new file mode 100644
index 0000000..6f726c4
--- /dev/null
+++ b/game_manager.js
@@ -0,0 +1,11 @@
+class GameManager
+{
+ constructor(grid)
+ {
+ this.grid = grid;
+
+ this.dummy_player = new Player("test_player", "rgb(0,128,0)");
+
+ this.grid.player_change_listener(this.dummy_player);
+ }
+}
\ No newline at end of file
diff --git a/grid.js b/grid.js
new file mode 100644
index 0000000..5301866
--- /dev/null
+++ b/grid.js
@@ -0,0 +1,124 @@
+class Grid
+{
+ constructor(n, grid_container_div, tile_width, tile_height, ground_color)
+ {
+ this.n = n;
+ this.grid_container_div = grid_container_div;
+ this.tile_width = tile_width;
+ this.tile_height = tile_height;
+ this.ground_color = ground_color;
+
+ this.subgrids = []
+
+ console.log("create grid of size " + this.n);
+
+ this.create();
+ }
+
+ create()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ var div_row = this.grid_container_div.appendChild(document.createElement("div"));
+ var row = [];
+ for (x = 0; x < this.n; x++)
+ {
+ var div_subgrid = div_row.appendChild(document.createElement("div"));
+ div_subgrid.className = "subgrid-container";
+
+ row.push(new Subgrid(this.n, x,y,div_subgrid,this.tile_width, this.tile_height, this.ground_color));
+ row[x].register_click_callback((i,j,k,l) => this.click_listener(i,j,k,l));
+
+ }
+
+ this.subgrids.push(row);
+ }
+ }
+
+ register_click_callback(func)
+ {
+ this.click_callback = func;
+ }
+
+
+ click_listener(sub_x, sub_y, x,y)
+ {
+ this.click_callback(sub_x, sub_y, x,y);
+ }
+
+ player_change_listener(player)
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].player_change_listener(player);
+ }
+ }
+ }
+
+ activate_all()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].activate_all();
+ }
+ }
+ }
+
+ deactivate_all()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].deactivate_all();
+ }
+ }
+ }
+
+ block()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].block();
+ }
+ }
+ }
+
+ unblock()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].unblock();
+ }
+ }
+ }
+
+ on_screen_orientation_change(tile_w,tile_h)
+ {
+ this.tile_width = tile_w;
+ this.tile_h = tile_h;
+
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.subgrids[y][x].on_screen_orientation_change(tile_w, tile_h);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6fd0013
--- /dev/null
+++ b/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ ultimate tictactoe
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..6334317
--- /dev/null
+++ b/main.js
@@ -0,0 +1,13 @@
+var style = getComputedStyle(document.body);
+var n = style.getPropertyValue("--tictactoe_n");
+var tilesize = style.getPropertyValue("--tile-size");
+var default_opacity = style.getPropertyValue("--opacity");
+var ground_color = style.getPropertyValue("--ground-color");
+
+var grid = new Grid(n, document.getElementById("grid-container"), tilesize, tilesize, ground_color);
+var game_manager = new GameManager(grid);
+
+window.addEventListener("resize", function() {
+ var tilesize = getComputedStyle(document.body).getPropertyValue("--tile-size");
+ grid.on_screen_orientation_change(tilesize, tilesize);
+})
diff --git a/player.js b/player.js
new file mode 100644
index 0000000..0c6f44b
--- /dev/null
+++ b/player.js
@@ -0,0 +1,30 @@
+class Player
+{
+ constructor(name, color)
+ {
+ this.name = name;
+ this.color = color;
+ }
+
+ set_name(name)
+ {
+ this.name = name;
+ }
+
+ set_color(color)
+ {
+ this.color = color;
+ }
+
+ get_name()
+ {
+ return this.name
+ }
+
+ get_color()
+ {
+ return this.color;
+ }
+
+
+}
\ No newline at end of file
diff --git a/sidebar.js b/sidebar.js
new file mode 100644
index 0000000..e69de29
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..241bfbf
--- /dev/null
+++ b/style.css
@@ -0,0 +1,95 @@
+html, body {
+ overflow-x: hidden;
+}
+
+body {
+ margin: 0;
+ background: rgb(39, 34, 39);
+ outline: 0;
+ position: relative;
+}
+
+:root{
+ --tictactoe_n: 3;
+ --button-margin: 0.5vh;
+ --tile-size: calc((100vh / (var(--tictactoe_n) * var(--tictactoe_n))) - 2 * var(--button-margin));
+ --opacity: 0.2;
+ --border-radius: 1vh;
+ --sidebar-width: 25vh;
+ --ground-color: rgb(58, 67, 73);
+ --board-size: calc(var(--tictactoe_n) * var(--tictactoe_n) * var(--tile-size) + var(--tictactoe_n) * var(--button-margin) * 4);
+ --sidebar-height: none; /* unused if not in portrait mode */
+}
+
+/* override settings on portrait mode: */
+@media (orientation: portrait) {
+
+ :root{
+ --button-margin: 0.5vw;
+ --tile-size: calc((100vw / (var(--tictactoe_n) * var(--tictactoe_n))) - 2 * var(--button-margin));
+ --border-radius: 1vw;
+ --sidebar-width: 40vw;
+ --sidebar-height: 15vw;
+ }
+
+}
+
+.main-container {
+ text-align: center;
+ white-space: nowrap;
+ padding-top: calc(50vh - 0.5 * (var(--grid-height) * var(--tile-size)) - var(--border-radius));
+}
+
+/* override settings on portrait mode: */
+@media (orientation: portrait) {
+
+ .main-container {
+ white-space: normal;
+ padding-top: calc(50vh - 0.5 * (var(--grid-height) * var(--tile-size) + 2 * var(--sidebar-height)) - 2 * var(--border-radius));
+ }
+
+}
+
+.grid-tile {
+ position: absolute;
+ width: var(--tile-size);
+ height: var(--tile-size);
+ float: center;
+ background: none;
+}
+
+.grid-button {
+ border-radius: var(--button-margin);
+ margin: var(--button-margin);
+ width: calc(var(--tile-size) - (var(--button-margin) * 2));
+ height: calc(var(--tile-size) - (var(--button-margin) * 2));
+ display: inline-block;
+ outline: 0;
+ transition-duration: 0.1s;
+}
+
+.grid-button:focus {
+ outline: 0!important;
+}
+
+.grid-container {
+ border-radius: var(--border-radius);
+ padding: var(--border-radius);
+ margin: auto;
+ width: calc(var(--board-size));
+ height: calc(var(--board-size));
+ background: rgba(0, 0, 0, var(--opacity));
+ display: inline-block;
+ transition-duration: 0.5s;
+}
+
+.subgrid-container {
+ border-radius: var(--border-radius);
+ padding: var(--border-radius);
+ margin: auto;
+ width: calc(var(--tictactoe_n) * var(--tile-size));
+ height: calc(var(--tictactoe_n) * var(--tile-size));
+ background: rgba(0, 0, 0, var(--opacity));
+ display: inline-block;
+ transition-duration: 0.5s;
+}
diff --git a/subgrid.js b/subgrid.js
new file mode 100644
index 0000000..41fb2e8
--- /dev/null
+++ b/subgrid.js
@@ -0,0 +1,144 @@
+class Subgrid
+{
+ constructor(n, sub_x, sub_y,subgrid_container_div, tile_width, tile_height, ground_color)
+ {
+ this.n = n;
+ this.subgrid_container_div = subgrid_container_div;
+ this.tile_height = tile_height;
+ this.tile_width = tile_width;
+
+ this.cells = [];
+
+ this.sub_x = sub_x;
+ this.sub_y = sub_y;
+
+ this.ground_color = ground_color;
+
+ this.create();
+
+ console.log("new subgrid created");
+ }
+
+ create()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ var div_row = this.subgrid_container_div.appendChild(document.createElement("div"));
+ var row = [];
+ for (x = 0; x < this.n; x++)
+ {
+ var div_button = div_row.appendChild(document.createElement("div"));
+ div_button.className = "grid-tile";
+ var button = document.createElement("button");
+ button.className = "grid-button";
+ div_button.appendChild(button);
+
+ var b_x = x;
+ var b_y = y;
+ var b_w = this.tile_width;
+ var b_h = this.tile_height;
+
+ row.push(new Tile(b_x, b_y, b_w, b_h, button, this.ground_color, div_button));
+ // TODO: register listener?
+ row[x].register_click_callback((i,j) => this.click_listener(i,j));
+
+ }
+ this.cells.push(row);
+ }
+ }
+
+ register_click_callback(func)
+ {
+ this.click_callback = func;
+ }
+
+
+ click_listener(x,y)
+ {
+ this.click_callback(this.sub_x, this.sub_y, x, y);
+ }
+
+ player_change_listener(player)
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].player_change_listener(player);
+ }
+ }
+ }
+
+ unlock_request_listener(x,y)
+ {
+ // TODO: unused so far
+
+ //return this.unlock_request_callback(x,y);
+ return false;
+ }
+
+ activate_all()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].activate();
+ }
+ }
+ }
+
+ deactivate_all()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].deactivate();
+ }
+ }
+ }
+
+ block()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].lock();
+ }
+ }
+ }
+
+ unblock()
+ {
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].unlock();
+ }
+ }
+ }
+
+ on_screen_orientation_change(tile_w,tile_h)
+ {
+ this.tile_width = tile_w;
+ this.tile_h = tile_h;
+
+ var x,y;
+ for (y = 0; y < this.n; y++)
+ {
+ for (x = 0; x < this.n; x++)
+ {
+ this.cells[y][x].update_css_transform(tile_w, tile_h);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tile.js b/tile.js
new file mode 100644
index 0000000..281d737
--- /dev/null
+++ b/tile.js
@@ -0,0 +1,130 @@
+class Tile
+{
+ constructor(x,y,w,h, elem, ground_color, div)
+ {
+ this.x = x;
+ this.y = y;
+
+ this.w = w;
+ this.h = h;
+
+ // reference to element
+ this.elem = elem;
+ this.ground_color = ground_color;
+ this.div = div;
+
+ this.locked = false;
+
+ this.div.style.transform = "translate( calc( " + x + "*" + w + "), calc(" + y + "*" + h + "))";
+
+ this.player = null;
+ this.reset();
+ this.bind();
+
+ }
+
+ update_css_transform(w,h)
+ {
+ this.w = w;
+ this.h = h;
+ this.div.style.transform = "translate( calc( " + this.x + "*" + this.w + "), calc(" + this.y + "*" + this.h + "))";
+ }
+
+ register_click_callback(func)
+ {
+ this.click_callback = func;
+ }
+
+ register_unlock_request_callback(func)
+ {
+ this.unlock_request_callback = func;
+ }
+
+ player_change_listener(player)
+ {
+ this.player = player;
+ }
+
+ bind()
+ {
+ this.elem.addEventListener("mouseenter", c => this.on_mouseenter(c));
+ this.elem.addEventListener("mouseleave", c => this.on_mouseleave(c));
+ this.elem.addEventListener("mousedown", c => this.on_click(c));
+
+ // TODO: bind player listener
+ }
+
+ reset()
+ {
+ this.elem.style.backgroundColor = this.ground_color;
+ this.elem.style.opacity = 1;
+ this.deactivate();
+ }
+
+ lock()
+ {
+ this.locked = true;
+ }
+
+ unlock()
+ {
+ this.locked = false;
+ }
+
+ deactivate()
+ {
+ this.is_activated = false;
+ this.elem.style.opacity = 0.3;
+ this.elem.style.border = "none";
+ }
+
+ activate()
+ {
+ this.is_activated = true;
+ this.elem.style.background = this.player.get_color();
+ this.elem.style.opacity = 1.0;
+ this.elem.style.border = "2px solid rgba(255,255,255,0.3)";
+ }
+
+ on_mouseenter()
+ {
+ if (!this.is_activated && !this.locked)
+ {
+ this.elem.style.background = this.player.get_color();
+ this.elem.style.opacity = 0.7;
+ this.elem.style.border = "1px solid rgba(255,255,255,0.3)";
+ }
+ }
+
+ on_mouseleave()
+ {
+ if (!this.is_activated && !this.locked)
+ {
+ this.elem.style.background = this.ground_color;
+ this.elem.style.opacity = 0.3;
+ this.elem.style.border = "none";
+ }
+ }
+
+ on_click()
+ {
+ if (this.locked)
+ {
+ // just check whether we can request an unlock. if not we're
+ // doing nothing!
+ if (!this.unlock_request_callback(this.x, this.y))
+ {
+ return;
+ }
+ }
+
+ var active_before = this.is_activated;
+ this.activate();
+ if (!active_before)
+ {
+ this.click_callback(this.x, this.y);
+ }
+
+ }
+
+}
\ No newline at end of file