diff --git a/code/connect4.html b/code/connect4.html deleted file mode 100644 index 40db0e1..0000000 --- a/code/connect4.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - Vier gewinnt - - - - - - - - - - - -

Spieler ist am Zug.

-
- - - - - - diff --git a/code/fragments.js b/code/fragments.js new file mode 100644 index 0000000..9a019e0 --- /dev/null +++ b/code/fragments.js @@ -0,0 +1,53 @@ + +import { render } from "./lib/suiweb.js" + +let state = { + board: [ + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ] + ], + next: 'r' +} + +let stateSeq = [] + + +// Components +// +const App = () => [Board, {board: state.board}] + +const Board = ({board}) => { + let flatBoard = [].concat(...board) + let fields = flatBoard.map((type) => [Field, {type}]) + return ( + ["div", {className: "board"}, ...fields] + ) +} + +const Field = ({type}) => { + // ... +} + + +// Show board: +// render [App] +// +function showBoard () { + const app = document.querySelector(".app") + render([App], app) + return app +} + + + +// document.querySelector("button.undo").addEventListener("click", () => { +// if (stateSeq.length > 0) { +// state = stateSeq.pop() +// showBoard() +// } +// }) + diff --git a/code/index.js b/code/index.js index 777456c..55f979c 100644 --- a/code/index.js +++ b/code/index.js @@ -25,7 +25,7 @@ function guidGenerator() { } // Statische Dateien im Verzeichnis public -app.use(express.static('public')) +app.use(express.static(__dirname + "/public")) // API-Key überprüfen // @@ -113,7 +113,15 @@ app.use(function(err, req, res, next){ app.use(function(req, res){ res.status(404) res.send({ error: "not found" }) + console.log("sending 404") + console.log(req) }) app.listen(3000) -console.log('Express started on port 3000') \ No newline at end of file +console.log('Express started on port 3000') + + +const fs = require('fs') +const path = __dirname + "/public/styles.css" +console.log(fs.existsSync(path)) +console.log(path) diff --git a/code/public/connect4.html b/code/public/connect4.html index f3469a4..f7bb1ea 100644 --- a/code/public/connect4.html +++ b/code/public/connect4.html @@ -6,213 +6,24 @@ - -
+
- - - - - + + + + + +

's turn

- diff --git a/code/public/gamelogic.js b/code/public/gamelogic.js new file mode 100644 index 0000000..c61502b --- /dev/null +++ b/code/public/gamelogic.js @@ -0,0 +1,224 @@ + +/* + * This solution sould be considered as a proof of concept – the code + * definitely needs some cleanup and documentation + */ +import { render } from "./lib/suiweb.js" + +let datakey = '' +let state = { + board: [ + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ], + [ '', '', '', '', '', '', '' ] + ], + next: 'blue' +} +let stateSeq = [] + +const App = () => [Board, {board: state.board}] + +const Board = ({board}) => { + let flatBoard = [].concat(...board) + let fields = flatBoard.map((type) => [Field, {type}]) + return ( + ["div", {className: "board"}, ...fields] + ) +} + +const Field = ({type}) => { + return ( + ["div", {className: "field"}, + ["div", {className: ("piece " + type)}]] + ) +} + +const url = "http://localhost:3000/" +const SERVICE = "http://localhost:3000/api/data/c4state?api-key=c4game" + + +// Initialize game +// +function initGame () { + console.log("initializing game") + fetch(url + "api/data/" + datakey + "?api-key=c4game", { + method: 'POST', + headers: { 'Content-type': 'application/json' }, + body: JSON.stringify(state) + }).then(response => response.json()) + .then(data => { + datakey = data.id + }) + let board = showBoard() + attachEventHandler(board) + + document.getElementById("loadFromServer").addEventListener("click", event => {loadStateFromServer() }) + document.getElementById("saveToServer").addEventListener("click", event => {saveStateToServer() }) + document.getElementById("loadFromStorage").addEventListener("click", event => {loadStateFromLocalStorage() }) + document.getElementById("saveToStorage").addEventListener("click", event => {saveStateToLocalStorage() }) + document.getElementById("clearStorage").addEventListener("click", event => {clearLocalStorage() }) + document.getElementById("undo").addEventListener("click", event => {undo() }) + +} + + +// Show board + +function showBoard () { + + console.log("showing board") + let actualPlayerText = document.getElementById("actualPlayer") + if(actualPlayerText){ + actualPlayerText.innerText = state.next + } + const app = document.querySelector(".app") + render([App], app) + return app +} + + +// Attach event handler to board +// +function attachEventHandler (board) { + console.log("attaching Eventhandler") + // your implementation + board.addEventListener("click", event => { + let field = event.target + if(field.classList.contains("piece")){ + field = field.parentNode + } + let column = Array.prototype.indexOf.call(field.parentNode.children, field) % state.board[1].length + console.log("calculated column", column) + setColumn(column) + + showBoard() + }) +} + +function setColumn(column) { + console.log("setting Column") + let row = state.board.length - 1 + const column_number = column + while(row >= 0){ + if(state.board[row][column_number] !== ''){ + row-- + } else { + if(state.next === ''){ + console.log("not setting Field becuase there is a winner.") + } else { + setField(row, column_number) + } + break + } + } + } + + function setField(row, column) { + stateSeq.push(state) + state = setInObj(state, "board", setInList(state.board, row, setInList(state.board[row], column, state.next))) + //state.board[row][column] = state.next + showBoard() + if(connect4Winner(state.next, state.board)){ + document.getElementById("state-text").innerText = "Player " + state.next + " won!" + state = setInObj(state, "next", "") + //state.next = '' + } else { + switchNextColor() + } + } + + function switchNextColor() { + if(state.next === "red") { + state = setInObj(state, "next", "blue") + //state.next = "blue" + } else { + state = setInObj(state, "next", "red") + //state.next = "red" + } + } + + +// Get current state from server and re-draw board +// +function loadStateFromServer () { + // ... + // your implementation + // ... + console.log("loading State from Server") + fetch(url + "api/data/" + datakey + "?api-key=c4game", { + method: 'GET', + headers: { 'Content-type': 'application/json' } + }).then(response => response.json()) + .then(data => { + state = data + stateSeq = [] + showBoard() + }) +} + +// Put current state to server +// +function saveStateToServer () { + console.log("saving state To Server") + fetch(url + "api/data/" + datakey + "?api-key=c4game", { + method: 'PUT', + headers: { 'Content-type': 'application/json' }, + body: JSON.stringify(state) + }).then(response => response.json()) + .then(data => { + state = data + showBoard() + }) +} + +function loadStateFromLocalStorage() { + console.log("loading State from LocalStorage") + if(localStorage.getItem("state")){ + state = JSON.parse(localStorage.getItem("state")) + stateSeq = [] + } else { + console.log("no saved State in Localstorage") + } + showBoard() +} + +function saveStateToLocalStorage() { + console.log("saving State to LocalStorage") + localStorage.setItem("state", JSON.stringify(state)) +} + +function clearLocalStorage() { + console.log("clearing localStorage") + localStorage.clear() +} + +function undo() { + if(stateSeq.length > 0) { + console.log("undo") + state = stateSeq.pop() + showBoard() + } +} + +function setInList(lst, idx, val) { + const newList = [] + lst.forEach(element => { + newList.push(element) + }); + newList[idx] = val + return newList +} + +function setInObj(obj, attr, val) { + const newObject = {} + Object.keys(obj).forEach(key => { + newObject[key] = obj[key] + }) + newObject[attr] = val + return newObject +} + +export {initGame} \ No newline at end of file diff --git a/code/public/lib/suiweb.js b/code/public/lib/suiweb.js new file mode 100644 index 0000000..c50bf30 --- /dev/null +++ b/code/public/lib/suiweb.js @@ -0,0 +1,336 @@ +/** + * SuiWeb + * Simple User Interface Tool for Web Exercises + * + * @author bkrt + * @version 0.3.4 + * @date 11.12.2021 + * + * 0.3.4 - only null or undefined qualify as uninitialized state + * 0.3.3 - parseSJDON rewritten to use createElement + * - flatten children array for props.children in JSX + * 0.3.2 - save and restore of active element improved + * 0.3.1 - parseSJDON rewritten + * 0.3.0 - style property + * 0.2.3 - ES6 modules + * 0.2.2 - component state and hooks + * 0.2.1 - parse SJDON rewritten, function components + * 0.2.0 - render single SJDON structures to DOM + * + * Based on ideas and code from + * Rodrigo Pombo: Build your own React + * https://pomb.us/build-your-own-react/ + * + * Thanks to Rodrigo Pombo for a great tutorial and for sharing the + * code of the Didact library. Didact is a much more sophisticated + * re-implementtation of React's basics than the simple SuiWeb. + */ + +/* ===================================================================== + * SJDON - Conversion + * ===================================================================== +*/ + +// parseSJDON: convert SJDON to createElement calls +// +// note: this function can also help to use React with SJDON syntax +// +// to simplify calls add something like this to your components: +// let s = (data) => reactFromSJDON(data, React.createElement) +// +function parseSJDON ([type, ...rest], create=createElement) { + const isObj = (obj) => typeof(obj)==='object' && !Array.isArray(obj) + const children = rest.filter(item => !isObj(item)) + const repr = create(type, + Object.assign({}, ...rest.filter(isObj)), + ...children.map(ch => Array.isArray(ch) ? parseSJDON(ch, create) : ch) + ) + repr.sjdon = children + return repr +} + + +// create an element representation +// +function createElement(type, props, ...children) { + return { + type, + props: { + ...props, + children: children.flat().map(child => + typeof child === "object" + ? child + : createTextElement(child) + ), + }, + } +} + + +// create a text element representation +// +function createTextElement(text) { + return { + type: "TEXT_ELEMENT", + props: { + nodeValue: text, + children: [], + }, + } +} + +/* ===================================================================== + * Render node tree to DOM + * ===================================================================== +*/ + +// global context +let contextStore = createStore(); + + +// remove children and render new subtree +// +function render(element, container) { + while (container.firstChild) { + container.removeChild(container.lastChild); + } + renderInit(element, container, 0); +} + + +// render subtree +// and call effects after rendering +// +function renderInit(element, container, n, childIndex) { + contextStore("effects", []); + + // save focus and cursor position of active element + let [focusSelector, position] = getFocusInput(); + + // ** render the element ** + renderElem(element, container, n, childIndex); + + // restore focus and cursor position of active element + setFocusInput(focusSelector, position); + + // run effects + contextStore("effects").forEach(fun => fun()); + contextStore("effects", []); +} + + +// render an element +// - if it is in SJDON form: parse first +// - render function or host component +// +function renderElem(element, container, n, childIndex) { + if (Array.isArray(element)) { + element = parseSJDON(element); + } + + if (element.type instanceof Function) { + updateFunctionComponent(element, container, n, childIndex); + } else { + updateHostComponent(element, container, childIndex); + } +} + + +// function component +// - run function to get child node +// - render child node +// +function updateFunctionComponent(element, container, n, childIndex) { + // save re-render function to context + contextStore("re-render", () => renderInit(element, container, n, n)); + let children = element.sjdon ?? element.props.children; + let node = element.type({...element.props, children}); + renderElem(node, container, n, childIndex); +} + + +// host component +// - create dom node +// - assign properties +// - render child nodes +// - add host to dom +// +function updateHostComponent(element, container, childIndex) { + + // create DOM node + const dom = + element.type == "TEXT_ELEMENT" + ? document.createTextNode("") + : document.createElement(element.type) + + // assign the element props + const isProperty = key => key !== "children" + Object.keys(element.props) + .filter(isProperty) + .forEach(name => { + if (name=="style") { + updateStyleAttribute(dom, element.props.style); + } else { + dom[name] = element.props[name]; + } + }) + + // render children + element.props.children.forEach((child, index) => { + renderElem(child, dom, index); + }); + + if (typeof(childIndex) == 'number') { + // re-render: replace node + container.replaceChild(dom, container.childNodes[childIndex]); + } else { + // add node to container + container.appendChild(dom); + } +} + + +// update style attribute, value can be: +// - a CSS string: set to style attribute +// - an object: merged to style attribute +// - an array of objects: merged to style attribute +// +function updateStyleAttribute(dom, styles) { + if (typeof(styles)=="string") { + dom.style = styles; + } else if (Array.isArray(styles)) { + Object.assign(dom.style, ...styles); + } else if (typeof(styles)=="object") { + Object.assign(dom.style, styles); + } +} + + +/* ===================================================================== + * Handling state + * ===================================================================== +*/ + +// element state +let stateHooks = createStore(); + + +// state hook +// - access state via id and key +// - return state and update function +// +function useState(id, key, init) { + let idKey = "id:" + id + "-key:" + key; + + // prepare render function + let renderFunc = contextStore("re-render"); + + // define function to update state + function updateValue(updateFun, rerender=true) { + stateHooks(idKey, updateFun(stateHooks(idKey))); + if (rerender) renderFunc(); + } + + // new state: set initial value + if ([undefined, null].includes(stateHooks(idKey))) { + stateHooks(idKey, init); + } + + return [stateHooks(idKey), updateValue]; +} + + +// effect hook +// add function to effects array +// +function useEffect(fun) { + contextStore("effects", [...contextStore("effects"), fun]); +} + + +// create a key-value-store +// return accessor function +// +function createStore() { + let data = {}; + function access(key, ...value) { + if (value.length === 0) { + return data[key]; + } else { + data[key] = value[0]; + return value[0]; + } + } + return access; +} + + +/* ===================================================================== + * Get and set focus and position in certain elements + * Note: this is a quick&dirty solution that probably fails when + * elements are added or removed + * ===================================================================== +*/ + +// create a CSS selector for a given node +// +function getSelectorOf(node) { + let selector = "" + while (node != document.body) { + if (selector != "") selector = ">" + selector + + if (node.id) { + selector = "#"+node.id + selector + break + } + + let index = Array.from(node.parentNode.children) + .filter(item=>item.tagName==node.tagName) + .findIndex(item=>item==node) + + selector = node.tagName + ":nth-of-type(" + (index+1) + ")" + selector + node = node.parentNode + } + return selector +} + +// find a selector for the element that has focus and the cursor +// position in the element +// +function getFocusInput() { + const active = document.activeElement; + let sel = active ? getSelectorOf(active) : undefined + let position = active ? active.selectionStart : undefined; + return [sel, position]; +} + +// set focus to an element in a list of elements matching a +// selector and position cursor in the element +// +function setFocusInput(selector, position) { + if (selector && typeof(selector) == 'string') { + console.log("Sel:"+selector) + let el = document.querySelector(selector); + if (el) el.focus(); + if (el && "selectionStart" in el + && "selectionEnd" in el + && position !== undefined) { + el.selectionStart = position; + el.selectionEnd = position; + } + } +} + + +/* ===================================================================== + * Module export + * ===================================================================== +*/ + +export { render, createElement, useState, useEffect }; + + +/* ===================================================================== + * EOF + * ===================================================================== +*/