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
|
|
|
}}
|
|
|
|
/>}
|
2023-10-14 04:16:30 +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>
|
|
|
|
}
|
|
|
|
|
2024-05-16 00:00:49 +00:00
|
|
|
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>
|
|
|
|
</>
|
|
|
|
}
|