WordGrid/ui/src/UI.tsx

307 lines
10 KiB
TypeScript
Raw Normal View History

2023-08-24 03:46:43 +00:00
import * as React from "react";
2024-12-02 23:39:27 +00:00
import {ChangeEvent, JSX, useState} from "react";
2023-08-24 03:46:43 +00:00
import {
2023-09-23 01:24:05 +00:00
cellTypeToDetails,
CoordinateData,
GridArrowData,
GridArrowDispatch,
GridArrowDispatchActionType,
HighlightableLetterData,
2023-08-24 03:46:43 +00:00
LocationType,
PlayableLetterData,
2023-09-23 01:24:05 +00:00
TileDispatch,
TileDispatchActionType,
2023-08-24 03:46:43 +00:00
} from "./utils";
2024-12-02 23:39:27 +00:00
import {API, APIPlayer, CellType, Difficulty} from "./api";
import {GameWasm} from "./wasm";
import {Game} from "./Game";
2023-08-24 03:46:43 +00:00
2023-09-23 01:24:05 +00:00
export function TileSlot(props: {
tile?: React.JSX.Element | undefined,
location: CoordinateData,
tileDispatch: TileDispatch,
arrowDispatch?: GridArrowDispatch,
}): React.JSX.Element {
2023-08-24 03:46:43 +00:00
let isDraggable = props.tile !== undefined;
function onDragStart(e: React.DragEvent<HTMLDivElement>) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("wordGrid/coords", JSON.stringify(props.location));
}
function onDrop(e: React.DragEvent<HTMLDivElement>) {
const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords"));
const thisLocation = props.location;
2023-09-23 01:24:05 +00:00
props.tileDispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
2023-08-24 03:46:43 +00:00
}
let className = "tileSpot";
if (props.location.location === LocationType.GRID) {
className += " ephemeral";
}
2023-09-23 01:24:05 +00:00
let onClick: () => void;
if(props.arrowDispatch != null && props.location.location == LocationType.GRID) {
onClick = () => {
props.arrowDispatch({action: GridArrowDispatchActionType.CYCLE, position: props.location.index});
}
} else if(props.location.location == LocationType.TRAY && props.tile != null && props.tile.props.data.text != ' ' && props.tile.props.data.text != '') {
onClick = () => {
props.tileDispatch({
action: TileDispatchActionType.MOVE_TO_ARROW, end: undefined, start: props.location,
});
}
}
2023-08-24 03:46:43 +00:00
return <div className={className}
draggable={isDraggable}
onDragStart={onDragStart}
onDrop={onDrop}
2023-09-23 01:24:05 +00:00
onClick={onClick}
2023-08-24 03:46:43 +00:00
onDragOver={(e) => {e.preventDefault()}}
>
{props.tile}
</div>
}
2023-09-15 02:38:34 +00:00
export function Letter(props: { data: HighlightableLetterData, letterUpdater?: (value: string) => void }): React.JSX.Element {
2023-08-24 03:46:43 +00:00
function modifyThisLetter(value:string){
props.letterUpdater(value);
}
function onBlankInput(e: ChangeEvent<HTMLInputElement>){
let value = e.target.value.toUpperCase();
if(value.length > 1){
value = value[value.length - 1];
} else if(value.length == 0){
modifyThisLetter(value);
return;
}
// Now check that it's a letter
let is_letter = value.match("[A-Z]") != null;
if(is_letter){
modifyThisLetter(value);
} else {
// Cancel and do nothing
e.preventDefault();
}
}
if(props.data.is_blank && props.data.ephemeral) {
return <div className="letter">
<input className="blank-input" type="text" onChange={onBlankInput} value={props.data.text} />
<div className="letter-points">{props.data.points}</div>
</div>
} else {
let className = "text";
if (props.data.is_blank) { // not ephemeral
className += " prev-blank";
}
2023-09-15 02:38:34 +00:00
let letterClassName = "letter";
if (props.data.highlight) {
letterClassName += " highlight";
}
return <div className={letterClassName}>
2023-08-24 03:46:43 +00:00
<div className={className}>{props.data.text}</div>
<div className="letter-points">{props.data.points}</div>
</div>
}
}
2023-09-23 01:24:05 +00:00
export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, trayDispatch: TileDispatch }): React.JSX.Element {
2023-08-24 03:46:43 +00:00
let elements: JSX.Element[] = [];
for (let i=0; i<props.trayLength; i++) {
elements.push(
<TileSlot
key={"empty" + i}
location={{location: LocationType.TRAY, index: i}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.trayDispatch} />
2023-08-24 03:46:43 +00:00
);
}
props.letters
.forEach((letter, i) => {
2023-10-14 04:55:34 +00:00
if (letter != null && letter.location === LocationType.TRAY) {
2023-08-24 03:46:43 +00:00
elements[letter.index] =
<TileSlot
tile={<Letter
2023-09-23 01:24:05 +00:00
data={{...letter, highlight: false}}
2023-08-24 03:46:43 +00:00
letterUpdater={(value) => {
2023-09-23 01:24:05 +00:00
props.trayDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value})
2023-08-24 03:46:43 +00:00
}}
/>}
key={"letter_tray" + letter.index}
2023-08-24 03:46:43 +00:00
location={{location: LocationType.TRAY, index: letter.index}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.trayDispatch} />
2023-08-24 03:46:43 +00:00
}
});
return (
<div className="tray">
{elements}
</div>
)
}
2023-09-23 01:24:05 +00:00
export function Arrow(props: {
data: GridArrowData,
dispatch: GridArrowDispatch,
}) {
return <div
className={`arrow ${props.data.direction}`}
>
</div>;
}
2023-08-24 03:46:43 +00:00
export function Grid(props: {
cellTypes: CellType[],
playerLetters: Array<PlayableLetterData>,
2023-09-15 02:38:34 +00:00
boardLetters: HighlightableLetterData[],
2023-09-23 01:24:05 +00:00
tileDispatch: TileDispatch,
arrow: GridArrowData,
arrowDispatch: GridArrowDispatch,
}) {
2023-08-24 03:46:43 +00:00
const elements = props.cellTypes.map((ct, i) => {
const {className, text} = cellTypeToDetails(ct);
let tileElement: JSX.Element;
if (props.boardLetters[i] !== undefined) {
tileElement = <Letter data={props.boardLetters[i]} />;
} else {
tileElement = <>
<span>{text}</span>
<TileSlot
2023-09-23 01:24:05 +00:00
location={{location: LocationType.GRID, index: i}}
tileDispatch={props.tileDispatch}
arrowDispatch={props.arrowDispatch}
/>
</>;
2023-08-24 03:46:43 +00:00
}
2023-09-23 01:24:05 +00:00
let arrowElement: JSX.Element;
if(props.arrow != null && props.arrow.position == i) {
arrowElement = <Arrow data={props.arrow} dispatch={props.arrowDispatch} />;
}
2023-08-24 03:46:43 +00:00
2023-09-23 01:24:05 +00:00
return <div key={i} className={"grid-spot " + className}>
2023-08-24 03:46:43 +00:00
{tileElement}
2023-09-23 01:24:05 +00:00
{arrowElement}
2023-08-24 03:46:43 +00:00
</div>;
});
props.playerLetters
.forEach((letter, i) => {
2023-10-14 04:55:34 +00:00
if (letter != null && letter.location === LocationType.GRID) {
2023-08-24 03:46:43 +00:00
const ct = props.cellTypes[letter.index];
2023-08-24 04:58:34 +00:00
const {className} = cellTypeToDetails(ct);
2023-08-24 03:46:43 +00:00
elements[letter.index] =
<div key={"letter" + letter.index} className={"grid-spot " + className}>
<TileSlot
tile={<Letter
2023-09-23 01:24:05 +00:00
data={{...letter, highlight: false}}
2023-08-24 03:46:43 +00:00
letterUpdater={(value) => {
2023-09-23 01:24:05 +00:00
props.tileDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value});
2023-08-24 03:46:43 +00:00
}}
/>}
location={{location: LocationType.GRID, index: letter.index}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.tileDispatch} />
2023-08-24 03:46:43 +00:00
</div>;
}
});
return <div className="board-grid">
{elements}
</div>
}
export function Scores(props: {playerScores: Array<APIPlayer>}){
2023-08-24 04:58:34 +00:00
let elements = props.playerScores.map((ps) => {
2023-08-24 03:46:43 +00:00
return <div key={ps.name}>
<h3>{ps.name}</h3>
<span>{ps.score}</span>
</div>;
});
return <div className="scoring">
{elements}
</div>
}
2024-12-02 23:39:27 +00:00
export function AISelection(props: {
aiRandomness: number,
setAIRandomness: (x: number) => void,
proportionDictionary: number,
setProportionDictionary: (x: number) => void,
}) {
// Can change log scale to control shape of curve using following equation:
// aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1
const logBase: number = 10000;
const processedAIRandomness = Math.log(1 + (logBase - 1)*props.aiRandomness/100) / Math.log(logBase);
//const processedProportionDictionary = 1.0 - props.proportionDictionary / 100;
return <>
<div className="grid">
<label htmlFor="proportion-dictionary">AI's proportion of dictionary:</label>
<input type="number"
name="proportion-dictionary"
value={props.proportionDictionary}
onInput={(e) => {
props.setProportionDictionary(e.currentTarget.valueAsNumber);
}}
min={1}
max={100}/>
<label htmlFor="randomness">Level of randomness in AI:</label>
<input type="number"
name="randomness"
value={props.aiRandomness}
onInput={(e) => {
props.setAIRandomness(e.currentTarget.valueAsNumber);
}}
min={0}
max={100}/>
</div>
<details>
<ul>
<li>
"AI's proportion of dictionary" controls what percent of the total AI dictionary
the AI can form words with. At 100%, it has access to its entire dictionary -
although this dictionary is still less than what the player has access to.</li>
<li>
<div>
"Level of randomness in AI" controls the degree to which the AI picks the optimal move
for each of its turns. At 0, it always picks the highest scoring move it can do using the
dictionary it has access to. At 1, it picks from its available moves at random.
</div>
<div>
Note that "Level of randomness in AI" is now mapped on a log scale.
Your current setting is equivalent to {(100*processedAIRandomness).toFixed(1)}% on the previous scale.
</div>
</li>
</ul>
</details>
</>
}