You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
429 lines
25 KiB
429 lines
25 KiB
---
|
|
gitea: none
|
|
include_toc: true
|
|
---
|
|
|
|
# Assignment
|
|
|
|
Source: https://www.programmfabrik.de/en/assignment-frontend-backend-developer-job-berlin/
|
|
|
|
> Code a tic-tac-toe game
|
|
>
|
|
> Depending on the job you are applying for, you can code in Javascript (ECMA) or C++.
|
|
>
|
|
> Requirements
|
|
>
|
|
> * Use your own code only and start from scratch
|
|
> * Player can choose the opponent to be human or computer
|
|
> * Use [L]GPL'ed libraries if necessary, please include copyright notes
|
|
> * Let us know how long it took you to code the game
|
|
>
|
|
> JavaScript version
|
|
>
|
|
> * Implement in Javascript so that it works in Mozilla Firefox
|
|
> * Make use of CSS, provide nice visuals
|
|
> * Make the back button work
|
|
>
|
|
> C++ version
|
|
>
|
|
> * Implement in C++, so that i works on the command line under Linux
|
|
> * The opponent has to be unbeatable
|
|
|
|
# Solution
|
|
|
|
## Features
|
|
|
|
* Does not have any client-side dependencies, does not use any third-party libraries on the client.
|
|
* Works in modern browsers as a single-page application, in offline mode.
|
|
* Works in older browsers / browsers with JS disabled as a plain old multi-page application (requiring internet connection).
|
|
* Hopefully more accessible than a typical single-page application.
|
|
* Supports multiple games on the same page, playable at the same time, concurrently and independently.
|
|
* Supports different game rules (the provided main page has two games with different rule sets).
|
|
* Supports custom board size.
|
|
* The computer opponent is unbeatable.
|
|
* The user can choose for any player to become a computer opponent at any time.
|
|
* The user can even choose for both players to become computer opponents and let them finish the game.
|
|
* Browser back and forward buttons work as undo / redo buttons for user moves (and other user actions).
|
|
* Visuals are acceptable.
|
|
|
|
## Screenshot
|
|
|
|
![screenshot](docs/screenshot.png)
|
|
|
|
## Usage
|
|
|
|
Tested to work on Node.js v20.15.1, should probably also work on all v20 and v22.
|
|
Not tested with earlier Node.js versions.
|
|
Node.js v23.2.0 (unstable; probably other releases of Node.js v23 as well) is known to result in errors inside tests runner (in third-party code).
|
|
|
|
* `npm ci` to install dependencies;
|
|
* `npm run check-and-start` to run linting, typechecking, tests, build everything and serve it from `PORT` environment variable or from port 3000.
|
|
* `npm run build-and-start` to only build everything and serve (if linting, typechecking or tests are failing for some reason).
|
|
|
|
(Yes, strictly speaking it's not just a static webpage with a bunch of client-side scripts.
|
|
It's better. See "User interface" section in "Design" for more details.)
|
|
|
|
## Design
|
|
|
|
### Project layout
|
|
|
|
A monorepo would complicate things, so everything here is in a single package.
|
|
|
|
* `src/backend` for code that's running on backend only;
|
|
* `src/frontend` for code (and static assets) that's running in browser only;
|
|
* `src/shared` for code that's running on both.
|
|
|
|
No bundlers are used; all TS (and TSX) code is compiled to JS, preserving module imports;
|
|
and then files in `src/frontend` and `src/shared` are served at `/frontend` and `/shared` respectively.
|
|
|
|
### User interface / handling interactions
|
|
|
|
Of course the obvious choice would be to implement everything in React or another similar framework,
|
|
but where is the fun in that?
|
|
I wanted to minimise the size of the resulting frontend application,
|
|
and the best way to reduce the number of dependencies on frontend is to not have any dependencies,
|
|
to write vanilla JS.
|
|
|
|
I've been hearing a lot about Web Components lately,
|
|
so I decided to use this as an opportunity to learn more about them.
|
|
But there are some major design choices to be made with Web Components too,
|
|
different people use them in different ways.
|
|
|
|
I liked what I've read at https://adactio.com/journal/20618,
|
|
https://toot.cat/@zkat/113134268521905449 and https://toot.cat/@zkat/113034798494038446,
|
|
so I decided to use them to augment / enhance regular static HTML.
|
|
I'm not sure if I was successful in trying to follow this approach, but it works!
|
|
|
|
So the key ideas are:
|
|
* Store game state in a query string parameter;
|
|
make every interactable element on the page a button submitting this state;
|
|
* On backend, when the page is requested, generate a complete HTML page for this game state;
|
|
* On frontend, when JS is enabled and Web Components are supported:
|
|
intercept button clicks and update the URL accordingly on the client side;
|
|
* On frontend, when JS is enabled and Web Components are supported:
|
|
subscribe to URL changes and update the DOM accordingly
|
|
(which in most cases (except for resizing the board)
|
|
only involves setting some class names and button values and inner texts).
|
|
|
|
As a result:
|
|
* On the first visit, a complete fully working HTML page is served, and can be interacted with immediately.
|
|
* If JS is disabled, everything will still work!
|
|
You click on a button, it submits a form, server generates another updated fully working HTML.
|
|
* But if JS is enabled, magic happens, you don't even need the network connection anymore.
|
|
You click on a button, client-side script updates the URL,
|
|
another client-side script notices this and updates the gaming board.
|
|
* When you click on back/forward browser buttons with JS disabled,
|
|
server is going to serve fully working HTML every time for the relevant game state.
|
|
* When you click on back/forward browser buttons with JS enabled,
|
|
client-side script notices this just the same as if you clicked on a game button,
|
|
and updates the gaming board accordingly.
|
|
|
|
Plus some additional glue to make the components independent from each other,
|
|
so that there can be many games on the same page at the same time,
|
|
all in varying states, without conflicting or competing with each other.
|
|
|
|
And, since I had to generate HTML on the server, I needed a templating library for that.
|
|
While I'm not using React on frontend here, JSX is a really nice language with good IDE support,
|
|
so I decided to use it on backend, just as a template engine.
|
|
|
|
Main components of interest:
|
|
|
|
#### Progressive form
|
|
|
|
The idea: no matter whether JS is enabled or disabled, no matter whether the browser supports Web Components or not,
|
|
submitting the form should result in page URL being updated with all the new parameters overriding the old ones
|
|
(while the old parameters not present in the form should stay intact).
|
|
|
|
Additionally, if JS is enabled and Web Components are supported, this should happen entirely on the client,
|
|
without any network requests.
|
|
|
|
In order to achieve this, the form must be submitted with method=POST
|
|
(so that the current query parameters are not all erased),
|
|
and then either the server will merge current query parameters with submitted data
|
|
and redirect the client to the page with the new query parameters,
|
|
or the client-side web component will do the merging.
|
|
|
|
* Server part is implemented in [`src/backend/progressive-form.ts`](src/backend/progressive-form.ts).
|
|
* Client part is implemented in [`src/frontend/components/progressive-form.ts`](src/frontend/components/progressive-form.ts).
|
|
* Usage example is in [`src/backend/components/counter.tsx`](src/backend/components/counter.tsx).
|
|
|
|
#### Game board
|
|
|
|
The idea: minimize the amount of DOM tree changes, and minimize the amount of duplicate code between frontend and backend.
|
|
|
|
So we have the main game element, to which a bunch of classes are applied dynamically,
|
|
depending on the current state of the game.
|
|
And various blocks of text / buttons which are displayed or not depending on the classes applied to the main element,
|
|
according to CSS stylesheet.
|
|
This helps to avoid having to store any texts in client-side JS; everything is already present in the generated HTML.
|
|
|
|
Additionally, the board itself is just a table with a button in every cell.
|
|
And these buttons get their values and texts set dynamically,
|
|
and get enabled (when it's a valid move) or disabled (when the cell is occupied, or when the game is over) automatically.
|
|
|
|
* Server part is implemented in [`src/backend/components/boardgame.tsx`](src/backend/components/boardgame.tsx).
|
|
* Client part is implemented as web component in [`src/frontend/components/board-game.ts`](src/frontend/components/board-game.ts).
|
|
* Shared part is implemented in [`src/shared/display.ts`](src/shared/display.ts).
|
|
|
|
#### Computer opponent
|
|
|
|
The idea: if the game is configured by user to use a computer player, and it's the move of a computer player,
|
|
then compute the board state _after_ the move of a computer player,
|
|
and redirect the user to this new board state (in client-server mode)
|
|
or replace the location with this new board state (in offline SPA mode),
|
|
without leaving a trace of the intermediate state (when it was computer player's turn) in the history,
|
|
so that the history back/forward button only work as undo/redo for user actions
|
|
(with subsequent computer actions coupled to user actions that caused them).
|
|
|
|
* Server part is implemented in [`src/backend/main/boardgame-handler.ts`](src/backend/main/boardgame-handler.ts)
|
|
* Client part is implemented in the same [`src/frontend/components/board-game.ts`](src/frontend/components/board-game.ts) (see call to `replaceLocation`).
|
|
* Shared part is implemented in [`src/shared/gameplay/boardgame.ts`](src/shared/gameplay/boardgame.ts).
|
|
|
|
### Gameplay
|
|
|
|
Mostly defined in [`src/shared/datatypes`](src/shared/datatypes), [`src/shared/gameplay`](src/shared/gameplay) and [`src/shared/game-variants`](src/shared/game-variants).
|
|
|
|
The main idea is that every possible state of the board belongs to one of the four categories:
|
|
player X won, player Y won, undecided (neither won but moves can still be made)
|
|
and draw (neither won and moves can no longer be made because the board is full).
|
|
|
|
Different game rulesets differ in how they define the win.
|
|
|
|
And knowing which states are final in which way, we can walk through all possible gameplays
|
|
(since thankfully the game graph is cycle-free, because on every move the number of free squares decreases until it reaches zero)
|
|
to determine the eventual game outcome assuming perfect play, and the moves left until this outcome.
|
|
|
|
For example, if it's X move now, and by adding X to one of the free squares we can achieve the state "X won",
|
|
this means that for the current state, the eventual outcome is "X wins in 1 move".
|
|
If, no matter where we add X, we end up in a state with eventual outcome "O wins in 3 moves",
|
|
this means that for the current state, the eventual outcome is "O wins in 4 moves".
|
|
And so on.
|
|
|
|
The possible outcomes are ordered by their desirability
|
|
(wins for the current player are the most preferable, then draws, then loses;
|
|
smaller number of moves to win is preferable to larger number of moves to win,
|
|
while for draws and loses the prolonged game is preferred),
|
|
and then the most desirable one is picked and saved.
|
|
|
|
So for every board state that can be achieved in legal game,
|
|
we know what's the most desirable outcome for the current player,
|
|
achievable assuming that both players play perfectly in their own interests.
|
|
And having only this data, it is easy to get the optimal move:
|
|
just pick the one that will get you to the state
|
|
where the most desirable outcome for the opponent is the same,
|
|
just with one less move until the end.
|
|
|
|
This means that we can compute desirable outcomes for all possible achievable board states once,
|
|
and then use this information to generate moves for the computer player.
|
|
|
|
The problem is that computing these states is somewhat computationally expensive.
|
|
Easy to do for 3x3 board, but the number of possible achievable board states explodes exponentially with board size;
|
|
for example for 4x4 board there are a bit under ten million achievable board states,
|
|
according to https://math.stackexchange.com/a/613505.
|
|
|
|
If I was doing this in low-level language such as Zig, and for one fixed board size,
|
|
I would probably manage to optimize the algorithm to run in a reasonable time and consume reasonable amounts of memory:
|
|
e.g. every 4x4 board state could easily be compressed to a 32-bit value,
|
|
which would also allow one to do some bit flipping to relatively quickly
|
|
determine which squares are still free and generate next boards;
|
|
determining winning boards would be as easy as applying 10 different bit masks...
|
|
all in all, it would probably take O(n*log(n)) ~= on the order of hundreds of millions of instructions
|
|
(i.e. fractions of a second), and under a hundred megabytes of RAM.
|
|
|
|
But writing such a heavily optimized code in JS, while also supporting custom board sizes,
|
|
is very much outside of the scope I wanted to take, and I spent too much time on this project already.
|
|
|
|
The current inefficient JS implementation takes on the order of minute to compute all solutions for 4x4 board,
|
|
while consuming gigabytes of RAM, so I decided to limit it to 12 cells (i.e. 4x3/3x4 boards) max.
|
|
|
|
The solutions are computed where they are needed (i.e. on backend for no-JS MPA, on frontend for SPA),
|
|
and then cached.
|
|
|
|
Originally my idea was to use WebWorkers to compute them on frontend, to avoid blocking the UI,
|
|
but since boards larger than 3x4 are not supported anyway,
|
|
and all solutions for 3x4 boards are computed in fractions of a second,
|
|
I decided that this complexity is not needed.
|
|
|
|
If computer player is selected for boards larger than 12 squares, no computer moves will be made,
|
|
and humans will have to make moves both for O and for X.
|
|
|
|
Implementation: [`src/shared/gameplay/solver.ts`](src/shared/gameplay/solver.ts).
|
|
|
|
### Tests
|
|
|
|
In addition to strict TS configuration (`npm run typecheck`)
|
|
and strict typechecked eslint configuration (`npm run lint`),
|
|
most of the shared code files are covered by extensive tests,
|
|
defined in `.spec.ts` files next to the code files (and also `src/shared/integration-tests`),
|
|
written with node-tap and runnable with `npm test` (which also checks code coverage for tested files,
|
|
and by default returns an error if the coverage is not 100%).
|
|
|
|
### Dependencies
|
|
|
|
Listed in `package.json`, installed with `npm ci`, not vendored.
|
|
|
|
#### Dev dependencies (build/compile-time)
|
|
|
|
* `tap`, a test framework (https://node-tap.org/).
|
|
* `typescript`, because it's so much easier to write even small projects in TS than in JS.
|
|
* `@tsconfig/strictest`, to avoid having to enable all the strict TS compiler options manually.
|
|
* `eslint`, for linting.
|
|
* `prettier`, to ensure common code style.
|
|
* `typescript-eslint`, for type-aware and type-checked linting.
|
|
* `copyfiles`, to copy static assets to the `dist` directory so that they can be next to compiled JS for frontend.
|
|
* `rimraf`, to cleanup `dist` directory before building.
|
|
* `typed-query-selector`, because https://github.com/microsoft/TypeScript/issues/29037 is not addressed yet.
|
|
|
|
#### Backend dependencies (runtime)
|
|
|
|
* `express`, because I needed a minimal server framework to handle requests and serve static files.
|
|
* `body-parser`, to handle POST requests in express.
|
|
* `preact`, `preact-render-to-string`, to render intermediate JSX code to HTML
|
|
(because I'm using JSX as a template language on backend) (https://preactjs.com/).
|
|
|
|
#### Frontend dependencies (runtime)
|
|
|
|
None.
|
|
|
|
## Supported browser engines
|
|
|
|
There are three main fully functional modes:
|
|
|
|
1. Main simple offline-based JS-only mode, using modern web standards;
|
|
2. More elaborate (almost) offline-based (almost) JS-only mode (game works offline,
|
|
but demo counters on the top of the page (separate from the game) only work online
|
|
because I don't have the motivation to redesign `ReactiveButton` and `ReactiveSpan`),
|
|
using modern web standards _without_ custom built-in elements
|
|
(which means old, but not too old Blink-based browsers like Chromium 54-66,
|
|
and all modern WebKit-based browsers like Safari 10.1+);
|
|
3. Online-based (plain old client-server) mode available in most browsers even with JS disabled.
|
|
|
|
The main required features are:
|
|
|
|
* ES modules: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type,
|
|
without them no JS will load, and this demo will fall back to `mode 3`
|
|
(can be worked around by using bundlers, but this will only affect Chromium-based browsers from 54 (2016) to 60 (2017),
|
|
so it's not worth it to change the entire build setup just for better UX in these browsers).
|
|
* Custom elements: https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define,
|
|
without them this demo will fall back to `mode 3`.
|
|
* Custom built-in elements: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is,
|
|
without them this demo will fall back to `mode 2`.
|
|
* Templates: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template,
|
|
without them `mode 1` or `mode 2` will break, but they're in baseline HTML,
|
|
so if they're not supported, ES modules are not supported either, and this demo will fall back to `mode 3` anyway.
|
|
* `:scope` CSS selector, needed in `mode 2` only:
|
|
https://developer.mozilla.org/en-US/docs/Web/CSS/:scope
|
|
* `MutationObserver`, needed in `mode 2` only:
|
|
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
|
* `:not` CSS selector for critical UI features, needed in all modes
|
|
(hiding and showing different messages and buttons depending on the game state):
|
|
https://developer.mozilla.org/en-US/docs/Web/CSS/:not.
|
|
* `hidden` attribute or inline `style="display: none"` support in `mode 3` to not display templates.
|
|
* Also CSS grid for nice presentation: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template
|
|
(presentation is less nice on small screens, should be easy to make layout responsive, but I already spent too much time on this project).
|
|
|
|
Therefore, for desktop browsers:
|
|
|
|
* Gecko (Firefox): checked and works in v132, both with JS enabled (`mode 1`) and with JS disabled (`mode 3`).
|
|
Should work in `mode 1` starting with Firefox 63 (2018); should fall back plain old client-server mode in all versions,
|
|
although the design in Firefox 51 (2017) and older will probably not look very good due to the lack of the recent CSS features.
|
|
* Blink (e.g. Chromium and Chromium-based browsers): superficially checked in Chromium 130, basic functionality seems to work in `mode 1`.
|
|
Should work in `mode 1` starting with Chrome 67 (2018); should fall back to `mode 2` in Chrome plain old client-server mode in all versions,
|
|
although the design in Chrome 56 (2017) and older will probably not look very good due to the lack of the recent CSS features.
|
|
* WebKit (e.g. qutebrowser, Konqueror, Epiphany and notably Safari): superficialy checked in [BadWolf](https://hacktivis.me/projects/badwolf) 1.3 / webkit2gtk-4.1 2.46.3,
|
|
basic functionality seems to work both with JS enabled (in `mode 2`) and with JS disabled (in `mode 3`).
|
|
Should work with JS enabled in `mode 2` in all browsers supporting custom elements (e.g. in Safari 10.1 (2017) and newer);
|
|
should fall back to `mode 3` in older browsers, except that the design in Safari before 10.1 (2017) will not look very good (but will still be functional),
|
|
and will probably become disfunctional in Safari before 3.1 (2008).
|
|
* Servo: checked, does **not** work in nightly as of 2024-11-20 and as of 2024-11-25 (even progressive form doesn't work),
|
|
and its developer tools host seemingly crashes on this page
|
|
(at least on the borrowed Windows machine; Servo is not packaged for Alpine, and its regular Linux builds don't run on Alpine).
|
|
I decided to _not_ disable dynamic features in Servo because:
|
|
1. Servo is not production-ready, its users know that it is not suitable to be used as a daily driver;
|
|
2. This is likely a bug in servo, it will be fixed in the future, so
|
|
3. Gating offline JS mode for non-servo browsers only will mean that in the future, when servo is fixed,
|
|
this demo will only work in client-server mode, just because servo once had a bug. Not ideal.
|
|
(Also not even attempting to use dynamic features in servo will make it more difficult to debug the problem in it.)
|
|
* EdgeHTML (classic MS Edge before they migrated to Chromium): not checked, should fall back to `mode 3` in all versions,
|
|
but the page will not look very pretty in Edge before 16 (2017).
|
|
* Trident (Internet Explorer): not checked, should fall back to `mode 3`,
|
|
UI should be functional (not pretty) in IE8+ (2008) and dysfunctional in IE7 and lower
|
|
(fixable by refactoring the approach to displaying/hiding elements to avoid the usage of `:not` selector,
|
|
but this is outside the scope of the task).
|
|
* links: checked on 2.30, mostly functional:
|
|
* Links doesn't support disabled buttons
|
|
so all board fields are always clickable (even if they're already filled),
|
|
this does not cause any functional issues (clicking on the occupied field,
|
|
or clicking on an empty field when the game is over, does not change anything),
|
|
but still that's a poor UX.
|
|
* elinks: checked on 0.17.1.1, mostly functional:
|
|
* elinks just like links does not support disabled buttons in 0.17.1.1,
|
|
but this is fixed in https://github.com/rkd77/elinks/issues/341.
|
|
* elinks does not support `hidden` attribute and by default does not support `display: none` in 0.17.1.1,
|
|
so it will display the content of all (unused) templates.
|
|
Support for `hidden` attribute is already implemented in https://github.com/rkd77/elinks/issues/341,
|
|
and will automatically work in newer versions; in older versions, the user should add
|
|
`set document.css.ignore_display_none = 0` to their elinks config.
|
|
|
|
So presumably, according to caniuse.com,
|
|
|
|
* `mode 1` should work in 88% desktop browsers and 80% total browsers
|
|
(with Safari being the notable exception contributing 9% on desktop and 18% overall);
|
|
* Fallback to `mode 2` (which is just as functional but with a bit more convoluted JS code)
|
|
should bring the total to at least 96% overall (98% on desktop; for some less popular mobile browsers support status is unknown);
|
|
* The demo should at least fall back to functional `mode 3` (client-server) with the functional UI
|
|
in all more or less mainstream browsers (including presumably Internet Explorer 6 and NetFront 3, unchecked),
|
|
and in some non-mainstream ones, including text browsers such as links and elinks (checked) but notably not lynx.
|
|
|
|
## Known issues and quirks
|
|
|
|
* Playing against the computer opponent is only supported for boards under 12 squares (i.e. 4x3 or 3x4 max).
|
|
* The computer player moves might be counterintuitive at times,
|
|
and it might be difficult for a human player to get to lose the game,
|
|
because the computer player always assumes that the other player plays perfectly.
|
|
* Boards larger than 12 squares are not supported with computer players
|
|
(regardless of computer players selected, all moves on large boards must be made by humans);
|
|
but the user does not get any explicit visible notification about this,
|
|
the only explicit indication that something is wrong is an error message in console.
|
|
|
|
## Remaining tasks
|
|
|
|
* Implement error handling and handling of incorrect / malformed game states (since they come from the client).
|
|
* Improve UI for large boards (disable autoplayers when board is too large,
|
|
hide "enable autoplayer" buttons, provide clear indication to the user instead).
|
|
* Figure out better API for game board,
|
|
`board.get(row, column)` and `board.get(row, column)` seemed to be a good way to encapsulate readonly state,
|
|
but they became very inconvenient as the codebase grew and became more complex,
|
|
considering that board does not even expose its dimensions.
|
|
* Figure out better and more consistent naming and project structure.
|
|
* Implement end-to-end tests with puppeteer / chromedriver (and run them both with JS enabled and with JS disabled).
|
|
* Implement unit tests for remaining shared code.
|
|
* Implement integration tests for backend and frontend code, maybe?
|
|
* Ensure that everything is accessible.
|
|
* For better accessibility, maybe there is also a need for a form where an user can enter row number and column number to make a move,
|
|
instead of clicking on a square (or activating it from keyboard, which works just fine now,
|
|
but idk how it is compatible with screen readers exactly).
|
|
* Should I also add row number and column number hints to squares for screen readers? idk.
|
|
* Rewrite solver so that it doesn't take a minute to compute all games on 4x4 board
|
|
(which only has under 10 millions states that can be encountered during legal play).
|
|
|
|
## Time spent
|
|
|
|
By major chunks of work in git history:
|
|
|
|
* ~0.5 hours to set up the project;
|
|
* ~5 hours to implement game board serialization, tic-tac-toe rules and game solver (with tests);
|
|
* ~1 hour to set up the backend / frontend structure with JSX templating and static resources;
|
|
* ~2 hours to implement progressively enhanced form and query string value trackers
|
|
(both on backend and in web components);
|
|
* ~3 hours to implement basic game UI in client-server mode;
|
|
* ~2 hours to enhance the most basic game UI features on frontend for offline mode;
|
|
* ~2 hours to make UI fully functional,
|
|
to cleanup the code and reduce duplication between frontend and backend,
|
|
and to make the resulting page somewhat presentable
|
|
* ~2 hours to implement another set of tic-tac-toe rules (with tests),
|
|
support for changing board size (both on backend and in web components).
|
|
* ~2 hours to write this README completely.
|
|
|
|
...which makes it ~20 hours total. I definitely did not expect to spend that much;
|
|
but also originally I didn't think that the scope will expand this much.
|
|
|