├── wordtris-server ├── README.md ├── settings.gradle.kts ├── src │ ├── main │ │ ├── kotlin │ │ │ └── khivy │ │ │ │ └── wordtrisserver │ │ │ │ ├── protobuf_gen │ │ │ │ └── main │ │ │ │ │ ├── kotlin │ │ │ │ │ ├── PlayerSubmissionDataOuterClassKt.kt │ │ │ │ │ └── PlayerSubmissionDataKt.kt │ │ │ │ │ └── java │ │ │ │ │ └── PlayerSubmissionDataOuterClass.java │ │ │ │ ├── setup.kt │ │ │ │ ├── repositories │ │ │ │ ├── IpRepository.kt │ │ │ │ ├── NameRepository.kt │ │ │ │ └── ScoreRepository.kt │ │ │ │ ├── WordtrisServerApplication.kt │ │ │ │ ├── services │ │ │ │ ├── CacheService.kt │ │ │ │ ├── ProfanityFilterService.kt │ │ │ │ └── DataService.kt │ │ │ │ ├── datamodel │ │ │ │ └── DataModel.kt │ │ │ │ ├── config │ │ │ │ └── RedisConfig.kt │ │ │ │ └── web │ │ │ │ └── ScoreController.kt │ │ └── resources │ │ │ ├── application.properties │ │ │ └── banned_words.txt │ └── test │ │ └── kotlin │ │ └── khivy │ │ └── wordtrisserver │ │ └── WordtrisServerApplicationTests.kt ├── lib │ ├── kotlin-test.jar │ ├── kotlin-reflect.jar │ ├── kotlin-stdlib.jar │ ├── kotlin-test-sources.jar │ ├── kotlin-reflect-sources.jar │ └── kotlin-stdlib-sources.jar ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── Dockerfile ├── .gitignore ├── docker-compose.yml ├── build.gradle.kts ├── gradlew.bat └── gradlew ├── wordtris ├── bunfig.toml ├── .gitignore ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── src │ ├── images.d.ts │ ├── fonts │ │ └── PressStart2P-Regular.ttf │ ├── util │ │ ├── UserCell.ts │ │ ├── BoardCell.ts │ │ ├── useInterval.ts │ │ ├── boardUtil.ts │ │ ├── webUtil.ts │ │ ├── weightedDistribution.ts │ │ └── playerUtil.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── App.css │ ├── components │ │ ├── GameSidePanel.tsx │ │ ├── CountdownOverlay.tsx │ │ ├── PersonalHighScore.tsx │ │ ├── PlayerBlock.tsx │ │ ├── BoardCells.tsx │ │ ├── Leaderboard.tsx │ │ ├── WordList.tsx │ │ ├── FallingBlock.tsx │ │ ├── GameOverOverlay.tsx │ │ ├── Header.tsx │ │ └── Prompt.tsx │ ├── index.tsx │ ├── setup.ts │ ├── logo.svg │ ├── protobuf_gen │ │ └── PlayerSubmissionData.ts │ └── GameLoop.tsx ├── .vscode │ └── settings.json ├── test │ ├── cellTest.ts │ └── webTest.ts ├── scripts │ └── gen_protobuf_classes.sh ├── justfile ├── README.md ├── deno.jsonc ├── LICENSE ├── package.json └── tsconfig.json ├── img ├── wordtris_demo.jpg └── wordtris_stack.jpg ├── protobuf └── PlayerSubmissionData.proto ├── .github └── workflows │ ├── lint.yml │ ├── format.yml │ ├── bundle.yml │ ├── tsc.yml │ ├── unitTest.yml │ └── pages.yml └── README.md /wordtris-server/README.md: -------------------------------------------------------------------------------- 1 | # wordtris-server 2 | -------------------------------------------------------------------------------- /wordtris/bunfig.toml: -------------------------------------------------------------------------------- 1 | [bundle] 2 | entryPoints = ["./src/index.tsx"] 3 | -------------------------------------------------------------------------------- /wordtris-server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "wordtris-server" 2 | -------------------------------------------------------------------------------- /img/wordtris_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/img/wordtris_demo.jpg -------------------------------------------------------------------------------- /img/wordtris_stack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/img/wordtris_stack.jpg -------------------------------------------------------------------------------- /wordtris/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | node_modules.bun 3 | bun.lockb 4 | *.tsbuildinfo 5 | bundle.js 6 | -------------------------------------------------------------------------------- /wordtris/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris/public/favicon.ico -------------------------------------------------------------------------------- /wordtris/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris/public/logo192.png -------------------------------------------------------------------------------- /wordtris/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris/public/logo512.png -------------------------------------------------------------------------------- /wordtris/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/protobuf_gen/main/kotlin/PlayerSubmissionDataOuterClassKt.kt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-test.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-test.jar -------------------------------------------------------------------------------- /wordtris/src/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: unknown; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-reflect.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-reflect.jar -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-stdlib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-stdlib.jar -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-test-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-test-sources.jar -------------------------------------------------------------------------------- /wordtris/src/fonts/PressStart2P-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris/src/fonts/PressStart2P-Regular.ttf -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-reflect-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-reflect-sources.jar -------------------------------------------------------------------------------- /wordtris-server/lib/kotlin-stdlib-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/lib/kotlin-stdlib-sources.jar -------------------------------------------------------------------------------- /wordtris-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khivy/wordtris/HEAD/wordtris-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /wordtris/src/util/UserCell.ts: -------------------------------------------------------------------------------- 1 | export interface UserCell { 2 | r: number; 3 | c: number; 4 | char: string; 5 | uid: string; 6 | } 7 | -------------------------------------------------------------------------------- /wordtris/src/util/BoardCell.ts: -------------------------------------------------------------------------------- 1 | export interface BoardCell { 2 | r: number; 3 | c: number; 4 | char: string; 5 | hasMatched: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /protobuf/PlayerSubmissionData.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message PlayerSubmissionData { 4 | int32 score = 1; 5 | string name = 2; 6 | repeated string words = 3; 7 | bytes checksum = 4; 8 | } 9 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/setup.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.setup 2 | 3 | const val NAME_LENGTH_MAX = 10 4 | const val MAX_SCORES_PER_IP = 5 5 | const val MAX_LEADERS_ON_BOARD = 50 6 | -------------------------------------------------------------------------------- /wordtris/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnSaveMode": "modificationsIfAvailable", 4 | "editor.defaultFormatter": "denoland.vscode-deno", 5 | "deno.enable": false 6 | } 7 | -------------------------------------------------------------------------------- /wordtris-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /wordtris-server/src/test/kotlin/khivy/wordtrisserver/WordtrisServerApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class WordtrisServerApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/repositories/IpRepository.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.repositories.ip 2 | 3 | import org.springframework.stereotype.Repository 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import khivy.wordtrisserver.datamodel.* 6 | 7 | 8 | @Repository 9 | interface IpRepository : JpaRepository { 10 | } 11 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/repositories/NameRepository.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.repositories.name 2 | 3 | import org.springframework.stereotype.Repository 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import khivy.wordtrisserver.datamodel.* 6 | 7 | 8 | @Repository 9 | interface NameRepository : JpaRepository { 10 | } -------------------------------------------------------------------------------- /wordtris/test/cellTest.ts: -------------------------------------------------------------------------------- 1 | import { createBoard, getGroundHeight } from "../src/util/boardUtil"; 2 | 3 | function testBaseHeight() { 4 | const len = 5; 5 | const matrix = createBoard(len, len); 6 | for (let i = 0; i < len; ++i) { 7 | console.assert( 8 | getGroundHeight(i, len - 1, matrix) == len - 1, 9 | ); 10 | } 11 | } 12 | 13 | testBaseHeight(); 14 | -------------------------------------------------------------------------------- /wordtris/scripts/gen_protobuf_classes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generates Typescript Protobuf classes based on the `ts_proto` plugin. 3 | 4 | mkdir temp_protobuf_gen 5 | cd temp_protobuf_gen 6 | ln -s ../../../protobuf/PlayerSubmissionData.proto . 7 | protoc --plugin=../../node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=../../src/protobuf_gen/ PlayerSubmissionData.proto 8 | cd .. 9 | rm -rf temp_protobuf_gen 10 | -------------------------------------------------------------------------------- /wordtris/justfile: -------------------------------------------------------------------------------- 1 | set shell := ["bash", "-uc"] 2 | 3 | default: 4 | just --list 5 | 6 | fmt: 7 | deno fmt 8 | 9 | check: 10 | deno fmt --check 11 | deno lint 12 | bun tsc 13 | 14 | alias c := check 15 | 16 | build: 17 | bun bun 18 | 19 | alias b := build 20 | 21 | run: 22 | bun dev 23 | 24 | alias r := run 25 | 26 | test: 27 | bun test/cellTest.ts 28 | 29 | alias t := test 30 | -------------------------------------------------------------------------------- /wordtris/README.md: -------------------------------------------------------------------------------- 1 | # Tetris + words! 2 | 3 | (✿◠‿◠) [Play here!](https://khivy.github.io/wordtris/) ✧♡(◕‿◕✿) (It works on mobile!!) 4 | Update: The leaderboard server is now shut off because I can no longer pay for it 😓 5 | 6 | # Roadmap 7 | 8 | - Fix non-determinism bugs 9 | - Use a better dictionary / explore more mechanics to make it more fun (turns 10 | out the Scrabble dictionary is weird!) 11 | - Add leaderboard 12 | -------------------------------------------------------------------------------- /wordtris/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: vx.x.x 21 | 22 | - name: Lint 23 | run: deno lint wordtris/src wordtris/test 24 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: vx.x.x 21 | 22 | - name: Format 23 | run: deno fmt --check wordtris/src wordtris/test 24 | 25 | -------------------------------------------------------------------------------- /wordtris-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-alpine 2 | 3 | COPY gradlew . 4 | COPY gradle gradle 5 | COPY build.gradle.kts . 6 | COPY settings.gradle.kts . 7 | COPY src src 8 | RUN chmod +x ./gradlew 9 | RUN ./gradlew bootJar --info -x extractProto -x generateProto -x extractTestProto 10 | VOLUME /tmp 11 | 12 | # If you'd rather build a jar separately, comment out the above block and uncomment the line below. 13 | #COPY build/libs/*.jar app.jar 14 | 15 | ENTRYPOINT ["java","-jar","build/libs/app.jar"] 16 | -------------------------------------------------------------------------------- /wordtris/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lint": { 3 | "files": { 4 | "include": [ 5 | "src", 6 | "test" 7 | ] 8 | } 9 | }, 10 | "fmt": { 11 | "files": { 12 | "include": [ 13 | "." 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | }, 19 | "options": { 20 | "lineWidth": 80, 21 | "indentWidth": 4 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/bundle.yml: -------------------------------------------------------------------------------- 1 | name: Bundle 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Bun 18 | uses: antongolub/action-setup-bun@v1 19 | with: 20 | cache: true 21 | cache-bin: true 22 | 23 | - name: Install 24 | run: bun install --cwd wordtris 25 | 26 | - name: Bundle 27 | run: bun bun --cwd wordtris 28 | -------------------------------------------------------------------------------- /wordtris/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | export const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (!(onPerfEntry && onPerfEntry instanceof Function)) { 5 | return; 6 | } 7 | (async () => { 8 | const { getCLS, getFID, getFCP, getLCP, getTTFB } = await import( 9 | "web-vitals" 10 | ); 11 | getCLS(onPerfEntry); 12 | getFID(onPerfEntry); 13 | getFCP(onPerfEntry); 14 | getLCP(onPerfEntry); 15 | getTTFB(onPerfEntry); 16 | })(); 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/tsc.yml: -------------------------------------------------------------------------------- 1 | name: Type Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Bun 18 | uses: antongolub/action-setup-bun@v1 19 | with: 20 | cache: true 21 | cache-bin: true 22 | 23 | - name: Install 24 | run: bun install --cwd wordtris/ 25 | 26 | - name: Type Check 27 | run: bun --cwd wordtris/ tsc 28 | 29 | -------------------------------------------------------------------------------- /wordtris/src/util/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export function useInterval(callback: () => void, delay: number) { 4 | const savedCallback = useRef(); 5 | 6 | // Remember the latest callback. 7 | useEffect(() => { 8 | savedCallback.current = callback; 9 | }, [callback]); 10 | 11 | // Set up the interval. 12 | useEffect(() => { 13 | function tick() { 14 | savedCallback.current?.(); 15 | } 16 | const id = setInterval(tick, delay); 17 | return () => clearInterval(id); 18 | }, [delay]); 19 | } 20 | -------------------------------------------------------------------------------- /wordtris-server/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /wordtris/src/App.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | display: flex; 3 | border-radius: 0.4vmin; 4 | justify-content: center; 5 | justify-items: center; 6 | align-content: center; 7 | align-items: center; 8 | text-align: center; 9 | line-height: normal; 10 | } 11 | 12 | .cell.with-margin { 13 | margin: 0.4vmin; 14 | } 15 | 16 | .with-text-style { 17 | font-family: "Arial", "Courier New", "Lucida Console", monospace; 18 | font-weight: bold; 19 | text-transform: uppercase; 20 | } 21 | 22 | @font-face { 23 | font-family: 'Press Start 2P'; 24 | src: local('Press Start 2P'), url(./fonts/PressStart2P-Regular.ttf) format('truetype'); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/unitTest.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Bun 18 | uses: antongolub/action-setup-bun@v1 19 | with: 20 | cache: true 21 | cache-bin: true 22 | 23 | - name: Install 24 | run: bun install --cwd wordtris/ 25 | 26 | - name: Run cellTest.ts 27 | run: bun wordtris/test/cellTest.ts 28 | 29 | - name: Run webTest.ts 30 | run: bun wordtris/test/webTest.ts 31 | -------------------------------------------------------------------------------- /wordtris/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Wordtris", 3 | "name": "Wordtris", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/WordtrisServerApplication.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter 7 | 8 | @SpringBootApplication 9 | class WordtrisServerApplication() { 10 | 11 | @Bean 12 | fun protobufHttpMessageConverter(): ProtobufHttpMessageConverter { 13 | return ProtobufHttpMessageConverter(); 14 | } 15 | } 16 | 17 | fun main(args: Array) { 18 | runApplication(*args) 19 | } 20 | -------------------------------------------------------------------------------- /wordtris/src/components/GameSidePanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { MENU_TEXT_COLOR, UNIVERSAL_BORDER_RADIUS } from "../setup"; 3 | import { WordList } from "./WordList"; 4 | 5 | export const GameSidePanel = React.memo( 6 | ({ displayedWords }: { displayedWords: string[] }) => { 7 | const outerStyle = { 8 | display: "flex", 9 | flexDirection: "column", 10 | color: MENU_TEXT_COLOR, 11 | paddingLeft: UNIVERSAL_BORDER_RADIUS, 12 | paddingRight: UNIVERSAL_BORDER_RADIUS, 13 | marginBottom: UNIVERSAL_BORDER_RADIUS, 14 | } as const; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /wordtris-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:5432/ 2 | spring.datasource.username=${POSTGRES_USER:postgres} 3 | spring.datasource.password=${POSTGRES_PASSWORD:password} 4 | spring.datasource.driverClassName = org.postgresql.Driver 5 | spring.jpa.generate-ddl=true 6 | spring.jpa.show-sql = true 7 | spring.redis.host=${REDIS_HOST:localhost} 8 | spring.redis.port=${REDIS_PORT:6379} 9 | # TODO Re-add these. 10 | #server.ssl.key-store-type=PKCS12 11 | #server.ssl.key-store=classpath:${SSL_KEY_FILENAME:key.p12} 12 | #server.ssl.key-store-password=${SSL_KEY_PASSWORD:password} 13 | #server.ssl.key-alias=${SSL_KEY_ALIAS:key} 14 | #trust.store=classpath:keystore/${SSL_KEY_FILENAME:key.p12} 15 | #trust.store.password=${SSL_KEY_PASSWORD:password} 16 | #server.port=${SERVER_PORT:8443} -------------------------------------------------------------------------------- /wordtris/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/stable"; 2 | import * as React from "react"; 3 | import { Suspense } from "react"; 4 | import * as ReactDOM from "react-dom/client"; 5 | import "./index.css"; 6 | import { GameLoop } from "./GameLoop"; 7 | import { reportWebVitals } from "./reportWebVitals"; 8 | import { StrictMode } from "react"; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById("root")!); 11 | root.render( 12 | 13 | Loading...}> 14 | 15 | 16 | , 17 | ); 18 | 19 | // If you want to start measuring performance in your app, pass a function 20 | // to log results (for example: reportWebVitals(console.log)) 21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 22 | reportWebVitals(); 23 | -------------------------------------------------------------------------------- /wordtris/src/util/boardUtil.ts: -------------------------------------------------------------------------------- 1 | import { BoardCell } from "./BoardCell"; 2 | import { EMPTY } from "../setup"; 3 | 4 | export function createBoard(rows: number, cols: number): BoardCell[][] { 5 | // Init cells. 6 | const cells = []; 7 | for (let r = 0; r < rows; ++r) { 8 | const row = []; 9 | for (let c = 0; c < cols; ++c) { 10 | row.push({ c: c, r: r, char: EMPTY, hasMatched: false }); 11 | } 12 | cells.push(row); 13 | } 14 | return cells; 15 | } 16 | 17 | export function getGroundHeight( 18 | col: number, 19 | startRow: number, 20 | board: BoardCell[][], 21 | ): number { 22 | // Search for first non-EMPTY board cell from the top. 23 | for (let row = startRow; row < board.length - 1; ++row) { 24 | if (board[row + 1][col].char !== EMPTY) { 25 | return row; 26 | } 27 | } 28 | return board.length - 1; 29 | } 30 | -------------------------------------------------------------------------------- /wordtris/src/components/CountdownOverlay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BOARD_CELL_COLOR, LARGE_TEXT_SIZE, MENU_TEXT_COLOR } from "../setup"; 3 | 4 | export const CountdownOverlay = React.memo( 5 | ( 6 | { isVisible, countdownSec }: { 7 | isVisible: boolean; 8 | countdownSec: number; 9 | }, 10 | ) => { 11 | const divStyle = { 12 | visibility: isVisible ? "visible" as const : "hidden" as const, 13 | position: "absolute", 14 | top: "35%", 15 | left: "50%", 16 | transform: "translate(-50%, -50%)", 17 | zIndex: 2, 18 | color: MENU_TEXT_COLOR, 19 | fontSize: "13vmin", 20 | WebkitTextStroke: "0.2vmin", 21 | WebkitTextStrokeColor: BOARD_CELL_COLOR, 22 | } as const; 23 | return ( 24 |
25 | {countdownSec} 26 |
27 | ); 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tetris + words! 2 | 3 | (✿◠‿◠) [Play here!](https://khivy.github.io/wordtris/) ✧♡(◕‿◕✿) (It works on mobile!!) 4 | Update: The leaderboard server is now shut off because I can no longer pay for it 😓 5 | 6 | ![](img/wordtris_demo.jpg) 7 | 8 | # Deploying the server 9 | First set any environment vars described in `wordtris-server/docker-compose.yml` 10 | 11 | Then make sure your Redis and Postgres server(s) are configured. 12 | 13 | Lastly run the following: 14 | ```shell 15 | docker-compose up -d 16 | ``` 17 | Note: it may take a while due to the Docker image building a new .jar. If you intend to run the above command 18 | multiple times I recommend following the comments in the `wordtris-server/Dockerfile`. 19 | 20 | # Systems Diagram 21 | ![](img/wordtris_stack.jpg) 22 | 23 | # Development 24 | For contributing, I recommend using the following to hot reload the server: 25 | ```sh 26 | ./gradlew build --continuous --info 27 | ``` 28 | And in separate terminal run: 29 | ```sh 30 | ./gradlew bootRun 31 | ``` 32 | 33 | # Roadmap 34 | 35 | - Fix non-determinism bugs 36 | - Look into using more suitable dictionary 37 | -------------------------------------------------------------------------------- /wordtris/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Emma Raine and Khyber Sen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /wordtris/src/components/PersonalHighScore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect } from "react"; 3 | import { NORMAL_TEXT_SIZE } from "../setup"; 4 | import { getPlayerScores } from "../util/webUtil"; 5 | 6 | export const PersonalHighScore = React.memo( 7 | ({ localHighScore }: { localHighScore: number }) => { 8 | const [remoteHighScore, setRemoteHighScore] = React.useState(0); 9 | 10 | useEffect(() => { 11 | getPlayerScores() 12 | .then((response) => response.json()) 13 | .then((data: Array<{ score: number }>) => { 14 | if (data.length <= 0) { 15 | return; 16 | } 17 | setRemoteHighScore(data.sort().at(-1)!.score); 18 | }); 19 | }, []); 20 | 21 | const textStyle = { 22 | fontSize: NORMAL_TEXT_SIZE, 23 | textAlign: "center", 24 | } as const; 25 | 26 | return ( 27 |
28 | High score: {localHighScore < remoteHighScore 29 | ? remoteHighScore 30 | : localHighScore} 31 |
32 | ); 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/services/CacheService.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.services 2 | 3 | import khivy.wordtrisserver.datamodel.Score 4 | import khivy.wordtrisserver.services.score.DataService 5 | import khivy.wordtrisserver.setup.MAX_LEADERS_ON_BOARD 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.cache.annotation.CacheEvict 8 | import org.springframework.cache.annotation.Cacheable 9 | import org.springframework.cache.annotation.EnableCaching 10 | import org.springframework.stereotype.Service 11 | 12 | 13 | @Service 14 | @EnableCaching 15 | class CacheService { 16 | 17 | @Autowired 18 | lateinit var dataService: DataService 19 | 20 | @Cacheable("leaders") 21 | fun getLeaders(): List { 22 | return dataService.scoreRepository.findLeadersNative(MAX_LEADERS_ON_BOARD) 23 | } 24 | 25 | @CacheEvict("leaders") 26 | fun evictLeaders() { 27 | } 28 | 29 | fun getLowestLeaderScoreInt(): Int { 30 | val leaders = getLeaders() 31 | return if (leaders.size < MAX_LEADERS_ON_BOARD) { 32 | 0 33 | } else { 34 | getLeaders().reduce { a, b -> if (a.score < b.score) a else b }.score 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/services/ProfanityFilterService.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.services 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.core.io.ClassPathResource 5 | import org.springframework.stereotype.Service 6 | import org.springframework.util.FileCopyUtils 7 | import java.io.IOException 8 | import java.nio.charset.StandardCharsets 9 | import kotlin.system.exitProcess 10 | 11 | 12 | @Service 13 | class ProfanityFilterService { 14 | 15 | lateinit var bannedWords: Set 16 | 17 | @Autowired 18 | fun ProfanityFilterService() { 19 | var data = "" 20 | val cpr = ClassPathResource("banned_words.txt") 21 | try { 22 | val bdata = FileCopyUtils.copyToByteArray(cpr.inputStream) 23 | data = String(bdata, StandardCharsets.UTF_8) 24 | } catch (e: IOException) { 25 | println("Error: Could not read profanity file. Exiting.") 26 | exitProcess(1) 27 | } 28 | this.bannedWords = data.split('\n').toSet() 29 | } 30 | 31 | fun containsProfanity(word: String): Boolean { 32 | for (i in word.indices) { 33 | for (j in i + 1..word.length) { 34 | if (word.substring(i, j) in bannedWords) { 35 | return true 36 | } 37 | } 38 | } 39 | return false 40 | } 41 | } -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/repositories/ScoreRepository.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.repositories.score 2 | 3 | import org.springframework.stereotype.Repository 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.data.jpa.repository.Query 6 | import org.springframework.data.repository.query.Param 7 | import khivy.wordtrisserver.datamodel.* 8 | 9 | 10 | @Repository 11 | interface ScoreRepository : JpaRepository { 12 | @Query( 13 | value = """ 14 | SELECT * 15 | FROM Score as s, Name as n 16 | WHERE s.name_id = n.id 17 | AND n.ip_fk = :ip 18 | """, nativeQuery = true 19 | ) 20 | fun findScoresWithGivenIpNative(@Param("ip") ip: String): List 21 | 22 | @Query( 23 | value = """ 24 | SELECT * 25 | FROM Score as s, Name as n 26 | WHERE s.name_id = n.id 27 | AND n.ip_fk = :ip 28 | AND n.name = :name 29 | """, nativeQuery = true 30 | ) 31 | fun findScoresWithGivenIpAndNameNative(@Param("ip") ip: String, @Param("name") name: String): List 32 | 33 | @Query( 34 | value = """ 35 | SELECT * 36 | FROM score 37 | ORDER BY score DESC 38 | LIMIT :amount 39 | """, nativeQuery = true 40 | ) 41 | fun findLeadersNative(@Param("amount") amount: Int): List 42 | } 43 | 44 | -------------------------------------------------------------------------------- /wordtris/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordtris", 3 | "version": "0.1.2", 4 | "description": "tetris but with words", 5 | "keywords": [ 6 | "game", 7 | "tetris" 8 | ], 9 | "homepage": "https://khivy.github.io/wordtris/", 10 | "bugs": "https://github.com/khivy/wordtris/issues", 11 | "license": "MIT", 12 | "contributors": [ 13 | { 14 | "name": "Ivy Raine" 15 | }, 16 | { 17 | "name": "Khyber Sen", 18 | "email": "kkysen@gmail.com" 19 | } 20 | ], 21 | "type": "module", 22 | "browser": "src/index.tsx", 23 | "repository": "github:khivy/wordtris", 24 | "dependencies": { 25 | "axios": "^0.26.1", 26 | "fast-sha256": "^1.3.0", 27 | "react": "^18.2.0", 28 | "react-app-polyfill": "^3.0.0", 29 | "react-dom": "^18.2.0", 30 | "react-spring": "^9.5.2", 31 | "ts-proto": "^1.126.1", 32 | "web-vitals": "^2.1.4", 33 | "xstate": "^4.32.1" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@types/react": "^18.0.15", 42 | "@types/react-dom": "^18.0.6", 43 | "react-refresh": "0.10.0", 44 | "typescript": "latest" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | "defaults and supports es6-module" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | }, 56 | "scripts": { 57 | "build": "TSC_COMPILE_ON_ERROR=true react-scripts build" 58 | } 59 | } -------------------------------------------------------------------------------- /wordtris/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 10 | "allowUnreachableCode": false, 11 | "allowUnusedLabels": false, 12 | "alwaysStrict": true, 13 | "exactOptionalPropertyTypes": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitOverride": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "noUncheckedIndexedAccess": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "strict": true, 24 | "strictBindCallApply": true, 25 | "strictFunctionTypes": true, 26 | "strictNullChecks": true, 27 | "strictPropertyInitialization": true, 28 | "useUnknownInCatchVariables": true, 29 | // 30 | "allowSyntheticDefaultImports": false, 31 | "downlevelIteration": true, 32 | "esModuleInterop": false, 33 | "forceConsistentCasingInFileNames": true, 34 | "incremental": true, 35 | "isolatedModules": true, 36 | "jsx": "react", 37 | "module": "esnext", 38 | "moduleResolution": "node", 39 | "newLine": "lf", 40 | "noEmit": true, 41 | "resolveJsonModule": true, 42 | "skipLibCheck": true 43 | }, 44 | "include": [ 45 | "src", 46 | "test" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /wordtris-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres_container 6 | image: postgres 7 | environment: 8 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-localhost} 10 | PGDATA: /data/postgres 11 | volumes: 12 | - postgres:/data/postgres 13 | ports: 14 | - "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}" 15 | networks: 16 | - postgres 17 | restart: unless-stopped 18 | 19 | pgadmin: 20 | container_name: pgadmin_container 21 | image: dpage/pgadmin4 22 | environment: 23 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 24 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} 25 | PGADMIN_CONFIG_SERVER_MODE: 'False' 26 | volumes: 27 | - pgadmin:/var/lib/pgadmin 28 | 29 | ports: 30 | - "${PGADMIN_PORT:-5050}:80" 31 | networks: 32 | - postgres 33 | restart: unless-stopped 34 | 35 | wordtris-server: 36 | image: wordtris-server 37 | network_mode: "host" 38 | build: 39 | context: . 40 | dockerfile: Dockerfile 41 | ports: 42 | - "8080:8080" 43 | environment: 44 | POSTGRES_USER: ${POSTGRES_USER:-password} 45 | POSTGRES_HOST: ${POSTGRES_HOST:-localhost} 46 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} 47 | REDIS_HOST: ${REDIS_HOST:-localhost} 48 | REDIS_PORT: ${REDIS_PORT:-6379} 49 | 50 | networks: 51 | postgres: 52 | driver: bridge 53 | 54 | volumes: 55 | postgres: 56 | pgadmin: 57 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/services/DataService.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.services.score 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import java.time.OffsetDateTime 5 | import khivy.wordtrisserver.datamodel.* 6 | import khivy.wordtrisserver.repositories.ip.IpRepository 7 | import khivy.wordtrisserver.repositories.name.NameRepository 8 | import khivy.wordtrisserver.repositories.score.ScoreRepository 9 | import org.springframework.stereotype.Service 10 | 11 | 12 | @Service 13 | class DataService { 14 | @Autowired 15 | lateinit var scoreRepository: ScoreRepository 16 | 17 | @Autowired 18 | lateinit var nameRepository: NameRepository 19 | 20 | @Autowired 21 | lateinit var ipRepository: IpRepository 22 | 23 | fun saveScoreAndFlush(ip: String, data: PlayerSubmissionDataOuterClass.PlayerSubmissionData) { 24 | val ip = Ip(ip) 25 | ipRepository.saveAndFlush(ip) 26 | val name = Name(data.name, ip) 27 | nameRepository.saveAndFlush(name) 28 | val score = Score(data.score, name, OffsetDateTime.now()) 29 | scoreRepository.saveAndFlush(score) 30 | } 31 | 32 | fun evictLowestScoresFromList(possibleScoresToEvict: List, numToEvict: Int) { 33 | if (numToEvict <= 0) return 34 | val sorted = possibleScoresToEvict.sortedBy { score -> score.score } 35 | val allScoresToRemove = sorted 36 | .take(numToEvict) 37 | .map { score -> score.name_id } 38 | nameRepository.deleteAllByIdInBatch(allScoresToRemove) // TODO: Assert that it cascades. 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/datamodel/DataModel.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.datamodel 2 | 3 | import org.hibernate.annotations.OnDelete 4 | import org.hibernate.annotations.OnDeleteAction 5 | import java.time.OffsetDateTime 6 | import javax.persistence.* 7 | import java.io.Serializable 8 | import khivy.wordtrisserver.setup.* 9 | 10 | 11 | @Entity 12 | data class Ip( 13 | @Id 14 | @Column(nullable = false) 15 | val ip: String 16 | ) : Serializable { 17 | @OneToMany(mappedBy = "ip_fk") 18 | private var names: MutableSet = mutableSetOf() 19 | } 20 | 21 | @Entity 22 | data class Score( 23 | var score: Int, 24 | @OneToOne(fetch = FetchType.LAZY, optional = false) 25 | @OnDelete(action = OnDeleteAction.CASCADE) 26 | @MapsId 27 | @JoinColumn(name = "name_id") 28 | val name_fk: Name, 29 | @Column(name = "created_at") 30 | var created_at: OffsetDateTime 31 | ) : Serializable { 32 | @Id 33 | @Column(name = "name_id") 34 | var name_id: Long = 0 35 | } 36 | 37 | @Entity 38 | data class Name( 39 | @Column(name = "name", nullable = false, length = NAME_LENGTH_MAX) 40 | val name: String, 41 | @ManyToOne(fetch = FetchType.LAZY, optional = false) 42 | @OnDelete(action = OnDeleteAction.CASCADE) 43 | @JoinColumn(name = "ip_fk", nullable = false) 44 | val ip_fk: Ip, 45 | ) : Serializable { 46 | @Id 47 | @GeneratedValue(strategy = GenerationType.AUTO) 48 | @Column(name = "id") 49 | var id: Long = 0 50 | 51 | @OneToOne(mappedBy = "name_fk", cascade = [CascadeType.ALL]) 52 | @PrimaryKeyJoinColumn 53 | private var score: Score? = null 54 | } 55 | -------------------------------------------------------------------------------- /wordtris/test/webTest.ts: -------------------------------------------------------------------------------- 1 | import { hash } from "fast-sha256"; 2 | import { 3 | getLeaders, 4 | serializeWordsArray, 5 | submitScore, 6 | } from "../src/util/webUtil"; 7 | 8 | async function testSuccess() { 9 | let words = ["hag", "fish"]; 10 | return await submitScore(2, "SampleName", "127.0.0.1/32", words); 11 | } 12 | 13 | async function testFailureInvalidChecksum() { 14 | let words = ["hag", "fish"]; 15 | let falseWordsAsUInt8Array = serializeWordsArray(["hag", "fish", "fish"]); 16 | return await submitScore( 17 | 2, 18 | "SampleName", 19 | "127.0.0.1/32", 20 | words, 21 | hash(falseWordsAsUInt8Array), 22 | ); 23 | } 24 | 25 | async function testFailureInvalidScore() { 26 | let words = ["hag", "fish"]; 27 | return await submitScore( 28 | words.length + 1, 29 | "SampleName", 30 | "127.0.0.1/32", 31 | words, 32 | ); 33 | } 34 | 35 | async function testGetLeaders() { 36 | return await getLeaders(); 37 | } 38 | 39 | function runTests(): boolean { 40 | testFailureInvalidScore().then((res) => 41 | console.assert(res.status === 406, "Test failure: invalid score") 42 | ); 43 | testFailureInvalidChecksum().then((res) => 44 | console.assert(res.status === 406, "Test failure: invalid checksum") 45 | ); 46 | testSuccess().then((res) => 47 | console.assert(res.status === 202, "Test failure: valid submission") 48 | ); 49 | testGetLeaders().then((res) => 50 | console.assert( 51 | res.status === 202, 52 | "Test failure: failed to get leaderboard", 53 | ) 54 | ); 55 | return true; 56 | } 57 | 58 | runTests(); 59 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: 8 | - main 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup Node.js environment 33 | uses: actions/setup-node@v3.4.1 34 | 35 | - name: Install react-scripts 36 | run: npm add --prefix wordtris/ react-scripts 37 | 38 | - name: Install packages with NPM 39 | run: npm install --prefix wordtris/ 40 | 41 | - name: Build 42 | run: npm run --prefix wordtris/ build 43 | 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v1 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v1 48 | with: 49 | path: './wordtris/build' 50 | 51 | deploy: 52 | # Add a dependency to the build job 53 | needs: build 54 | 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@main 64 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/config/RedisConfig.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.config 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.boot.autoconfigure.AutoConfigureAfter 5 | import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.data.redis.connection.RedisConnectionFactory 9 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration 10 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory 11 | import org.springframework.data.redis.core.RedisOperations 12 | import org.springframework.data.redis.core.RedisTemplate 13 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer 14 | 15 | 16 | @Configuration 17 | @AutoConfigureAfter(RedisAutoConfiguration::class) 18 | class RedisConfig { 19 | 20 | @Value("\${spring.redis.host}") 21 | private val redisHost: String? = null 22 | 23 | @Value("\${spring.redis.port}") 24 | private val redisPort: Int? = null 25 | 26 | @Bean 27 | fun lettuceConnectionFactory(): RedisConnectionFactory? { 28 | return LettuceConnectionFactory(RedisStandaloneConfiguration(redisHost!!, redisPort!!)) 29 | } 30 | 31 | @Bean 32 | fun redisTemplate(lettuceConnectionFactory: RedisConnectionFactory?): RedisOperations? { 33 | val template = RedisTemplate() 34 | template.setConnectionFactory(lettuceConnectionFactory!!) 35 | template.valueSerializer = GenericJackson2JsonRedisSerializer() 36 | return template 37 | } 38 | } -------------------------------------------------------------------------------- /wordtris/src/components/PlayerBlock.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | _ENABLE_UP_KEY, 4 | BOARD_CELL_TEXT_COLOR, 5 | ENABLE_SMOOTH_FALL, 6 | interp, 7 | NORMAL_TEXT_SIZE, 8 | PLAYER_COLOR, 9 | UNIVERSAL_BORDER_RADIUS, 10 | } from "../setup"; 11 | import { UserCell } from "../util/UserCell"; 12 | 13 | export const PlayerBlock = React.memo( 14 | ( 15 | { isVisible, adjustedCells }: { 16 | isVisible: boolean; 17 | adjustedCells: UserCell[]; 18 | }, 19 | ) => { 20 | const adjustedCellsStyled = adjustedCells.map((cell) => { 21 | const divStyle = { 22 | background: PLAYER_COLOR, 23 | color: BOARD_CELL_TEXT_COLOR, 24 | fontSize: NORMAL_TEXT_SIZE, 25 | gridRow: cell.r + 1, 26 | gridColumn: cell.c + 1, 27 | marginTop: ENABLE_SMOOTH_FALL ? `${interp.val}%` : "0.4vmin", 28 | marginBottom: ENABLE_SMOOTH_FALL 29 | ? `${-interp.val}%` 30 | : "0.4vmin", 31 | marginLeft: "0.4vmin", 32 | marginRight: "0.4vmin", 33 | visibility: isVisible ? "visible" as const : "hidden" as const, 34 | zIndex: 1, 35 | }; 36 | return ( 37 |
42 | {cell.char} 43 |
44 | ); 45 | }); 46 | 47 | // Return an array of PlayerCells, adjusted to the 1-indexed CSS Grid. 48 | return <>{adjustedCellsStyled}; 49 | }, 50 | ); 51 | -------------------------------------------------------------------------------- /wordtris/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Wordtris 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /wordtris/src/components/BoardCells.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BoardCell } from "../util/BoardCell"; 3 | import { 4 | BOARD_CELL_COLOR, 5 | BOARD_CELL_TEXT_COLOR, 6 | EMPTY, 7 | EMPTY_CELL_COLOR, 8 | MATCH_COLOR, 9 | MATCH_TEXT_COLOR, 10 | NORMAL_TEXT_SIZE, 11 | } from "../setup"; 12 | 13 | export const BoardCells = React.memo( 14 | ({ boardCellMatrix }: { boardCellMatrix: BoardCell[][] }) => { 15 | const boardCells = boardCellMatrix.map((row, r) => 16 | row.map((cell, c) => { 17 | const bg = () => { 18 | if (cell.char === EMPTY) { 19 | return EMPTY_CELL_COLOR; 20 | } else if (cell.hasMatched) { 21 | return MATCH_COLOR; 22 | } else { 23 | return BOARD_CELL_COLOR; 24 | } 25 | }; 26 | const textColor = () => { 27 | if (cell.hasMatched) { 28 | return MATCH_TEXT_COLOR; 29 | } else { 30 | return BOARD_CELL_TEXT_COLOR; 31 | } 32 | }; 33 | const divStyle = { 34 | gridRow: r + 1, 35 | gridColumn: c + 1, 36 | background: bg(), 37 | color: textColor(), 38 | fontSize: NORMAL_TEXT_SIZE, 39 | } as const; 40 | 41 | return ( 42 |
47 | {cell.char} 48 |
49 | ); 50 | }) 51 | ); 52 | 53 | return <>{boardCells}; 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /wordtris/src/components/Leaderboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect } from "react"; 3 | import { 4 | BOARD_CELL_COLOR, 5 | LEADERBOARD_ROW_COLOR_1, 6 | LEADERBOARD_ROW_COLOR_2, 7 | SMALL_TEXT_SIZE, 8 | UNIVERSAL_BORDER_RADIUS, 9 | } from "../setup"; 10 | 11 | export const Leaderboard = React.memo( 12 | ({ leaders }: { leaders: Array<{ name: string; score: number }> }) => { 13 | const leaderboardRowStyle = { 14 | borderRadius: UNIVERSAL_BORDER_RADIUS, 15 | color: BOARD_CELL_COLOR, 16 | fontSize: SMALL_TEXT_SIZE, 17 | } as const; 18 | 19 | const leaderboardRows = leaders.map( 20 | (leader: { name: string; score: number }, index: number) => { 21 | return ( 22 |
31 | 32 |  {index}. {leader.name} 33 | {" "} 34 | 35 | {leader.score}  36 | 37 |
38 |
39 | ); 40 | }, 41 | ); 42 | 43 | const containerStyle = { 44 | flex: "auto", 45 | overflowY: "auto", 46 | height: "90vh", 47 | } as const; 48 | 49 | return ( 50 |
51 | <>{leaderboardRows} 52 |
53 | ); 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /wordtris/src/util/webUtil.ts: -------------------------------------------------------------------------------- 1 | import { PlayerSubmissionData } from "../protobuf_gen/PlayerSubmissionData"; 2 | import { hash } from "fast-sha256"; 3 | 4 | export function submitScore( 5 | score: number, 6 | name: string, 7 | words: string[], 8 | checksum?: Uint8Array, 9 | ): Promise { 10 | const data = PlayerSubmissionData.encode({ 11 | score, 12 | name, 13 | words, 14 | checksum: checksum ? checksum! : hash(serializeWordsArray(words)), 15 | }).finish(); 16 | 17 | return fetch( 18 | "https://wordtris-server.com/submitscore", 19 | { 20 | method: "PUT", 21 | headers: { 22 | "Content-Type": "application/x-protobuf", 23 | }, 24 | body: data, 25 | }, 26 | ); 27 | } 28 | 29 | export function getLeaders(): Promise { 30 | return fetch( 31 | "https://wordtris-server.com/leaderboard", 32 | { 33 | method: "GET", 34 | headers: { 35 | Accept: "application/json", 36 | }, 37 | }, 38 | ); 39 | } 40 | 41 | function isAsciiOnly(str: string) { 42 | for (let i = 0; i < str.length; i++) { 43 | if (str.charCodeAt(i) > 255) { 44 | return false; 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | export function serializeWordsArray(words: Array) { 51 | const joined = words.join(" "); 52 | console.assert( 53 | isAsciiOnly(joined), 54 | "Error: Given list of words isn't ASCII-only!", 55 | ); 56 | const serialized = Uint8Array.from( 57 | joined.split("").map((letter) => letter.charCodeAt(0)), 58 | ); 59 | return serialized; 60 | } 61 | 62 | export function getPlayerScores() { 63 | return fetch( 64 | "https://wordtris-server.com/leaderboard", 65 | { 66 | method: "GET", 67 | headers: { 68 | Accept: "application/json", 69 | }, 70 | }, 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /wordtris/src/setup.ts: -------------------------------------------------------------------------------- 1 | // Main features: 2 | export const ENABLE_SMOOTH_FALL = false; 3 | export const MIN_WORD_LENGTH = 3; 4 | export const ENABLE_INSTANT_DROP = true; 5 | // Debug features: 6 | export const _ENABLE_UP_KEY = true; 7 | export const _IS_PRINTING_STATE = false; 8 | 9 | export const TBD = "@"; 10 | export const EMPTY = ""; 11 | export const BOARD_ROWS = 10; 12 | export const BOARD_COLS = 9; 13 | 14 | // Interp determines the distance between the player block's current row and the next row. 15 | export const interp = { val: 0 }; 16 | export const interpRate = 1; 17 | export const interpKeydownMult = 30; 18 | export const interpMax = 100; 19 | 20 | /* Note: with 60 FPS, this is a float (16.666..7). Might run into issues. */ 21 | export const framesPerSecLimit = 60; 22 | export const frameStep = 1000 / framesPerSecLimit; 23 | 24 | /* The amount of time it takes before a block locks in place. */ 25 | export const lockMax = 1200; 26 | export const matchAnimLength = 750; 27 | export const groundExitPenaltyRate = 250; 28 | export const countdownTotalSecs = 3; 29 | 30 | export const boardCellFallDurationMillisecondsRate = 75; 31 | export const playerCellFallDurationMillisecondsRate = 10; 32 | 33 | export const FONT_COLOR = "#FFFFFF"; 34 | export const BOARD_COLOR = "#FDEDD8"; 35 | export const EMPTY_CELL_COLOR = "#F4A261"; 36 | export const BOARD_CELL_COLOR = "#2B5F8C"; 37 | export const PLAYER_COLOR = "#499F68"; 38 | export const MATCH_COLOR = "#FFEA00"; 39 | export const MATCH_TEXT_COLOR = BOARD_CELL_COLOR; 40 | export const MENU_TEXT_COLOR = "#FFFFFF"; 41 | export const BOARD_CELL_TEXT_COLOR = "#EBF5EE"; 42 | export const LEADERBOARD_HEADER_COLOR = "#f5bd90"; 43 | export const LEADERBOARD_ROW_COLOR_1 = "#f7ece4"; 44 | export const LEADERBOARD_ROW_COLOR_2 = "#f7caa6"; 45 | export const REFRESH_BUTTON_COLOR = "#609c76"; 46 | 47 | export const UNIVERSAL_BORDER_RADIUS = "1vmin"; 48 | export const CELL_SIZE = "7vmin"; 49 | export const LARGE_TEXT_SIZE = "7vmin"; 50 | export const NORMAL_TEXT_SIZE = "3.5vmin"; 51 | export const SMALL_TEXT_SIZE = "3.0vmin"; 52 | -------------------------------------------------------------------------------- /wordtris/src/components/WordList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | BOARD_CELL_COLOR, 4 | NORMAL_TEXT_SIZE, 5 | PLAYER_COLOR, 6 | SMALL_TEXT_SIZE, 7 | UNIVERSAL_BORDER_RADIUS, 8 | } from "../setup"; 9 | 10 | export const WordList = React.memo( 11 | ({ displayedWords }: { displayedWords: string[] }) => { 12 | const wordStyle = { 13 | background: BOARD_CELL_COLOR, 14 | padding: UNIVERSAL_BORDER_RADIUS, 15 | margin: UNIVERSAL_BORDER_RADIUS, 16 | borderRadius: UNIVERSAL_BORDER_RADIUS, 17 | fontSize: SMALL_TEXT_SIZE, 18 | fontStyle: "italic", 19 | } as const; 20 | 21 | const scrollBoxStyle = { 22 | flex: "auto", 23 | overflowY: "auto", 24 | height: 0, 25 | } as const; 26 | 27 | const titleStyle = { 28 | color: BOARD_CELL_COLOR, 29 | fontSize: NORMAL_TEXT_SIZE, 30 | } as const; 31 | 32 | const pointsStyle = { 33 | color: PLAYER_COLOR, 34 | fontSize: NORMAL_TEXT_SIZE, 35 | } as const; 36 | 37 | return ( 38 | <> 39 |
40 | Matches [ 41 | 42 | {displayedWords.length} 43 | 44 | ] 45 |
46 |
47 | <> 48 | {displayedWords.map((word, i) => ( 49 | // Invert the key to keep scroll bar at bottom if set to bottom. 50 |
55 | {word} 56 |
57 | ))} 58 | 59 |
60 | 61 | ); 62 | }, 63 | ); 64 | -------------------------------------------------------------------------------- /wordtris/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wordtris/src/components/FallingBlock.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { animated, useSpring } from "react-spring"; 3 | import { BoardCell } from "../util/BoardCell"; 4 | import { BOARD_CELL_TEXT_COLOR, NORMAL_TEXT_SIZE } from "../setup"; 5 | 6 | export const FallingBlock = React.memo( 7 | ( 8 | { fallingLetters, durationRate, color }: { 9 | fallingLetters: BoardCell[]; 10 | durationRate: number; 11 | color: string; 12 | }, 13 | ) => { 14 | const fallenLetters = fallingLetters 15 | .map((fallingLetterBeforeAndAfter) => ( 16 | 24 | )); 25 | return <>{fallenLetters}; 26 | }, 27 | ); 28 | 29 | const FallingLetter = React.memo( 30 | ( 31 | { fallingLetterBeforeAndAfter, durationRate, color }: { 32 | fallingLetterBeforeAndAfter: BoardCell[]; 33 | durationRate: number; 34 | color: string; 35 | }, 36 | ) => { 37 | console.assert(fallingLetterBeforeAndAfter.length == 2); 38 | const [before, after] = fallingLetterBeforeAndAfter; 39 | const margin = 100 * Math.abs(after.r - before.r); 40 | 41 | const styles = useSpring({ 42 | from: { 43 | gridRow: before.r + 1, 44 | gridColumn: before.c + 1, 45 | zIndex: 5, 46 | marginTop: "0%", 47 | marginBottom: "0%", 48 | }, 49 | to: { 50 | marginTop: `${margin}%`, 51 | marginBottom: `-${margin}%`, 52 | }, 53 | reset: true, 54 | config: { 55 | duration: durationRate * (after.r - before.r), 56 | }, 57 | }); 58 | 59 | const innerStyle = { 60 | height: "88%", 61 | background: color, 62 | color: BOARD_CELL_TEXT_COLOR, 63 | fontSize: NORMAL_TEXT_SIZE, 64 | } as const; 65 | 66 | return ( 67 | 68 |
72 | {before.char} 73 |
74 |
75 | ); 76 | }, 77 | ); 78 | -------------------------------------------------------------------------------- /wordtris-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import com.google.protobuf.gradle.* 3 | 4 | group = "khivy" 5 | version = "1.0.0" 6 | java.sourceCompatibility = JavaVersion.VERSION_17 7 | 8 | tasks.getByName("bootJar") { 9 | this.archiveFileName.set("app.jar") 10 | } 11 | 12 | plugins { 13 | id("org.springframework.boot") version "2.7.4" 14 | id("io.spring.dependency-management") version "1.0.14.RELEASE" 15 | kotlin("jvm") version "1.6.21" 16 | kotlin("plugin.spring") version "1.6.21" 17 | kotlin("plugin.jpa") version "1.6.21" 18 | id("java") 19 | id("idea") 20 | id("com.google.protobuf") version "0.8.19" 21 | } 22 | 23 | repositories { 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | implementation("org.springframework.boot:spring-boot-starter-mustache") 30 | implementation("org.springframework.boot:spring-boot-starter-web") 31 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 32 | implementation("org.jetbrains.kotlin:kotlin-reflect") 33 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 34 | implementation("com.google.protobuf:protobuf-gradle-plugin:0.8.19") 35 | implementation("com.google.protobuf:protobuf-kotlin:3.21.6") 36 | implementation("org.springframework.data:spring-data-bom:2021.2.3") 37 | implementation("redis.clients:jedis:4.2.3") 38 | implementation("org.springframework.data:spring-data-redis:2.7.3") 39 | implementation("com.bucket4j:bucket4j-core:8.1.0") 40 | developmentOnly("org.springframework.boot:spring-boot-devtools") 41 | implementation("io.lettuce:lettuce-core:6.2.0.RELEASE") 42 | runtimeOnly("com.h2database:h2") 43 | runtimeOnly("org.postgresql:postgresql") 44 | runtimeOnly("mysql:mysql-connector-java") 45 | implementation("org.springframework.boot:spring-boot-starter-test:2.7.3") 46 | implementation("com.google.protobuf:protobuf-java:3.21.6") 47 | implementation("io.grpc:grpc-stub:1.49.1") 48 | implementation("io.grpc:grpc-protobuf:1.49.1") 49 | if (JavaVersion.current().isJava9Compatible()) { 50 | // Workaround for @javax.annotation.Generated 51 | // see: https://github.com/grpc/grpc-java/issues/3633 52 | implementation("javax.annotation:javax.annotation-api:1.3.1") 53 | } 54 | protobuf(files("../protobuf/")) 55 | } 56 | 57 | protobuf { 58 | protoc { 59 | artifact = "com.google.protobuf:protoc:3.21.6" 60 | } 61 | 62 | generatedFilesBaseDir = "$projectDir/src/main/kotlin/khivy/wordtrisserver/protobuf_gen/" 63 | 64 | generateProtoTasks { 65 | ofSourceSet("main").forEach { 66 | it.builtins { 67 | id("kotlin") 68 | } 69 | } 70 | } 71 | } 72 | 73 | tasks.withType { 74 | kotlinOptions { 75 | freeCompilerArgs = listOf("-Xjsr305=strict") 76 | jvmTarget = "17" 77 | } 78 | } 79 | 80 | tasks.withType { 81 | useJUnitPlatform() 82 | } 83 | -------------------------------------------------------------------------------- /wordtris-server/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /wordtris/src/components/GameOverOverlay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ReactNode, useState } from "react"; 3 | import { 4 | MENU_TEXT_COLOR, 5 | NORMAL_TEXT_SIZE, 6 | PLAYER_COLOR, 7 | SMALL_TEXT_SIZE, 8 | UNIVERSAL_BORDER_RADIUS, 9 | } from "../setup"; 10 | import { submitScore } from "../util/webUtil"; 11 | 12 | export const GameOverOverlay = React.memo( 13 | ( 14 | { children, isVisible }: { 15 | children: ReactNode[]; 16 | isVisible: boolean; 17 | }, 18 | ) => { 19 | const divStyle = { 20 | visibility: isVisible ? "visible" as const : "hidden" as const, 21 | position: "absolute", 22 | top: "50%", 23 | left: "50%", 24 | whiteSpace: "nowrap", 25 | transform: "translate(-50%, -50%)", 26 | zIndex: 2, 27 | color: MENU_TEXT_COLOR, 28 | fontSize: "200%", 29 | background: "rgba(0, 0, 0, 0.5)", 30 | padding: UNIVERSAL_BORDER_RADIUS, 31 | borderRadius: UNIVERSAL_BORDER_RADIUS, 32 | } as const; 33 | 34 | return ( 35 |
36 | <> 37 | {children} 38 | 39 |
40 | ); 41 | }, 42 | ); 43 | 44 | export const PlayAgainButton = React.memo( 45 | ( 46 | { stateHandler, words }: { 47 | stateHandler: { send: (arg0: string) => void }; 48 | words: string[]; 49 | }, 50 | ) => { 51 | const buttonStyle = { 52 | cursor: "pointer", 53 | border: "none", 54 | background: PLAYER_COLOR, 55 | color: MENU_TEXT_COLOR, 56 | borderRadius: UNIVERSAL_BORDER_RADIUS, 57 | padding: "0.4vmin", 58 | textAlign: "center", 59 | marginTop: "0.4vmin", 60 | fontSize: NORMAL_TEXT_SIZE, 61 | } as const; 62 | 63 | const formStyle = { 64 | textAlign: "center", 65 | size: "20", 66 | fontSize: SMALL_TEXT_SIZE, 67 | placeholder: "Enter name", 68 | color: "black", 69 | } as const; 70 | 71 | const [name, setName] = useState("" as string); 72 | 73 | const handleChange = (event) => { 74 | setName(event.target.value); 75 | }; 76 | 77 | return ( 78 | <> 79 |
80 | 87 |
88 |
{ 92 | if (0 < name.length) { 93 | submitScore(words.length, name, words); 94 | } 95 | stateHandler.send("RESTART"); 96 | }} 97 | > 98 | {0 < name.length ? "Submit & Play Again" : "Play Again"} 99 |
100 | 101 | ); 102 | }, 103 | ); 104 | -------------------------------------------------------------------------------- /wordtris/src/util/weightedDistribution.ts: -------------------------------------------------------------------------------- 1 | export class WeightedDistribution { 2 | keys: K[]; 3 | prefixWeights: number[]; 4 | totalWeight: number; 5 | 6 | constructor(weightedDistribution: Iterable<[K, number]>) { 7 | const keys = []; 8 | const prefixWeights = []; 9 | let prefixWeight = 0; 10 | for (const [key, weight] of weightedDistribution) { 11 | prefixWeight += weight; 12 | keys.push(key); 13 | prefixWeights.push(prefixWeight); 14 | } 15 | this.keys = keys; 16 | this.prefixWeights = prefixWeights; 17 | this.totalWeight = prefixWeight; 18 | } 19 | 20 | static ofMap( 21 | weightedDistribution: Map, 22 | ): WeightedDistribution { 23 | return new WeightedDistribution(weightedDistribution.entries()); 24 | } 25 | 26 | static ofRecord( 27 | weightedDistribution: Record, 28 | ): WeightedDistribution { 29 | return new WeightedDistribution(Object.entries(weightedDistribution)); 30 | } 31 | 32 | get(weight: number): K | undefined { 33 | // Binary Search 34 | let left = 0; 35 | let right = this.prefixWeights.length - 1; 36 | while (left < right) { 37 | const mid = Math.floor((left + right) / 2); 38 | if (this.prefixWeights[mid]! < weight) { 39 | left = mid + 1; 40 | } else { 41 | right = mid; 42 | } 43 | } 44 | return this.keys[Math.floor((left + right) / 2)]; 45 | } 46 | 47 | getRandom(): K | undefined { 48 | const weight = Math.random() * this.totalWeight; 49 | return this.get(weight); 50 | } 51 | 52 | [Symbol.iterator](): Iterator> { 53 | return new WeightIterator(this); 54 | } 55 | 56 | static englishLetters(): WeightedDistribution { 57 | return WeightedDistribution.ofRecord({ 58 | a: 8.2, 59 | b: 1.5, 60 | c: 2.8, 61 | d: 4.3, 62 | e: 13, 63 | f: 2.2, 64 | g: 2, 65 | h: 6.1, 66 | i: 7, 67 | j: 0.15, 68 | k: 0.77, 69 | l: 4, 70 | m: 2.4, 71 | n: 6.7, 72 | o: 7.5, 73 | p: 1.9, 74 | q: 0.1, 75 | r: 6, 76 | s: 6.3, 77 | t: 9.1, 78 | u: 2.8, 79 | v: 1, 80 | w: 2.4, 81 | x: 0.15, 82 | y: 2, 83 | z: 0.074, 84 | }); 85 | } 86 | } 87 | 88 | export interface Weight { 89 | index: number; 90 | key: K; 91 | weight: number; 92 | prefixWeight: number; 93 | } 94 | 95 | class WeightIterator { 96 | weightedDistribution: WeightedDistribution; 97 | i: number; 98 | prevPrefixWeight: number; 99 | 100 | constructor(weightedDistribution: WeightedDistribution) { 101 | this.weightedDistribution = weightedDistribution; 102 | this.i = 0; 103 | this.prevPrefixWeight = 0; 104 | } 105 | 106 | next(): IteratorResult> { 107 | const index = this.i; 108 | if (index >= this.weightedDistribution.keys.length) { 109 | return { done: true, value: undefined }; 110 | } 111 | const key = this.weightedDistribution.keys[index]!; 112 | const prefixWeight = this.weightedDistribution.prefixWeights[index]!; 113 | const weight = prefixWeight - this.prevPrefixWeight; 114 | return { 115 | done: false, 116 | value: { 117 | index, 118 | key, 119 | weight, 120 | prefixWeight, 121 | }, 122 | }; 123 | } 124 | } 125 | 126 | const englishLetters = WeightedDistribution.englishLetters(); 127 | 128 | export function generateRandomChar(): string { 129 | return englishLetters.getRandom()!; 130 | } 131 | -------------------------------------------------------------------------------- /wordtris/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | BOARD_CELL_COLOR, 4 | LEADERBOARD_HEADER_COLOR, 5 | REFRESH_BUTTON_COLOR, 6 | SMALL_TEXT_SIZE, 7 | UNIVERSAL_BORDER_RADIUS, 8 | } from "../setup"; 9 | import { getLeaders } from "../util/webUtil"; 10 | import { Leaderboard } from "./Leaderboard"; 11 | 12 | export const Header = React.memo(({ refreshCallback, leaders }: { 13 | refreshCallback: () => void; 14 | leaders: Array<{ name: string; score: number }>; 15 | }) => { 16 | const outerStyle = { 17 | display: "flex", 18 | // The following options prevent the flex from filling & blocking the page from clicks. 19 | zIndex: 1, 20 | maxHeight: "0px", 21 | } as const; 22 | 23 | const leaderboardTitle = "⮛ Toggle Leaderboard"; 24 | return ( 25 |
26 |
27 | 28 |
29 | 34 |
35 | ); 36 | }); 37 | 38 | export const GameTitle = React.memo(() => { 39 | const containerStyle = { 40 | marginTop: "3vmin", 41 | marginLeft: "2vmin", 42 | zIndex: 20, 43 | } as const; 44 | 45 | const textStyle = { 46 | fontSize: "4vmin", 47 | textTransform: "uppercase", 48 | fontWeight: "bolder", 49 | color: BOARD_CELL_COLOR, 50 | padding: "10px", 51 | fontFamily: `"Press Start 2P"`, 52 | } as const; 53 | 54 | return ( 55 | 63 | ); 64 | }); 65 | 66 | export const LeaderboardToggle = React.memo( 67 | ({ title, refreshCallback, leaders }: { 68 | title: string; 69 | refreshCallback: () => void; 70 | leaders: Array<{ name: string; score: number }>; 71 | }) => { 72 | const [isVisible, setIsVisible] = React.useState(false); 73 | 74 | const toggleContainerStyle = { 75 | zIndex: 30, 76 | height: "100vh", 77 | marginLeft: "auto", 78 | maxHeight: "0px", 79 | } as const; 80 | 81 | const adjustTogglePositionStyle = { 82 | marginTop: "3vmin", 83 | marginRight: "2vmin", 84 | maxHeight: "0px", 85 | background: "red", 86 | } as const; 87 | 88 | const staticToggleStyle = { 89 | zIndex: 20, 90 | cursor: "pointer", 91 | fontWeight: "bold", 92 | textAlign: "center", 93 | fontSize: SMALL_TEXT_SIZE, 94 | background: LEADERBOARD_HEADER_COLOR, 95 | color: BOARD_CELL_COLOR, 96 | paddingTop: "0.5vmin", 97 | paddingLeft: "1vmin", 98 | paddingRight: "1vmin", 99 | borderTopLeftRadius: UNIVERSAL_BORDER_RADIUS, 100 | borderBottomLeftRadius: UNIVERSAL_BORDER_RADIUS, 101 | } as const; 102 | 103 | const toggleStyle = { 104 | visibility: isVisible ? "visible" as const : "hidden" as const, 105 | marginRight: "1.5vmin", 106 | border: "none", 107 | } as const; 108 | 109 | const refreshStyle = { 110 | background: REFRESH_BUTTON_COLOR, 111 | paddingTop: "1vmin", 112 | paddingRight: "2vmin", 113 | paddingLeft: "2vmin", 114 | borderTopRightRadius: UNIVERSAL_BORDER_RADIUS, 115 | borderBottomRightRadius: UNIVERSAL_BORDER_RADIUS, 116 | color: "white", 117 | cursor: "pointer", 118 | textAlign: "center", 119 | fontSize: SMALL_TEXT_SIZE, 120 | } as const; 121 | 122 | return ( 123 |
124 |
125 |
126 |
{ 128 | setIsVisible((prev) => !prev); 129 | }} 130 | style={staticToggleStyle} 131 | > 132 | {title} 133 |
134 |
{ 136 | refreshCallback(); 137 | }} 138 | style={refreshStyle} 139 | > 140 | ⟳ 141 |
142 |
143 |
144 | 145 |
146 |
147 |
148 | ); 149 | }, 150 | ); 151 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/protobuf_gen/main/kotlin/PlayerSubmissionDataKt.kt: -------------------------------------------------------------------------------- 1 | //Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: PlayerSubmissionData.proto 3 | 4 | @kotlin.jvm.JvmName("-initializeplayerSubmissionData") 5 | inline fun playerSubmissionData(block: PlayerSubmissionDataKt.Dsl.() -> kotlin.Unit): PlayerSubmissionDataOuterClass.PlayerSubmissionData = 6 | PlayerSubmissionDataKt.Dsl._create(PlayerSubmissionDataOuterClass.PlayerSubmissionData.newBuilder()).apply { block() }._build() 7 | object PlayerSubmissionDataKt { 8 | @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) 9 | @com.google.protobuf.kotlin.ProtoDslMarker 10 | class Dsl private constructor( 11 | private val _builder: PlayerSubmissionDataOuterClass.PlayerSubmissionData.Builder 12 | ) { 13 | companion object { 14 | @kotlin.jvm.JvmSynthetic 15 | @kotlin.PublishedApi 16 | internal fun _create(builder: PlayerSubmissionDataOuterClass.PlayerSubmissionData.Builder): Dsl = Dsl(builder) 17 | } 18 | 19 | @kotlin.jvm.JvmSynthetic 20 | @kotlin.PublishedApi 21 | internal fun _build(): PlayerSubmissionDataOuterClass.PlayerSubmissionData = _builder.build() 22 | 23 | /** 24 | * int32 score = 1; 25 | */ 26 | var score: kotlin.Int 27 | @JvmName("getScore") 28 | get() = _builder.getScore() 29 | @JvmName("setScore") 30 | set(value) { 31 | _builder.setScore(value) 32 | } 33 | /** 34 | * int32 score = 1; 35 | */ 36 | fun clearScore() { 37 | _builder.clearScore() 38 | } 39 | 40 | /** 41 | * string name = 2; 42 | */ 43 | var name: kotlin.String 44 | @JvmName("getName") 45 | get() = _builder.getName() 46 | @JvmName("setName") 47 | set(value) { 48 | _builder.setName(value) 49 | } 50 | /** 51 | * string name = 2; 52 | */ 53 | fun clearName() { 54 | _builder.clearName() 55 | } 56 | 57 | /** 58 | * An uninstantiable, behaviorless type to represent the field in 59 | * generics. 60 | */ 61 | @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) 62 | class WordsProxy private constructor() : com.google.protobuf.kotlin.DslProxy() 63 | /** 64 | * repeated string words = 3; 65 | * @return A list containing the words. 66 | */ 67 | val words: com.google.protobuf.kotlin.DslList 68 | @kotlin.jvm.JvmSynthetic 69 | get() = com.google.protobuf.kotlin.DslList( 70 | _builder.getWordsList() 71 | ) 72 | /** 73 | * repeated string words = 3; 74 | * @param value The words to add. 75 | */ 76 | @kotlin.jvm.JvmSynthetic 77 | @kotlin.jvm.JvmName("addWords") 78 | fun com.google.protobuf.kotlin.DslList.add(value: kotlin.String) { 79 | _builder.addWords(value) 80 | } 81 | /** 82 | * repeated string words = 3; 83 | * @param value The words to add. 84 | */ 85 | @kotlin.jvm.JvmSynthetic 86 | @kotlin.jvm.JvmName("plusAssignWords") 87 | @Suppress("NOTHING_TO_INLINE") 88 | inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(value: kotlin.String) { 89 | add(value) 90 | } 91 | /** 92 | * repeated string words = 3; 93 | * @param values The words to add. 94 | */ 95 | @kotlin.jvm.JvmSynthetic 96 | @kotlin.jvm.JvmName("addAllWords") 97 | fun com.google.protobuf.kotlin.DslList.addAll(values: kotlin.collections.Iterable) { 98 | _builder.addAllWords(values) 99 | } 100 | /** 101 | * repeated string words = 3; 102 | * @param values The words to add. 103 | */ 104 | @kotlin.jvm.JvmSynthetic 105 | @kotlin.jvm.JvmName("plusAssignAllWords") 106 | @Suppress("NOTHING_TO_INLINE") 107 | inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(values: kotlin.collections.Iterable) { 108 | addAll(values) 109 | } 110 | /** 111 | * repeated string words = 3; 112 | * @param index The index to set the value at. 113 | * @param value The words to set. 114 | */ 115 | @kotlin.jvm.JvmSynthetic 116 | @kotlin.jvm.JvmName("setWords") 117 | operator fun com.google.protobuf.kotlin.DslList.set(index: kotlin.Int, value: kotlin.String) { 118 | _builder.setWords(index, value) 119 | }/** 120 | * repeated string words = 3; 121 | */ 122 | @kotlin.jvm.JvmSynthetic 123 | @kotlin.jvm.JvmName("clearWords") 124 | fun com.google.protobuf.kotlin.DslList.clear() { 125 | _builder.clearWords() 126 | } 127 | /** 128 | * bytes checksum = 4; 129 | */ 130 | var checksum: com.google.protobuf.ByteString 131 | @JvmName("getChecksum") 132 | get() = _builder.getChecksum() 133 | @JvmName("setChecksum") 134 | set(value) { 135 | _builder.setChecksum(value) 136 | } 137 | /** 138 | * bytes checksum = 4; 139 | */ 140 | fun clearChecksum() { 141 | _builder.clearChecksum() 142 | } 143 | } 144 | } 145 | @kotlin.jvm.JvmSynthetic 146 | inline fun PlayerSubmissionDataOuterClass.PlayerSubmissionData.copy(block: PlayerSubmissionDataKt.Dsl.() -> kotlin.Unit): PlayerSubmissionDataOuterClass.PlayerSubmissionData = 147 | PlayerSubmissionDataKt.Dsl._create(this.toBuilder()).apply { block() }._build() 148 | 149 | -------------------------------------------------------------------------------- /wordtris/src/util/playerUtil.tsx: -------------------------------------------------------------------------------- 1 | import "../App.css"; 2 | import { generateRandomChar } from "../util/weightedDistribution"; 3 | import { 4 | BOARD_COLS, 5 | BOARD_ROWS, 6 | EMPTY, 7 | interp, 8 | interpMax, 9 | interpRate, 10 | TBD, 11 | } from "../setup"; 12 | import { UserCell } from "./UserCell"; 13 | import { BoardCell } from "./BoardCell"; 14 | import { getGroundHeight } from "./boardUtil"; 15 | 16 | export const spawnPos: readonly [number, number] = [1, 4]; 17 | export const layout = [ 18 | [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY], 19 | [EMPTY, EMPTY, TBD, EMPTY, EMPTY], 20 | [EMPTY, EMPTY, TBD, EMPTY, EMPTY], 21 | [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY], 22 | [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY], 23 | ] as const; 24 | 25 | export function rotateCells( 26 | cells: UserCell[], 27 | isClockwise: boolean, 28 | ): UserCell[] { 29 | console.assert(layout.length === layout[0].length); 30 | console.assert(layout.length % 2 === 1); 31 | const mid = Math.floor(layout.length / 2); 32 | return cells.map(({ r, c, char, uid }) => { 33 | // Center around mid. 34 | // Remember, top-left is `(0, 0)` and bot-right is `(last, last)`. 35 | const r2 = r - mid; 36 | const c2 = c - mid; 37 | if (r2 !== 0 || c2 !== 0) { 38 | r = (isClockwise ? c2 : -c2) + mid; 39 | c = (isClockwise ? -r2 : r2) + mid; 40 | } 41 | return { 42 | r, 43 | c, 44 | char, 45 | uid, 46 | }; 47 | }); 48 | } 49 | 50 | export function getAdjustedLeftmostC(adjustedCells: UserCell[]): number { 51 | return adjustedCells.reduce((prev, cur) => prev.c < cur.c ? prev : cur).c; 52 | } 53 | 54 | export function getAdjustedRightmostC(adjustedCells: UserCell[]): number { 55 | return adjustedCells.reduce((prev, cur) => prev.c < cur.c ? cur : prev).c; 56 | } 57 | 58 | export function getAdjustedTopR(adjustedCells: UserCell[]): number { 59 | return adjustedCells.reduce((prev, cur) => prev.r < cur.r ? prev : cur).r; 60 | } 61 | 62 | export function getAdjustedBottomR(adjustedCells: UserCell[]): number { 63 | return adjustedCells.reduce((prev, cur) => prev.r < cur.r ? cur : prev).r; 64 | } 65 | 66 | export function isInRBounds(r: number): boolean { 67 | return 0 <= r && r < BOARD_ROWS; 68 | } 69 | 70 | export function isInCBounds(c: number): boolean { 71 | return 0 <= c && c < BOARD_COLS; 72 | } 73 | 74 | // Returns the number of times crossed onto a new row. 75 | export function doGradualFall( 76 | board: BoardCell[][], 77 | adjustedCells: UserCell[], 78 | ): number { 79 | interp.val += interpRate; 80 | if ( 81 | adjustedCells.some((cell) => 82 | !isInRBounds(cell.r + 1) || 83 | board[cell.r + 1][cell.c].char !== EMPTY 84 | ) 85 | ) { 86 | interp.val = 0; 87 | } 88 | let dr = 0; 89 | while (interpMax <= interp.val) { 90 | dr += 1; 91 | interp.val -= interpMax; 92 | } 93 | return dr; 94 | } 95 | 96 | export function generateUserCells(): UserCell[] { 97 | // Return starting block matrix of UserCells with randomly-assigned characters. 98 | // TODO: Make it pseudo-random. 99 | return layout.flatMap((row, r) => 100 | row.map((ch, c) => 101 | ch === TBD && ({ 102 | r, 103 | c, 104 | char: generateRandomChar(), 105 | uid: `user(${r},${c})`, 106 | }) 107 | ).filter((e): e is UserCell => !!e) 108 | ); 109 | } 110 | 111 | export function convertCellsToAdjusted( 112 | cells: UserCell[], 113 | pos: readonly [number, number], 114 | ) { 115 | return cells.map((cell) => getAdjustedUserCell(cell, pos)); 116 | } 117 | 118 | // Take a UserCell with coordinates based on the matrix, and adjust its height by `pos` and matrix center. 119 | export function getAdjustedUserCell( 120 | { r, c, char, uid }: UserCell, 121 | pos: readonly [number, number], 122 | ): UserCell { 123 | return { 124 | r: r + pos[0] - Math.floor(layout.length / 2), 125 | c: c + pos[1] - Math.floor(layout[0].length / 2), 126 | char, 127 | uid, 128 | }; 129 | } 130 | 131 | export function isPlayerTouchingGround( 132 | cells: UserCell[], 133 | board: BoardCell[][], 134 | ) { 135 | return cells.some((cell) => { 136 | return cell.r >= getGroundHeight(cell.c, cell.r, board); 137 | }); 138 | } 139 | 140 | export function dropFloatingCells( 141 | board: BoardCell[][], 142 | ): { 143 | boardWithoutFallCells: BoardCell[][]; 144 | postFallCells: BoardCell[]; 145 | preFallCells: BoardCell[]; 146 | } { 147 | const newBoard = board.slice(); 148 | const postFallCells: BoardCell[] = []; 149 | const preFallCells: BoardCell[] = []; 150 | for (let r = BOARD_ROWS - 2; r >= 0; --r) { 151 | for (let c = BOARD_COLS - 1; c >= 0; --c) { 152 | if ( 153 | newBoard[r][c].char !== EMPTY && 154 | newBoard[r + 1][c].char === EMPTY 155 | ) { 156 | const g = getGroundHeight(c, r, newBoard); 157 | const char = newBoard[r][c].char; 158 | newBoard[g][c].char = char; 159 | newBoard[r][c].char = EMPTY; 160 | // Update cell in placedCells. 161 | postFallCells.push({ r: g, c, char, hasMatched: false }); 162 | preFallCells.push({ r, c, char, hasMatched: false }); 163 | } 164 | } 165 | } 166 | // Remove chars here, since the iteration logic above depends on changes. 167 | const boardWithoutFallCells = newBoard; 168 | postFallCells.forEach((cell) => 169 | boardWithoutFallCells[cell.r][cell.c].char = EMPTY 170 | ); 171 | return { boardWithoutFallCells, postFallCells, preFallCells }; 172 | } 173 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/web/ScoreController.kt: -------------------------------------------------------------------------------- 1 | package khivy.wordtrisserver.web 2 | 3 | import PlayerSubmissionDataOuterClass 4 | import io.github.bucket4j.Bandwidth 5 | import io.github.bucket4j.Bucket 6 | import io.github.bucket4j.Refill 7 | import khivy.wordtrisserver.services.CacheService 8 | import khivy.wordtrisserver.services.ProfanityFilterService 9 | import khivy.wordtrisserver.services.score.DataService 10 | import khivy.wordtrisserver.setup.MAX_SCORES_PER_IP 11 | import khivy.wordtrisserver.setup.NAME_LENGTH_MAX 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.http.HttpStatus 14 | import org.springframework.http.ResponseEntity 15 | import org.springframework.web.bind.annotation.* 16 | import java.security.MessageDigest 17 | import java.time.Duration 18 | import javax.servlet.http.HttpServletRequest 19 | 20 | 21 | @RestController 22 | @CrossOrigin(origins = ["*"]) 23 | class ScoreController { 24 | 25 | @Autowired 26 | lateinit var dataService: DataService 27 | 28 | @Autowired 29 | lateinit var cacheService: CacheService 30 | 31 | @Autowired 32 | lateinit var profanityFilterService: ProfanityFilterService 33 | 34 | lateinit var submitScoreBucket: Bucket 35 | lateinit var getLeaderboardBucket: Bucket 36 | lateinit var getPlayerScoreBucket: Bucket 37 | 38 | @Autowired 39 | fun ScoreController() { 40 | val submitScoreLimit: Bandwidth = Bandwidth.classic(8, Refill.greedy(8, Duration.ofMinutes(1))) 41 | this.submitScoreBucket = Bucket.builder() 42 | .addLimit(submitScoreLimit) 43 | .build() 44 | 45 | val getLeaderboardLimit: Bandwidth = Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1))) 46 | this.getLeaderboardBucket = Bucket.builder() 47 | .addLimit(getLeaderboardLimit) 48 | .build() 49 | 50 | val getPlayerScoreLimit: Bandwidth = Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1))) 51 | this.getPlayerScoreBucket = Bucket.builder() 52 | .addLimit(getPlayerScoreLimit) 53 | .build() 54 | } 55 | 56 | @PutMapping(value = ["/submitscore"], consumes = ["application/x-protobuf"]) 57 | @ResponseBody 58 | fun submitScore(@RequestBody body: PlayerSubmissionDataOuterClass.PlayerSubmissionData, request: HttpServletRequest): ResponseEntity { 59 | 60 | if (!submitScoreBucket.tryConsume(1)) { 61 | return ResponseEntity(HttpStatus.TOO_MANY_REQUESTS) 62 | } 63 | 64 | if (profanityFilterService.containsProfanity(body.name)) { 65 | return ResponseEntity(HttpStatus.PRECONDITION_FAILED) 66 | } 67 | 68 | // Verify checksum. 69 | val md = MessageDigest.getInstance("SHA-256") 70 | val theirChecksum = body.checksum.toByteArray() 71 | val myChecksum = md.digest(body.wordsList.joinTo(StringBuilder(), separator = " ").toString().toByteArray()) 72 | if (body.wordsList.size != body.score || !myChecksum.contentEquals(theirChecksum)) { 73 | println("""Did not accept score: 74 | |Either given checksum: ${theirChecksum.contentHashCode()}!=${myChecksum.contentHashCode()} 75 | |or given score: ${body.score}!=${body.wordsList.size} 76 | """.trimMargin()) 77 | return ResponseEntity(HttpStatus.NOT_ACCEPTABLE) 78 | } 79 | 80 | // Ignores request if name is too long. 81 | if (NAME_LENGTH_MAX < body.name.length) { 82 | return ResponseEntity(HttpStatus.NOT_ACCEPTABLE) 83 | } 84 | 85 | dataService.saveScoreAndFlush(request.remoteAddr, body) 86 | 87 | // Evicts the lowest score(s) that match the IP & Name combination. 88 | val scoresMatchingIpAndName = dataService.scoreRepository.findScoresWithGivenIpAndNameNative(request.remoteAddr, body.name) 89 | dataService.evictLowestScoresFromList(scoresMatchingIpAndName, scoresMatchingIpAndName.size - 1) 90 | 91 | // Evicts the lowest score(s) from the IP. 92 | val scoresMatchingIp = dataService.scoreRepository.findScoresWithGivenIpNative(request.remoteAddr) 93 | if (MAX_SCORES_PER_IP < scoresMatchingIp.size) { 94 | dataService.evictLowestScoresFromList(scoresMatchingIp, scoresMatchingIp.size - MAX_SCORES_PER_IP) 95 | } 96 | 97 | // Evict cached leaders if this score is a new leader, so that on the next leaderboard request it is submitted. 98 | if (cacheService.getLowestLeaderScoreInt() < body.score) { 99 | cacheService.evictLeaders() 100 | } 101 | 102 | return ResponseEntity(HttpStatus.ACCEPTED) 103 | } 104 | 105 | data class LeaderboardResponse( 106 | val score: Int, 107 | val name: String, 108 | ) 109 | 110 | @RequestMapping("/leaderboard") 111 | @ResponseBody 112 | fun getLeaderboard(): ResponseEntity> { 113 | if (!getLeaderboardBucket.tryConsume(1)) { 114 | return ResponseEntity(HttpStatus.TOO_MANY_REQUESTS) 115 | } 116 | 117 | val body = cacheService.getLeaders().map{ LeaderboardResponse(it.score, it.name_fk.name) } 118 | return ResponseEntity(body, HttpStatus.ACCEPTED) 119 | } 120 | 121 | data class PlayerScoreResponse( 122 | val score: Int, 123 | ) 124 | 125 | @RequestMapping("/score") 126 | @ResponseBody 127 | fun getPlayerScore(request: HttpServletRequest): ResponseEntity> { 128 | if (!getPlayerScoreBucket.tryConsume(1)) { 129 | return ResponseEntity(HttpStatus.TOO_MANY_REQUESTS) 130 | } 131 | 132 | val body = dataService.scoreRepository.findScoresWithGivenIpNative(request.remoteAddr).map{ PlayerScoreResponse(it.score) } 133 | return ResponseEntity(body, HttpStatus.ACCEPTED) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /wordtris/src/protobuf_gen/PlayerSubmissionData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as _m0 from "protobufjs/minimal"; 3 | 4 | export const protobufPackage = ""; 5 | 6 | export interface PlayerSubmissionData { 7 | score: number; 8 | name: string; 9 | words: string[]; 10 | checksum: Uint8Array; 11 | } 12 | 13 | function createBasePlayerSubmissionData(): PlayerSubmissionData { 14 | return { score: 0, name: "", words: [], checksum: new Uint8Array() }; 15 | } 16 | 17 | export const PlayerSubmissionData = { 18 | encode( 19 | message: PlayerSubmissionData, 20 | writer: _m0.Writer = _m0.Writer.create(), 21 | ): _m0.Writer { 22 | if (message.score !== 0) { 23 | writer.uint32(8).int32(message.score); 24 | } 25 | if (message.name !== "") { 26 | writer.uint32(18).string(message.name); 27 | } 28 | for (const v of message.words) { 29 | writer.uint32(26).string(v!); 30 | } 31 | if (message.checksum.length !== 0) { 32 | writer.uint32(34).bytes(message.checksum); 33 | } 34 | return writer; 35 | }, 36 | 37 | decode( 38 | input: _m0.Reader | Uint8Array, 39 | length?: number, 40 | ): PlayerSubmissionData { 41 | const reader = input instanceof _m0.Reader 42 | ? input 43 | : new _m0.Reader(input); 44 | let end = length === undefined ? reader.len : reader.pos + length; 45 | const message = createBasePlayerSubmissionData(); 46 | while (reader.pos < end) { 47 | const tag = reader.uint32(); 48 | switch (tag >>> 3) { 49 | case 1: 50 | message.score = reader.int32(); 51 | break; 52 | case 2: 53 | message.name = reader.string(); 54 | break; 55 | case 3: 56 | message.words.push(reader.string()); 57 | break; 58 | case 4: 59 | message.checksum = reader.bytes(); 60 | break; 61 | default: 62 | reader.skipType(tag & 7); 63 | break; 64 | } 65 | } 66 | return message; 67 | }, 68 | 69 | fromJSON(object: any): PlayerSubmissionData { 70 | return { 71 | score: isSet(object.score) ? Number(object.score) : 0, 72 | name: isSet(object.name) ? String(object.name) : "", 73 | words: Array.isArray(object?.words) 74 | ? object.words.map((e: any) => String(e)) 75 | : [], 76 | checksum: isSet(object.checksum) 77 | ? bytesFromBase64(object.checksum) 78 | : new Uint8Array(), 79 | }; 80 | }, 81 | 82 | toJSON(message: PlayerSubmissionData): unknown { 83 | const obj: any = {}; 84 | message.score !== undefined && (obj.score = Math.round(message.score)); 85 | message.name !== undefined && (obj.name = message.name); 86 | if (message.words) { 87 | obj.words = message.words.map((e) => e); 88 | } else { 89 | obj.words = []; 90 | } 91 | message.checksum !== undefined && 92 | (obj.checksum = base64FromBytes( 93 | message.checksum !== undefined 94 | ? message.checksum 95 | : new Uint8Array(), 96 | )); 97 | return obj; 98 | }, 99 | 100 | fromPartial, I>>( 101 | object: I, 102 | ): PlayerSubmissionData { 103 | const message = createBasePlayerSubmissionData(); 104 | message.score = object.score ?? 0; 105 | message.name = object.name ?? ""; 106 | message.words = object.words?.map((e) => e) || []; 107 | message.checksum = object.checksum ?? new Uint8Array(); 108 | return message; 109 | }, 110 | }; 111 | 112 | declare var self: any | undefined; 113 | declare var window: any | undefined; 114 | declare var global: any | undefined; 115 | var globalThis: any = (() => { 116 | if (typeof globalThis !== "undefined") { 117 | return globalThis; 118 | } 119 | if (typeof self !== "undefined") { 120 | return self; 121 | } 122 | if (typeof window !== "undefined") { 123 | return window; 124 | } 125 | if (typeof global !== "undefined") { 126 | return global; 127 | } 128 | throw "Unable to locate global object"; 129 | })(); 130 | 131 | function bytesFromBase64(b64: string): Uint8Array { 132 | if (globalThis.Buffer) { 133 | return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); 134 | } else { 135 | const bin = globalThis.atob(b64); 136 | const arr = new Uint8Array(bin.length); 137 | for (let i = 0; i < bin.length; ++i) { 138 | arr[i] = bin.charCodeAt(i); 139 | } 140 | return arr; 141 | } 142 | } 143 | 144 | function base64FromBytes(arr: Uint8Array): string { 145 | if (globalThis.Buffer) { 146 | return globalThis.Buffer.from(arr).toString("base64"); 147 | } else { 148 | const bin: string[] = []; 149 | arr.forEach((byte) => { 150 | bin.push(String.fromCharCode(byte)); 151 | }); 152 | return globalThis.btoa(bin.join("")); 153 | } 154 | } 155 | 156 | type Builtin = 157 | | Date 158 | | Function 159 | | Uint8Array 160 | | string 161 | | number 162 | | boolean 163 | | undefined; 164 | 165 | export type DeepPartial = T extends Builtin ? T 166 | : T extends Array ? Array> 167 | : T extends ReadonlyArray ? ReadonlyArray> 168 | : T extends {} ? { [K in keyof T]?: DeepPartial } 169 | : Partial; 170 | 171 | type KeysOfUnion = T extends T ? keyof T : never; 172 | export type Exact = P extends Builtin ? P 173 | : 174 | & P 175 | & { [K in keyof P]: Exact } 176 | & { [K in Exclude>]: never }; 177 | 178 | function isSet(value: any): boolean { 179 | return value !== null && value !== undefined; 180 | } 181 | -------------------------------------------------------------------------------- /wordtris/src/components/Prompt.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ReactNode } from "react"; 3 | import { BOARD_CELL_COLOR } from "../setup"; 4 | import { useEffect, useState } from "react"; 5 | 6 | const SMALL_WINDOW_WIDTH_THRESHOLD = 1000; 7 | 8 | export const Prompt = React.memo( 9 | ( 10 | { children, keydownCallback }: { 11 | children: ReactNode; 12 | keydownCallback: ({ code }: { code: string }) => void; 13 | }, 14 | ) => { 15 | // To align `` above the game. 16 | const promptContainerStyle = { 17 | flexDirection: "column", 18 | display: "flex", 19 | } as const; 20 | 21 | const promptSize = 3; 22 | const paddingSize = 3; 23 | 24 | const promptStyle = { 25 | textAlign: "center", 26 | fontSize: `${promptSize}vmin`, 27 | paddingBottom: `${paddingSize}vmin`, 28 | color: BOARD_CELL_COLOR, 29 | } as const; 30 | 31 | return ( 32 |
33 | Create words of 3+ letters 34 | {children} 35 | 39 |
40 | ); 41 | }, 42 | ); 43 | 44 | export const Keys = React.memo( 45 | ( 46 | { totalSize, keydownCallback }: { 47 | totalSize: number; 48 | keydownCallback: ({ code }: { code: string }) => void; 49 | }, 50 | ) => { 51 | const [isSmallScreen, setIsSmallScreen] = useState( 52 | globalThis.innerWidth < SMALL_WINDOW_WIDTH_THRESHOLD, 53 | ); 54 | 55 | function handleResize() { 56 | setIsSmallScreen( 57 | globalThis.innerWidth < SMALL_WINDOW_WIDTH_THRESHOLD, 58 | ); 59 | } 60 | useEffect(() => { 61 | globalThis.addEventListener("resize", handleResize); 62 | return () => globalThis.removeEventListener("resize", handleResize); 63 | }); 64 | 65 | const keyHeight = 4; 66 | 67 | const containerStyle = { 68 | marginTop: `${totalSize - keyHeight}vmin`, 69 | display: "flex", 70 | justifyContent: "center", 71 | gap: "3.5vmin", 72 | fontSize: "2.5vmin", 73 | whiteSpace: "break-spaces", 74 | color: BOARD_CELL_COLOR, 75 | } as const; 76 | 77 | const guideCombinedBlockStyle = { 78 | display: "flex", 79 | flexDirection: isSmallScreen ? "column" : "row", 80 | gap: "0.5vmin", 81 | alignItems: "center", 82 | } as const; 83 | 84 | const guideKeyBlockStyle = { 85 | display: "flex", 86 | gap: "0.2vmin", 87 | } as const; 88 | 89 | const guideTextStyle = { 90 | display: "flex", 91 | alignItems: "center", 92 | } as const; 93 | 94 | const keyDivWrapper = { 95 | display: "flex", 96 | } as const; 97 | 98 | return ( 99 |
100 |
101 |
102 |
105 | keydownCallback({ code: "ArrowLeft" })} 106 | > 107 | 112 |
113 |
116 | keydownCallback({ code: "ArrowDown" })} 117 | > 118 | 123 |
124 |
127 | keydownCallback({ code: "ArrowRight" })} 128 | > 129 | 134 |
135 |
136 |
Move
137 |
138 |
139 |
keydownCallback({ code: "KeyZ" })} 142 | > 143 | 148 |
149 |
Rotate ↺
150 |
151 |
152 |
keydownCallback({ code: "KeyX" })} 155 | > 156 | 161 | 166 |
167 |
Rotate ↻
168 |
169 |
170 |
keydownCallback({ code: "Space" })} 173 | > 174 | 179 |
180 |
Drop
181 |
182 |
183 | ); 184 | }, 185 | ); 186 | 187 | export const Key = React.memo( 188 | ( 189 | { char, keyHeight, isSmallScreen }: { 190 | char: string; 191 | keyHeight: number; 192 | isSmallScreen: boolean; 193 | }, 194 | ) => { 195 | const adjustedKeyHeight = isSmallScreen ? keyHeight * 2 : keyHeight; 196 | const adjustedKeyTextHeight = isSmallScreen ? 6 : 3; 197 | 198 | const keyStyle = { 199 | display: "block", 200 | height: `${adjustedKeyHeight}vmin`, 201 | width: char === "space" 202 | ? `${adjustedKeyHeight * 2.5}vmin` 203 | : `${adjustedKeyHeight}vmin`, 204 | background: "white", 205 | border: `0.4vmin solid ${"grey"}`, 206 | borderRadius: "0.5vmin", 207 | fontSize: `${adjustedKeyTextHeight}vmin`, 208 | userSelect: "none", 209 | cursor: "pointer", 210 | } as const; 211 | 212 | const keyTextStyle = { 213 | textAlign: "center", 214 | } as const; 215 | 216 | return ( 217 |
218 |
{char}
219 |
220 | ); 221 | }, 222 | ); 223 | -------------------------------------------------------------------------------- /wordtris-server/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /wordtris-server/src/main/resources/banned_words.txt: -------------------------------------------------------------------------------- 1 | 2 girls 1 cup 2 | 2g1c 3 | 4r5e 4 | 5h1t 5 | 5hit 6 | a55 7 | a_s_s 8 | acrotomophilia 9 | alabama hot pocket 10 | alaskan pipeline 11 | anal 12 | anilingus 13 | anus 14 | apeshit 15 | ar5e 16 | arrse 17 | arse 18 | arsehole 19 | ass 20 | ass-fucker 21 | ass-hat 22 | ass-pirate 23 | assbag 24 | assbandit 25 | assbanger 26 | assbite 27 | assclown 28 | asscock 29 | asscracker 30 | asses 31 | assface 32 | assfucker 33 | assfukka 34 | assgoblin 35 | asshat 36 | asshead 37 | asshole 38 | assholes 39 | asshopper 40 | assjacker 41 | asslick 42 | asslicker 43 | assmonkey 44 | assmunch 45 | assmuncher 46 | asspirate 47 | assshole 48 | asssucker 49 | asswad 50 | asswhole 51 | asswipe 52 | auto erotic 53 | autoerotic 54 | b!tch 55 | b00bs 56 | b17ch 57 | b1tch 58 | babeland 59 | baby batter 60 | baby juice 61 | ball gag 62 | ball gravy 63 | ball kicking 64 | ball licking 65 | ball sack 66 | ball sucking 67 | ballbag 68 | balls 69 | ballsack 70 | bampot 71 | bangbros 72 | bareback 73 | barely legal 74 | barenaked 75 | bastard 76 | bastardo 77 | bastinado 78 | bbw 79 | bdsm 80 | beaner 81 | beaners 82 | beastial 83 | beastiality 84 | beastility 85 | beaver cleaver 86 | beaver lips 87 | bellend 88 | bestial 89 | bestiality 90 | bi+ch 91 | biatch 92 | big black 93 | big breasts 94 | big knockers 95 | big tits 96 | bimbos 97 | birdlock 98 | bitch 99 | bitcher 100 | bitchers 101 | bitches 102 | bitchin 103 | bitching 104 | black cock 105 | blonde action 106 | blonde on blonde action 107 | bloody 108 | blow job 109 | blow your load 110 | blowjob 111 | blowjobs 112 | blue waffle 113 | blumpkin 114 | boiolas 115 | bollock 116 | bollocks 117 | bollok 118 | bollox 119 | bondage 120 | boner 121 | boob 122 | boobie 123 | boobs 124 | booobs 125 | boooobs 126 | booooobs 127 | booooooobs 128 | booty call 129 | breasts 130 | brown showers 131 | brunette action 132 | buceta 133 | bugger 134 | bukkake 135 | bulldyke 136 | bullet vibe 137 | bullshit 138 | bum 139 | bung hole 140 | bunghole 141 | bunny fucker 142 | busty 143 | butt 144 | butt-pirate 145 | buttcheeks 146 | butthole 147 | buttmunch 148 | buttplug 149 | c0ck 150 | c0cksucker 151 | camel toe 152 | camgirl 153 | camslut 154 | camwhore 155 | carpet muncher 156 | carpetmuncher 157 | cawk 158 | chinc 159 | chink 160 | choad 161 | chocolate rosebuds 162 | chode 163 | cipa 164 | circlejerk 165 | cl1t 166 | cleveland steamer 167 | clit 168 | clitface 169 | clitoris 170 | clits 171 | clover clamps 172 | clusterfuck 173 | cnut 174 | cock 175 | cock-sucker 176 | cockbite 177 | cockburger 178 | cockface 179 | cockhead 180 | cockjockey 181 | cockknoker 182 | cockmaster 183 | cockmongler 184 | cockmongruel 185 | cockmonkey 186 | cockmunch 187 | cockmuncher 188 | cocknose 189 | cocknugget 190 | cocks 191 | cockshit 192 | cocksmith 193 | cocksmoker 194 | cocksuck 195 | cocksuck 196 | cocksucked 197 | cocksucked 198 | cocksucker 199 | cocksucking 200 | cocksucks 201 | cocksuka 202 | cocksukka 203 | cok 204 | cokmuncher 205 | coksucka 206 | coochie 207 | coochy 208 | coon 209 | coons 210 | cooter 211 | coprolagnia 212 | coprophilia 213 | cornhole 214 | cox 215 | crap 216 | creampie 217 | cum 218 | cumbubble 219 | cumdumpster 220 | cumguzzler 221 | cumjockey 222 | cummer 223 | cumming 224 | cums 225 | cumshot 226 | cumslut 227 | cumtart 228 | cunilingus 229 | cunillingus 230 | cunnie 231 | cunnilingus 232 | cunt 233 | cuntface 234 | cunthole 235 | cuntlick 236 | cuntlick 237 | cuntlicker 238 | cuntlicker 239 | cuntlicking 240 | cuntlicking 241 | cuntrag 242 | cunts 243 | cyalis 244 | cyberfuc 245 | cyberfuck 246 | cyberfucked 247 | cyberfucker 248 | cyberfuckers 249 | cyberfucking 250 | d1ck 251 | dammit 252 | damn 253 | darkie 254 | date rape 255 | daterape 256 | deep throat 257 | deepthroat 258 | dendrophilia 259 | dick 260 | dickbag 261 | dickbeater 262 | dickface 263 | dickhead 264 | dickhole 265 | dickjuice 266 | dickmilk 267 | dickmonger 268 | dickslap 269 | dicksucker 270 | dickwad 271 | dickweasel 272 | dickweed 273 | dickwod 274 | dike 275 | dildo 276 | dildos 277 | dingleberries 278 | dingleberry 279 | dink 280 | dinks 281 | dipshit 282 | dirsa 283 | dirty pillows 284 | dirty sanchez 285 | dlck 286 | dog style 287 | dog-fucker 288 | doggie style 289 | doggiestyle 290 | doggin 291 | dogging 292 | doggy style 293 | doggystyle 294 | dolcett 295 | domination 296 | dominatrix 297 | dommes 298 | donkey punch 299 | donkeyribber 300 | doochbag 301 | dookie 302 | doosh 303 | double dong 304 | double penetration 305 | douche 306 | douchebag 307 | dp action 308 | dry hump 309 | duche 310 | dumbshit 311 | dumshit 312 | dvda 313 | dyke 314 | eat my ass 315 | ecchi 316 | ejaculate 317 | ejaculated 318 | ejaculates 319 | ejaculating 320 | ejaculatings 321 | ejaculation 322 | ejakulate 323 | erotic 324 | erotism 325 | escort 326 | eunuch 327 | f u c k 328 | f u c k e r 329 | f4nny 330 | f_u_c_k 331 | fag 332 | fagbag 333 | fagg 334 | fagging 335 | faggit 336 | faggitt 337 | faggot 338 | faggs 339 | fagot 340 | fagots 341 | fags 342 | fagtard 343 | fanny 344 | fannyflaps 345 | fannyfucker 346 | fanyy 347 | fart 348 | farted 349 | farting 350 | farty 351 | fatass 352 | fcuk 353 | fcuker 354 | fcuking 355 | fecal 356 | feck 357 | fecker 358 | felatio 359 | felch 360 | felching 361 | fellate 362 | fellatio 363 | feltch 364 | female squirting 365 | femdom 366 | figging 367 | fingerbang 368 | fingerfuck 369 | fingerfucked 370 | fingerfucker 371 | fingerfuckers 372 | fingerfucking 373 | fingerfucks 374 | fingering 375 | fistfuck 376 | fistfucked 377 | fistfucker 378 | fistfuckers 379 | fistfucking 380 | fistfuckings 381 | fistfucks 382 | fisting 383 | flamer 384 | flange 385 | fook 386 | fooker 387 | foot fetish 388 | footjob 389 | frotting 390 | fuck 391 | fuck buttons 392 | fucka 393 | fucked 394 | fucker 395 | fuckers 396 | fuckhead 397 | fuckheads 398 | fuckin 399 | fucking 400 | fuckings 401 | fuckingshitmotherfucker 402 | fuckme 403 | fucks 404 | fucktards 405 | fuckwhit 406 | fuckwit 407 | fudge packer 408 | fudgepacker 409 | fuk 410 | fuker 411 | fukker 412 | fukkin 413 | fuks 414 | fukwhit 415 | fukwit 416 | futanari 417 | fux 418 | fux0r 419 | g-spot 420 | gang bang 421 | gangbang 422 | gangbanged 423 | gangbanged 424 | gangbangs 425 | gay sex 426 | gayass 427 | gaybob 428 | gaydo 429 | gaylord 430 | gaysex 431 | gaytard 432 | gaywad 433 | genitals 434 | giant cock 435 | girl on 436 | girl on top 437 | girls gone wild 438 | goatcx 439 | goatse 440 | god damn 441 | god-dam 442 | god-damned 443 | goddamn 444 | goddamned 445 | gokkun 446 | golden shower 447 | goo girl 448 | gooch 449 | goodpoop 450 | gook 451 | goregasm 452 | gringo 453 | grope 454 | group sex 455 | guido 456 | guro 457 | hand job 458 | handjob 459 | hard core 460 | hardcore 461 | hardcoresex 462 | heeb 463 | hell 464 | hentai 465 | heshe 466 | ho 467 | hoar 468 | hoare 469 | hoe 470 | hoer 471 | homo 472 | homoerotic 473 | honkey 474 | honky 475 | hooker 476 | hore 477 | horniest 478 | horny 479 | hot carl 480 | hot chick 481 | hotsex 482 | how to kill 483 | how to murder 484 | huge fat 485 | humping 486 | incest 487 | intercourse 488 | jack off 489 | jack-off 490 | jackass 491 | jackoff 492 | jail bait 493 | jailbait 494 | jap 495 | jelly donut 496 | jerk off 497 | jerk-off 498 | jigaboo 499 | jiggaboo 500 | jiggerboo 501 | jism 502 | jiz 503 | jiz 504 | jizm 505 | jizm 506 | jizz 507 | juggs 508 | kawk 509 | kike 510 | kinbaku 511 | kinkster 512 | kinky 513 | kiunt 514 | knob 515 | knobbing 516 | knobead 517 | knobed 518 | knobend 519 | knobhead 520 | knobjocky 521 | knobjokey 522 | kock 523 | kondum 524 | kondums 525 | kooch 526 | kootch 527 | kum 528 | kumer 529 | kummer 530 | kumming 531 | kums 532 | kunilingus 533 | kunt 534 | kyke 535 | l3i+ch 536 | l3itch 537 | labia 538 | leather restraint 539 | leather straight jacket 540 | lemon party 541 | lesbo 542 | lezzie 543 | lmfao 544 | lolita 545 | lovemaking 546 | lust 547 | lusting 548 | m0f0 549 | m0fo 550 | m45terbate 551 | ma5terb8 552 | ma5terbate 553 | make me come 554 | male squirting 555 | masochist 556 | master-bate 557 | masterb8 558 | masterbat* 559 | masterbat3 560 | masterbate 561 | masterbation 562 | masterbations 563 | masturbate 564 | menage a trois 565 | milf 566 | minge 567 | missionary position 568 | mo-fo 569 | mof0 570 | mofo 571 | mothafuck 572 | mothafucka 573 | mothafuckas 574 | mothafuckaz 575 | mothafucked 576 | mothafucker 577 | mothafuckers 578 | mothafuckin 579 | mothafucking 580 | mothafuckings 581 | mothafucks 582 | mother fucker 583 | motherfuck 584 | motherfucked 585 | motherfucker 586 | motherfuckers 587 | motherfuckin 588 | motherfucking 589 | motherfuckings 590 | motherfuckka 591 | motherfucks 592 | mound of venus 593 | mr hands 594 | muff 595 | muff diver 596 | muffdiver 597 | muffdiving 598 | mutha 599 | muthafecker 600 | muthafuckker 601 | muther 602 | mutherfucker 603 | n1gga 604 | n1gger 605 | nambla 606 | nawashi 607 | nazi 608 | negro 609 | neonazi 610 | nig nog 611 | nigg3r 612 | nigg4h 613 | nigga 614 | niggah 615 | niggas 616 | niggaz 617 | nigger 618 | niggers 619 | niglet 620 | nimphomania 621 | nipple 622 | nipples 623 | nob 624 | nob jokey 625 | nobhead 626 | nobjocky 627 | nobjokey 628 | nsfw images 629 | nude 630 | nudity 631 | numbnuts 632 | nutsack 633 | nympho 634 | nymphomania 635 | octopussy 636 | omorashi 637 | one cup two girls 638 | one guy one jar 639 | orgasim 640 | orgasim 641 | orgasims 642 | orgasm 643 | orgasms 644 | orgy 645 | p0rn 646 | paedophile 647 | paki 648 | panooch 649 | panties 650 | panty 651 | pawn 652 | pecker 653 | peckerhead 654 | pedobear 655 | pedophile 656 | pegging 657 | penis 658 | penisfucker 659 | phone sex 660 | phonesex 661 | phuck 662 | phuk 663 | phuked 664 | phuking 665 | phukked 666 | phukking 667 | phuks 668 | phuq 669 | piece of shit 670 | pigfucker 671 | pimpis 672 | pis 673 | pises 674 | pisin 675 | pising 676 | pisof 677 | piss 678 | piss pig 679 | pissed 680 | pisser 681 | pissers 682 | pisses 683 | pissflap 684 | pissflaps 685 | pissin 686 | pissin 687 | pissing 688 | pissoff 689 | pissoff 690 | pisspig 691 | playboy 692 | pleasure chest 693 | pole smoker 694 | polesmoker 695 | pollock 696 | ponyplay 697 | poo 698 | poof 699 | poon 700 | poonani 701 | poonany 702 | poontang 703 | poop 704 | poop chute 705 | poopchute 706 | porn 707 | porno 708 | pornography 709 | pornos 710 | prick 711 | pricks 712 | prince albert piercing 713 | pron 714 | pthc 715 | pube 716 | pubes 717 | punanny 718 | punany 719 | punta 720 | pusies 721 | pusse 722 | pussi 723 | pussies 724 | pussy 725 | pussylicking 726 | pussys 727 | pusy 728 | puto 729 | queaf 730 | queef 731 | queerbait 732 | queerhole 733 | quim 734 | raghead 735 | raging boner 736 | rape 737 | raping 738 | rapist 739 | rectum 740 | renob 741 | retard 742 | reverse cowgirl 743 | rimjaw 744 | rimjob 745 | rimming 746 | rosy palm 747 | rosy palm and her 5 sisters 748 | ruski 749 | rusty trombone 750 | s hit 751 | s&m 752 | s.o.b. 753 | s_h_i_t 754 | sadism 755 | sadist 756 | santorum 757 | scat 758 | schlong 759 | scissoring 760 | screwing 761 | scroat 762 | scrote 763 | scrotum 764 | semen 765 | sex 766 | sexo 767 | sexy 768 | sh!+ 769 | sh!t 770 | sh1t 771 | shag 772 | shagger 773 | shaggin 774 | shagging 775 | shaved beaver 776 | shaved pussy 777 | shemale 778 | shi+ 779 | shibari 780 | shit 781 | shit-ass 782 | shit-bag 783 | shit-bagger 784 | shit-brain 785 | shit-breath 786 | shit-cunt 787 | shit-dick 788 | shit-eating 789 | shit-face 790 | shit-faced 791 | shit-fit 792 | shit-head 793 | shit-heel 794 | shit-hole 795 | shit-house 796 | shit-load 797 | shit-pot 798 | shit-spitter 799 | shit-stain 800 | shitass 801 | shitbag 802 | shitbagger 803 | shitblimp 804 | shitbrain 805 | shitbreath 806 | shitcunt 807 | shitdick 808 | shite 809 | shiteating 810 | shited 811 | shitey 812 | shitface 813 | shitfaced 814 | shitfit 815 | shitfuck 816 | shitfull 817 | shithead 818 | shitheel 819 | shithole 820 | shithouse 821 | shiting 822 | shitings 823 | shitload 824 | shitpot 825 | shits 826 | shitspitter 827 | shitstain 828 | shitted 829 | shitter 830 | shitters 831 | shittiest 832 | shitting 833 | shittings 834 | shitty 835 | shitty 836 | shity 837 | shiz 838 | shiznit 839 | shota 840 | shrimping 841 | skank 842 | skeet 843 | slanteye 844 | slut 845 | slutbag 846 | sluts 847 | smeg 848 | smegma 849 | smut 850 | snatch 851 | snowballing 852 | sodomize 853 | sodomy 854 | son-of-a-bitch 855 | spac 856 | spic 857 | spick 858 | splooge 859 | splooge moose 860 | spooge 861 | spread legs 862 | spunk 863 | strap on 864 | strapon 865 | strappado 866 | strip club 867 | style doggy 868 | suck 869 | sucks 870 | suicide girls 871 | sultry women 872 | swastika 873 | swinger 874 | t1tt1e5 875 | t1tties 876 | tainted love 877 | tard 878 | taste my 879 | tea bagging 880 | teets 881 | teez 882 | testical 883 | testicle 884 | threesome 885 | throating 886 | thundercunt 887 | tied up 888 | tight white 889 | tit 890 | titfuck 891 | tits 892 | titt 893 | tittie5 894 | tittiefucker 895 | titties 896 | titty 897 | tittyfuck 898 | tittywank 899 | titwank 900 | tongue in a 901 | topless 902 | tosser 903 | towelhead 904 | tranny 905 | tribadism 906 | tub girl 907 | tubgirl 908 | turd 909 | tushy 910 | tw4t 911 | twat 912 | twathead 913 | twatlips 914 | twatty 915 | twink 916 | twinkie 917 | two girls one cup 918 | twunt 919 | twunter 920 | undressing 921 | upskirt 922 | urethra play 923 | urophilia 924 | v14gra 925 | v1gra 926 | va-j-j 927 | vag 928 | vagina 929 | venus mound 930 | viagra 931 | vibrator 932 | violet wand 933 | vjayjay 934 | vorarephilia 935 | voyeur 936 | vulva 937 | w00se 938 | wang 939 | wank 940 | wanker 941 | wanky 942 | wet dream 943 | wetback 944 | white power 945 | whoar 946 | whore 947 | willies 948 | willy 949 | wrapping men 950 | wrinkled starfish 951 | xrated 952 | xx 953 | xxx 954 | yaoi 955 | yellow showers 956 | yiffy 957 | zoophilia 958 | 🖕 959 | -------------------------------------------------------------------------------- /wordtris-server/src/main/kotlin/khivy/wordtrisserver/protobuf_gen/main/java/PlayerSubmissionDataOuterClass.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: PlayerSubmissionData.proto 3 | 4 | public final class PlayerSubmissionDataOuterClass { 5 | private PlayerSubmissionDataOuterClass() {} 6 | public static void registerAllExtensions( 7 | com.google.protobuf.ExtensionRegistryLite registry) { 8 | } 9 | 10 | public static void registerAllExtensions( 11 | com.google.protobuf.ExtensionRegistry registry) { 12 | registerAllExtensions( 13 | (com.google.protobuf.ExtensionRegistryLite) registry); 14 | } 15 | public interface PlayerSubmissionDataOrBuilder extends 16 | // @@protoc_insertion_point(interface_extends:PlayerSubmissionData) 17 | com.google.protobuf.MessageOrBuilder { 18 | 19 | /** 20 | * int32 score = 1; 21 | * @return The score. 22 | */ 23 | int getScore(); 24 | 25 | /** 26 | * string name = 2; 27 | * @return The name. 28 | */ 29 | java.lang.String getName(); 30 | /** 31 | * string name = 2; 32 | * @return The bytes for name. 33 | */ 34 | com.google.protobuf.ByteString 35 | getNameBytes(); 36 | 37 | /** 38 | * repeated string words = 3; 39 | * @return A list containing the words. 40 | */ 41 | java.util.List 42 | getWordsList(); 43 | /** 44 | * repeated string words = 3; 45 | * @return The count of words. 46 | */ 47 | int getWordsCount(); 48 | /** 49 | * repeated string words = 3; 50 | * @param index The index of the element to return. 51 | * @return The words at the given index. 52 | */ 53 | java.lang.String getWords(int index); 54 | /** 55 | * repeated string words = 3; 56 | * @param index The index of the value to return. 57 | * @return The bytes of the words at the given index. 58 | */ 59 | com.google.protobuf.ByteString 60 | getWordsBytes(int index); 61 | 62 | /** 63 | * bytes checksum = 4; 64 | * @return The checksum. 65 | */ 66 | com.google.protobuf.ByteString getChecksum(); 67 | } 68 | /** 69 | * Protobuf type {@code PlayerSubmissionData} 70 | */ 71 | public static final class PlayerSubmissionData extends 72 | com.google.protobuf.GeneratedMessageV3 implements 73 | // @@protoc_insertion_point(message_implements:PlayerSubmissionData) 74 | PlayerSubmissionDataOrBuilder { 75 | private static final long serialVersionUID = 0L; 76 | // Use PlayerSubmissionData.newBuilder() to construct. 77 | private PlayerSubmissionData(com.google.protobuf.GeneratedMessageV3.Builder builder) { 78 | super(builder); 79 | } 80 | private PlayerSubmissionData() { 81 | name_ = ""; 82 | words_ = com.google.protobuf.LazyStringArrayList.EMPTY; 83 | checksum_ = com.google.protobuf.ByteString.EMPTY; 84 | } 85 | 86 | @java.lang.Override 87 | @SuppressWarnings({"unused"}) 88 | protected java.lang.Object newInstance( 89 | UnusedPrivateParameter unused) { 90 | return new PlayerSubmissionData(); 91 | } 92 | 93 | @java.lang.Override 94 | public final com.google.protobuf.UnknownFieldSet 95 | getUnknownFields() { 96 | return this.unknownFields; 97 | } 98 | private PlayerSubmissionData( 99 | com.google.protobuf.CodedInputStream input, 100 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 101 | throws com.google.protobuf.InvalidProtocolBufferException { 102 | this(); 103 | if (extensionRegistry == null) { 104 | throw new java.lang.NullPointerException(); 105 | } 106 | int mutable_bitField0_ = 0; 107 | com.google.protobuf.UnknownFieldSet.Builder unknownFields = 108 | com.google.protobuf.UnknownFieldSet.newBuilder(); 109 | try { 110 | boolean done = false; 111 | while (!done) { 112 | int tag = input.readTag(); 113 | switch (tag) { 114 | case 0: 115 | done = true; 116 | break; 117 | case 8: { 118 | 119 | score_ = input.readInt32(); 120 | break; 121 | } 122 | case 18: { 123 | java.lang.String s = input.readStringRequireUtf8(); 124 | 125 | name_ = s; 126 | break; 127 | } 128 | case 26: { 129 | java.lang.String s = input.readStringRequireUtf8(); 130 | if (!((mutable_bitField0_ & 0x00000001) != 0)) { 131 | words_ = new com.google.protobuf.LazyStringArrayList(); 132 | mutable_bitField0_ |= 0x00000001; 133 | } 134 | words_.add(s); 135 | break; 136 | } 137 | case 34: { 138 | 139 | checksum_ = input.readBytes(); 140 | break; 141 | } 142 | default: { 143 | if (!parseUnknownField( 144 | input, unknownFields, extensionRegistry, tag)) { 145 | done = true; 146 | } 147 | break; 148 | } 149 | } 150 | } 151 | } catch (com.google.protobuf.InvalidProtocolBufferException e) { 152 | throw e.setUnfinishedMessage(this); 153 | } catch (com.google.protobuf.UninitializedMessageException e) { 154 | throw e.asInvalidProtocolBufferException().setUnfinishedMessage(this); 155 | } catch (java.io.IOException e) { 156 | throw new com.google.protobuf.InvalidProtocolBufferException( 157 | e).setUnfinishedMessage(this); 158 | } finally { 159 | if (((mutable_bitField0_ & 0x00000001) != 0)) { 160 | words_ = words_.getUnmodifiableView(); 161 | } 162 | this.unknownFields = unknownFields.build(); 163 | makeExtensionsImmutable(); 164 | } 165 | } 166 | public static final com.google.protobuf.Descriptors.Descriptor 167 | getDescriptor() { 168 | return PlayerSubmissionDataOuterClass.internal_static_PlayerSubmissionData_descriptor; 169 | } 170 | 171 | @java.lang.Override 172 | protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable 173 | internalGetFieldAccessorTable() { 174 | return PlayerSubmissionDataOuterClass.internal_static_PlayerSubmissionData_fieldAccessorTable 175 | .ensureFieldAccessorsInitialized( 176 | PlayerSubmissionDataOuterClass.PlayerSubmissionData.class, PlayerSubmissionDataOuterClass.PlayerSubmissionData.Builder.class); 177 | } 178 | 179 | public static final int SCORE_FIELD_NUMBER = 1; 180 | private int score_; 181 | /** 182 | * int32 score = 1; 183 | * @return The score. 184 | */ 185 | @java.lang.Override 186 | public int getScore() { 187 | return score_; 188 | } 189 | 190 | public static final int NAME_FIELD_NUMBER = 2; 191 | private volatile java.lang.Object name_; 192 | /** 193 | * string name = 2; 194 | * @return The name. 195 | */ 196 | @java.lang.Override 197 | public java.lang.String getName() { 198 | java.lang.Object ref = name_; 199 | if (ref instanceof java.lang.String) { 200 | return (java.lang.String) ref; 201 | } else { 202 | com.google.protobuf.ByteString bs = 203 | (com.google.protobuf.ByteString) ref; 204 | java.lang.String s = bs.toStringUtf8(); 205 | name_ = s; 206 | return s; 207 | } 208 | } 209 | /** 210 | * string name = 2; 211 | * @return The bytes for name. 212 | */ 213 | @java.lang.Override 214 | public com.google.protobuf.ByteString 215 | getNameBytes() { 216 | java.lang.Object ref = name_; 217 | if (ref instanceof java.lang.String) { 218 | com.google.protobuf.ByteString b = 219 | com.google.protobuf.ByteString.copyFromUtf8( 220 | (java.lang.String) ref); 221 | name_ = b; 222 | return b; 223 | } else { 224 | return (com.google.protobuf.ByteString) ref; 225 | } 226 | } 227 | 228 | public static final int WORDS_FIELD_NUMBER = 3; 229 | private com.google.protobuf.LazyStringList words_; 230 | /** 231 | * repeated string words = 3; 232 | * @return A list containing the words. 233 | */ 234 | public com.google.protobuf.ProtocolStringList 235 | getWordsList() { 236 | return words_; 237 | } 238 | /** 239 | * repeated string words = 3; 240 | * @return The count of words. 241 | */ 242 | public int getWordsCount() { 243 | return words_.size(); 244 | } 245 | /** 246 | * repeated string words = 3; 247 | * @param index The index of the element to return. 248 | * @return The words at the given index. 249 | */ 250 | public java.lang.String getWords(int index) { 251 | return words_.get(index); 252 | } 253 | /** 254 | * repeated string words = 3; 255 | * @param index The index of the value to return. 256 | * @return The bytes of the words at the given index. 257 | */ 258 | public com.google.protobuf.ByteString 259 | getWordsBytes(int index) { 260 | return words_.getByteString(index); 261 | } 262 | 263 | public static final int CHECKSUM_FIELD_NUMBER = 4; 264 | private com.google.protobuf.ByteString checksum_; 265 | /** 266 | * bytes checksum = 4; 267 | * @return The checksum. 268 | */ 269 | @java.lang.Override 270 | public com.google.protobuf.ByteString getChecksum() { 271 | return checksum_; 272 | } 273 | 274 | private byte memoizedIsInitialized = -1; 275 | @java.lang.Override 276 | public final boolean isInitialized() { 277 | byte isInitialized = memoizedIsInitialized; 278 | if (isInitialized == 1) return true; 279 | if (isInitialized == 0) return false; 280 | 281 | memoizedIsInitialized = 1; 282 | return true; 283 | } 284 | 285 | @java.lang.Override 286 | public void writeTo(com.google.protobuf.CodedOutputStream output) 287 | throws java.io.IOException { 288 | if (score_ != 0) { 289 | output.writeInt32(1, score_); 290 | } 291 | if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) { 292 | com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_); 293 | } 294 | for (int i = 0; i < words_.size(); i++) { 295 | com.google.protobuf.GeneratedMessageV3.writeString(output, 3, words_.getRaw(i)); 296 | } 297 | if (!checksum_.isEmpty()) { 298 | output.writeBytes(4, checksum_); 299 | } 300 | unknownFields.writeTo(output); 301 | } 302 | 303 | @java.lang.Override 304 | public int getSerializedSize() { 305 | int size = memoizedSize; 306 | if (size != -1) return size; 307 | 308 | size = 0; 309 | if (score_ != 0) { 310 | size += com.google.protobuf.CodedOutputStream 311 | .computeInt32Size(1, score_); 312 | } 313 | if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) { 314 | size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, name_); 315 | } 316 | { 317 | int dataSize = 0; 318 | for (int i = 0; i < words_.size(); i++) { 319 | dataSize += computeStringSizeNoTag(words_.getRaw(i)); 320 | } 321 | size += dataSize; 322 | size += 1 * getWordsList().size(); 323 | } 324 | if (!checksum_.isEmpty()) { 325 | size += com.google.protobuf.CodedOutputStream 326 | .computeBytesSize(4, checksum_); 327 | } 328 | size += unknownFields.getSerializedSize(); 329 | memoizedSize = size; 330 | return size; 331 | } 332 | 333 | @java.lang.Override 334 | public boolean equals(final java.lang.Object obj) { 335 | if (obj == this) { 336 | return true; 337 | } 338 | if (!(obj instanceof PlayerSubmissionDataOuterClass.PlayerSubmissionData)) { 339 | return super.equals(obj); 340 | } 341 | PlayerSubmissionDataOuterClass.PlayerSubmissionData other = (PlayerSubmissionDataOuterClass.PlayerSubmissionData) obj; 342 | 343 | if (getScore() 344 | != other.getScore()) return false; 345 | if (!getName() 346 | .equals(other.getName())) return false; 347 | if (!getWordsList() 348 | .equals(other.getWordsList())) return false; 349 | if (!getChecksum() 350 | .equals(other.getChecksum())) return false; 351 | if (!unknownFields.equals(other.unknownFields)) return false; 352 | return true; 353 | } 354 | 355 | @java.lang.Override 356 | public int hashCode() { 357 | if (memoizedHashCode != 0) { 358 | return memoizedHashCode; 359 | } 360 | int hash = 41; 361 | hash = (19 * hash) + getDescriptor().hashCode(); 362 | hash = (37 * hash) + SCORE_FIELD_NUMBER; 363 | hash = (53 * hash) + getScore(); 364 | hash = (37 * hash) + NAME_FIELD_NUMBER; 365 | hash = (53 * hash) + getName().hashCode(); 366 | if (getWordsCount() > 0) { 367 | hash = (37 * hash) + WORDS_FIELD_NUMBER; 368 | hash = (53 * hash) + getWordsList().hashCode(); 369 | } 370 | hash = (37 * hash) + CHECKSUM_FIELD_NUMBER; 371 | hash = (53 * hash) + getChecksum().hashCode(); 372 | hash = (29 * hash) + unknownFields.hashCode(); 373 | memoizedHashCode = hash; 374 | return hash; 375 | } 376 | 377 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 378 | java.nio.ByteBuffer data) 379 | throws com.google.protobuf.InvalidProtocolBufferException { 380 | return PARSER.parseFrom(data); 381 | } 382 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 383 | java.nio.ByteBuffer data, 384 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 385 | throws com.google.protobuf.InvalidProtocolBufferException { 386 | return PARSER.parseFrom(data, extensionRegistry); 387 | } 388 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 389 | com.google.protobuf.ByteString data) 390 | throws com.google.protobuf.InvalidProtocolBufferException { 391 | return PARSER.parseFrom(data); 392 | } 393 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 394 | com.google.protobuf.ByteString data, 395 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 396 | throws com.google.protobuf.InvalidProtocolBufferException { 397 | return PARSER.parseFrom(data, extensionRegistry); 398 | } 399 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom(byte[] data) 400 | throws com.google.protobuf.InvalidProtocolBufferException { 401 | return PARSER.parseFrom(data); 402 | } 403 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 404 | byte[] data, 405 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 406 | throws com.google.protobuf.InvalidProtocolBufferException { 407 | return PARSER.parseFrom(data, extensionRegistry); 408 | } 409 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom(java.io.InputStream input) 410 | throws java.io.IOException { 411 | return com.google.protobuf.GeneratedMessageV3 412 | .parseWithIOException(PARSER, input); 413 | } 414 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 415 | java.io.InputStream input, 416 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 417 | throws java.io.IOException { 418 | return com.google.protobuf.GeneratedMessageV3 419 | .parseWithIOException(PARSER, input, extensionRegistry); 420 | } 421 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseDelimitedFrom(java.io.InputStream input) 422 | throws java.io.IOException { 423 | return com.google.protobuf.GeneratedMessageV3 424 | .parseDelimitedWithIOException(PARSER, input); 425 | } 426 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseDelimitedFrom( 427 | java.io.InputStream input, 428 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 429 | throws java.io.IOException { 430 | return com.google.protobuf.GeneratedMessageV3 431 | .parseDelimitedWithIOException(PARSER, input, extensionRegistry); 432 | } 433 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 434 | com.google.protobuf.CodedInputStream input) 435 | throws java.io.IOException { 436 | return com.google.protobuf.GeneratedMessageV3 437 | .parseWithIOException(PARSER, input); 438 | } 439 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData parseFrom( 440 | com.google.protobuf.CodedInputStream input, 441 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 442 | throws java.io.IOException { 443 | return com.google.protobuf.GeneratedMessageV3 444 | .parseWithIOException(PARSER, input, extensionRegistry); 445 | } 446 | 447 | @java.lang.Override 448 | public Builder newBuilderForType() { return newBuilder(); } 449 | public static Builder newBuilder() { 450 | return DEFAULT_INSTANCE.toBuilder(); 451 | } 452 | public static Builder newBuilder(PlayerSubmissionDataOuterClass.PlayerSubmissionData prototype) { 453 | return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); 454 | } 455 | @java.lang.Override 456 | public Builder toBuilder() { 457 | return this == DEFAULT_INSTANCE 458 | ? new Builder() : new Builder().mergeFrom(this); 459 | } 460 | 461 | @java.lang.Override 462 | protected Builder newBuilderForType( 463 | com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { 464 | Builder builder = new Builder(parent); 465 | return builder; 466 | } 467 | /** 468 | * Protobuf type {@code PlayerSubmissionData} 469 | */ 470 | public static final class Builder extends 471 | com.google.protobuf.GeneratedMessageV3.Builder implements 472 | // @@protoc_insertion_point(builder_implements:PlayerSubmissionData) 473 | PlayerSubmissionDataOuterClass.PlayerSubmissionDataOrBuilder { 474 | public static final com.google.protobuf.Descriptors.Descriptor 475 | getDescriptor() { 476 | return PlayerSubmissionDataOuterClass.internal_static_PlayerSubmissionData_descriptor; 477 | } 478 | 479 | @java.lang.Override 480 | protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable 481 | internalGetFieldAccessorTable() { 482 | return PlayerSubmissionDataOuterClass.internal_static_PlayerSubmissionData_fieldAccessorTable 483 | .ensureFieldAccessorsInitialized( 484 | PlayerSubmissionDataOuterClass.PlayerSubmissionData.class, PlayerSubmissionDataOuterClass.PlayerSubmissionData.Builder.class); 485 | } 486 | 487 | // Construct using PlayerSubmissionDataOuterClass.PlayerSubmissionData.newBuilder() 488 | private Builder() { 489 | maybeForceBuilderInitialization(); 490 | } 491 | 492 | private Builder( 493 | com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { 494 | super(parent); 495 | maybeForceBuilderInitialization(); 496 | } 497 | private void maybeForceBuilderInitialization() { 498 | if (com.google.protobuf.GeneratedMessageV3 499 | .alwaysUseFieldBuilders) { 500 | } 501 | } 502 | @java.lang.Override 503 | public Builder clear() { 504 | super.clear(); 505 | score_ = 0; 506 | 507 | name_ = ""; 508 | 509 | words_ = com.google.protobuf.LazyStringArrayList.EMPTY; 510 | bitField0_ = (bitField0_ & ~0x00000001); 511 | checksum_ = com.google.protobuf.ByteString.EMPTY; 512 | 513 | return this; 514 | } 515 | 516 | @java.lang.Override 517 | public com.google.protobuf.Descriptors.Descriptor 518 | getDescriptorForType() { 519 | return PlayerSubmissionDataOuterClass.internal_static_PlayerSubmissionData_descriptor; 520 | } 521 | 522 | @java.lang.Override 523 | public PlayerSubmissionDataOuterClass.PlayerSubmissionData getDefaultInstanceForType() { 524 | return PlayerSubmissionDataOuterClass.PlayerSubmissionData.getDefaultInstance(); 525 | } 526 | 527 | @java.lang.Override 528 | public PlayerSubmissionDataOuterClass.PlayerSubmissionData build() { 529 | PlayerSubmissionDataOuterClass.PlayerSubmissionData result = buildPartial(); 530 | if (!result.isInitialized()) { 531 | throw newUninitializedMessageException(result); 532 | } 533 | return result; 534 | } 535 | 536 | @java.lang.Override 537 | public PlayerSubmissionDataOuterClass.PlayerSubmissionData buildPartial() { 538 | PlayerSubmissionDataOuterClass.PlayerSubmissionData result = new PlayerSubmissionDataOuterClass.PlayerSubmissionData(this); 539 | int from_bitField0_ = bitField0_; 540 | result.score_ = score_; 541 | result.name_ = name_; 542 | if (((bitField0_ & 0x00000001) != 0)) { 543 | words_ = words_.getUnmodifiableView(); 544 | bitField0_ = (bitField0_ & ~0x00000001); 545 | } 546 | result.words_ = words_; 547 | result.checksum_ = checksum_; 548 | onBuilt(); 549 | return result; 550 | } 551 | 552 | @java.lang.Override 553 | public Builder clone() { 554 | return super.clone(); 555 | } 556 | @java.lang.Override 557 | public Builder setField( 558 | com.google.protobuf.Descriptors.FieldDescriptor field, 559 | java.lang.Object value) { 560 | return super.setField(field, value); 561 | } 562 | @java.lang.Override 563 | public Builder clearField( 564 | com.google.protobuf.Descriptors.FieldDescriptor field) { 565 | return super.clearField(field); 566 | } 567 | @java.lang.Override 568 | public Builder clearOneof( 569 | com.google.protobuf.Descriptors.OneofDescriptor oneof) { 570 | return super.clearOneof(oneof); 571 | } 572 | @java.lang.Override 573 | public Builder setRepeatedField( 574 | com.google.protobuf.Descriptors.FieldDescriptor field, 575 | int index, java.lang.Object value) { 576 | return super.setRepeatedField(field, index, value); 577 | } 578 | @java.lang.Override 579 | public Builder addRepeatedField( 580 | com.google.protobuf.Descriptors.FieldDescriptor field, 581 | java.lang.Object value) { 582 | return super.addRepeatedField(field, value); 583 | } 584 | @java.lang.Override 585 | public Builder mergeFrom(com.google.protobuf.Message other) { 586 | if (other instanceof PlayerSubmissionDataOuterClass.PlayerSubmissionData) { 587 | return mergeFrom((PlayerSubmissionDataOuterClass.PlayerSubmissionData)other); 588 | } else { 589 | super.mergeFrom(other); 590 | return this; 591 | } 592 | } 593 | 594 | public Builder mergeFrom(PlayerSubmissionDataOuterClass.PlayerSubmissionData other) { 595 | if (other == PlayerSubmissionDataOuterClass.PlayerSubmissionData.getDefaultInstance()) return this; 596 | if (other.getScore() != 0) { 597 | setScore(other.getScore()); 598 | } 599 | if (!other.getName().isEmpty()) { 600 | name_ = other.name_; 601 | onChanged(); 602 | } 603 | if (!other.words_.isEmpty()) { 604 | if (words_.isEmpty()) { 605 | words_ = other.words_; 606 | bitField0_ = (bitField0_ & ~0x00000001); 607 | } else { 608 | ensureWordsIsMutable(); 609 | words_.addAll(other.words_); 610 | } 611 | onChanged(); 612 | } 613 | if (other.getChecksum() != com.google.protobuf.ByteString.EMPTY) { 614 | setChecksum(other.getChecksum()); 615 | } 616 | this.mergeUnknownFields(other.unknownFields); 617 | onChanged(); 618 | return this; 619 | } 620 | 621 | @java.lang.Override 622 | public final boolean isInitialized() { 623 | return true; 624 | } 625 | 626 | @java.lang.Override 627 | public Builder mergeFrom( 628 | com.google.protobuf.CodedInputStream input, 629 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 630 | throws java.io.IOException { 631 | PlayerSubmissionDataOuterClass.PlayerSubmissionData parsedMessage = null; 632 | try { 633 | parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); 634 | } catch (com.google.protobuf.InvalidProtocolBufferException e) { 635 | parsedMessage = (PlayerSubmissionDataOuterClass.PlayerSubmissionData) e.getUnfinishedMessage(); 636 | throw e.unwrapIOException(); 637 | } finally { 638 | if (parsedMessage != null) { 639 | mergeFrom(parsedMessage); 640 | } 641 | } 642 | return this; 643 | } 644 | private int bitField0_; 645 | 646 | private int score_ ; 647 | /** 648 | * int32 score = 1; 649 | * @return The score. 650 | */ 651 | @java.lang.Override 652 | public int getScore() { 653 | return score_; 654 | } 655 | /** 656 | * int32 score = 1; 657 | * @param value The score to set. 658 | * @return This builder for chaining. 659 | */ 660 | public Builder setScore(int value) { 661 | 662 | score_ = value; 663 | onChanged(); 664 | return this; 665 | } 666 | /** 667 | * int32 score = 1; 668 | * @return This builder for chaining. 669 | */ 670 | public Builder clearScore() { 671 | 672 | score_ = 0; 673 | onChanged(); 674 | return this; 675 | } 676 | 677 | private java.lang.Object name_ = ""; 678 | /** 679 | * string name = 2; 680 | * @return The name. 681 | */ 682 | public java.lang.String getName() { 683 | java.lang.Object ref = name_; 684 | if (!(ref instanceof java.lang.String)) { 685 | com.google.protobuf.ByteString bs = 686 | (com.google.protobuf.ByteString) ref; 687 | java.lang.String s = bs.toStringUtf8(); 688 | name_ = s; 689 | return s; 690 | } else { 691 | return (java.lang.String) ref; 692 | } 693 | } 694 | /** 695 | * string name = 2; 696 | * @return The bytes for name. 697 | */ 698 | public com.google.protobuf.ByteString 699 | getNameBytes() { 700 | java.lang.Object ref = name_; 701 | if (ref instanceof String) { 702 | com.google.protobuf.ByteString b = 703 | com.google.protobuf.ByteString.copyFromUtf8( 704 | (java.lang.String) ref); 705 | name_ = b; 706 | return b; 707 | } else { 708 | return (com.google.protobuf.ByteString) ref; 709 | } 710 | } 711 | /** 712 | * string name = 2; 713 | * @param value The name to set. 714 | * @return This builder for chaining. 715 | */ 716 | public Builder setName( 717 | java.lang.String value) { 718 | if (value == null) { 719 | throw new NullPointerException(); 720 | } 721 | 722 | name_ = value; 723 | onChanged(); 724 | return this; 725 | } 726 | /** 727 | * string name = 2; 728 | * @return This builder for chaining. 729 | */ 730 | public Builder clearName() { 731 | 732 | name_ = getDefaultInstance().getName(); 733 | onChanged(); 734 | return this; 735 | } 736 | /** 737 | * string name = 2; 738 | * @param value The bytes for name to set. 739 | * @return This builder for chaining. 740 | */ 741 | public Builder setNameBytes( 742 | com.google.protobuf.ByteString value) { 743 | if (value == null) { 744 | throw new NullPointerException(); 745 | } 746 | checkByteStringIsUtf8(value); 747 | 748 | name_ = value; 749 | onChanged(); 750 | return this; 751 | } 752 | 753 | private com.google.protobuf.LazyStringList words_ = com.google.protobuf.LazyStringArrayList.EMPTY; 754 | private void ensureWordsIsMutable() { 755 | if (!((bitField0_ & 0x00000001) != 0)) { 756 | words_ = new com.google.protobuf.LazyStringArrayList(words_); 757 | bitField0_ |= 0x00000001; 758 | } 759 | } 760 | /** 761 | * repeated string words = 3; 762 | * @return A list containing the words. 763 | */ 764 | public com.google.protobuf.ProtocolStringList 765 | getWordsList() { 766 | return words_.getUnmodifiableView(); 767 | } 768 | /** 769 | * repeated string words = 3; 770 | * @return The count of words. 771 | */ 772 | public int getWordsCount() { 773 | return words_.size(); 774 | } 775 | /** 776 | * repeated string words = 3; 777 | * @param index The index of the element to return. 778 | * @return The words at the given index. 779 | */ 780 | public java.lang.String getWords(int index) { 781 | return words_.get(index); 782 | } 783 | /** 784 | * repeated string words = 3; 785 | * @param index The index of the value to return. 786 | * @return The bytes of the words at the given index. 787 | */ 788 | public com.google.protobuf.ByteString 789 | getWordsBytes(int index) { 790 | return words_.getByteString(index); 791 | } 792 | /** 793 | * repeated string words = 3; 794 | * @param index The index to set the value at. 795 | * @param value The words to set. 796 | * @return This builder for chaining. 797 | */ 798 | public Builder setWords( 799 | int index, java.lang.String value) { 800 | if (value == null) { 801 | throw new NullPointerException(); 802 | } 803 | ensureWordsIsMutable(); 804 | words_.set(index, value); 805 | onChanged(); 806 | return this; 807 | } 808 | /** 809 | * repeated string words = 3; 810 | * @param value The words to add. 811 | * @return This builder for chaining. 812 | */ 813 | public Builder addWords( 814 | java.lang.String value) { 815 | if (value == null) { 816 | throw new NullPointerException(); 817 | } 818 | ensureWordsIsMutable(); 819 | words_.add(value); 820 | onChanged(); 821 | return this; 822 | } 823 | /** 824 | * repeated string words = 3; 825 | * @param values The words to add. 826 | * @return This builder for chaining. 827 | */ 828 | public Builder addAllWords( 829 | java.lang.Iterable values) { 830 | ensureWordsIsMutable(); 831 | com.google.protobuf.AbstractMessageLite.Builder.addAll( 832 | values, words_); 833 | onChanged(); 834 | return this; 835 | } 836 | /** 837 | * repeated string words = 3; 838 | * @return This builder for chaining. 839 | */ 840 | public Builder clearWords() { 841 | words_ = com.google.protobuf.LazyStringArrayList.EMPTY; 842 | bitField0_ = (bitField0_ & ~0x00000001); 843 | onChanged(); 844 | return this; 845 | } 846 | /** 847 | * repeated string words = 3; 848 | * @param value The bytes of the words to add. 849 | * @return This builder for chaining. 850 | */ 851 | public Builder addWordsBytes( 852 | com.google.protobuf.ByteString value) { 853 | if (value == null) { 854 | throw new NullPointerException(); 855 | } 856 | checkByteStringIsUtf8(value); 857 | ensureWordsIsMutable(); 858 | words_.add(value); 859 | onChanged(); 860 | return this; 861 | } 862 | 863 | private com.google.protobuf.ByteString checksum_ = com.google.protobuf.ByteString.EMPTY; 864 | /** 865 | * bytes checksum = 4; 866 | * @return The checksum. 867 | */ 868 | @java.lang.Override 869 | public com.google.protobuf.ByteString getChecksum() { 870 | return checksum_; 871 | } 872 | /** 873 | * bytes checksum = 4; 874 | * @param value The checksum to set. 875 | * @return This builder for chaining. 876 | */ 877 | public Builder setChecksum(com.google.protobuf.ByteString value) { 878 | if (value == null) { 879 | throw new NullPointerException(); 880 | } 881 | 882 | checksum_ = value; 883 | onChanged(); 884 | return this; 885 | } 886 | /** 887 | * bytes checksum = 4; 888 | * @return This builder for chaining. 889 | */ 890 | public Builder clearChecksum() { 891 | 892 | checksum_ = getDefaultInstance().getChecksum(); 893 | onChanged(); 894 | return this; 895 | } 896 | @java.lang.Override 897 | public final Builder setUnknownFields( 898 | final com.google.protobuf.UnknownFieldSet unknownFields) { 899 | return super.setUnknownFields(unknownFields); 900 | } 901 | 902 | @java.lang.Override 903 | public final Builder mergeUnknownFields( 904 | final com.google.protobuf.UnknownFieldSet unknownFields) { 905 | return super.mergeUnknownFields(unknownFields); 906 | } 907 | 908 | 909 | // @@protoc_insertion_point(builder_scope:PlayerSubmissionData) 910 | } 911 | 912 | // @@protoc_insertion_point(class_scope:PlayerSubmissionData) 913 | private static final PlayerSubmissionDataOuterClass.PlayerSubmissionData DEFAULT_INSTANCE; 914 | static { 915 | DEFAULT_INSTANCE = new PlayerSubmissionDataOuterClass.PlayerSubmissionData(); 916 | } 917 | 918 | public static PlayerSubmissionDataOuterClass.PlayerSubmissionData getDefaultInstance() { 919 | return DEFAULT_INSTANCE; 920 | } 921 | 922 | private static final com.google.protobuf.Parser 923 | PARSER = new com.google.protobuf.AbstractParser() { 924 | @java.lang.Override 925 | public PlayerSubmissionData parsePartialFrom( 926 | com.google.protobuf.CodedInputStream input, 927 | com.google.protobuf.ExtensionRegistryLite extensionRegistry) 928 | throws com.google.protobuf.InvalidProtocolBufferException { 929 | return new PlayerSubmissionData(input, extensionRegistry); 930 | } 931 | }; 932 | 933 | public static com.google.protobuf.Parser parser() { 934 | return PARSER; 935 | } 936 | 937 | @java.lang.Override 938 | public com.google.protobuf.Parser getParserForType() { 939 | return PARSER; 940 | } 941 | 942 | @java.lang.Override 943 | public PlayerSubmissionDataOuterClass.PlayerSubmissionData getDefaultInstanceForType() { 944 | return DEFAULT_INSTANCE; 945 | } 946 | 947 | } 948 | 949 | private static final com.google.protobuf.Descriptors.Descriptor 950 | internal_static_PlayerSubmissionData_descriptor; 951 | private static final 952 | com.google.protobuf.GeneratedMessageV3.FieldAccessorTable 953 | internal_static_PlayerSubmissionData_fieldAccessorTable; 954 | 955 | public static com.google.protobuf.Descriptors.FileDescriptor 956 | getDescriptor() { 957 | return descriptor; 958 | } 959 | private static com.google.protobuf.Descriptors.FileDescriptor 960 | descriptor; 961 | static { 962 | java.lang.String[] descriptorData = { 963 | "\n\032PlayerSubmissionData.proto\"T\n\024PlayerSu" + 964 | "bmissionData\022\r\n\005score\030\001 \001(\005\022\014\n\004name\030\002 \001(" + 965 | "\t\022\r\n\005words\030\003 \003(\t\022\020\n\010checksum\030\004 \001(\014b\006prot" + 966 | "o3" 967 | }; 968 | descriptor = com.google.protobuf.Descriptors.FileDescriptor 969 | .internalBuildGeneratedFileFrom(descriptorData, 970 | new com.google.protobuf.Descriptors.FileDescriptor[] { 971 | }); 972 | internal_static_PlayerSubmissionData_descriptor = 973 | getDescriptor().getMessageTypes().get(0); 974 | internal_static_PlayerSubmissionData_fieldAccessorTable = new 975 | com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( 976 | internal_static_PlayerSubmissionData_descriptor, 977 | new java.lang.String[] { "Score", "Name", "Words", "Checksum", }); 978 | } 979 | 980 | // @@protoc_insertion_point(outer_class_scope) 981 | } 982 | -------------------------------------------------------------------------------- /wordtris/src/GameLoop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect, useReducer, useState } from "react"; 3 | import "./App.css"; 4 | import { createMachine, interpret } from "xstate"; 5 | import { PlayerBlock } from "./components/PlayerBlock"; 6 | import { BoardCells } from "./components/BoardCells"; 7 | import { 8 | convertCellsToAdjusted, 9 | doGradualFall, 10 | dropFloatingCells, 11 | generateUserCells, 12 | getAdjustedBottomR, 13 | getAdjustedLeftmostC, 14 | getAdjustedRightmostC, 15 | getAdjustedTopR, 16 | getAdjustedUserCell, 17 | isInCBounds, 18 | isInRBounds, 19 | isPlayerTouchingGround, 20 | layout, 21 | rotateCells, 22 | spawnPos, 23 | } from "./util/playerUtil"; 24 | import { createBoard, getGroundHeight } from "./util/boardUtil"; 25 | import { BoardCell } from "./util/BoardCell"; 26 | import { useInterval } from "./util/useInterval"; 27 | import { GameOverOverlay, PlayAgainButton } from "./components/GameOverOverlay"; 28 | import { CountdownOverlay } from "./components/CountdownOverlay"; 29 | import { FallingBlock } from "./components/FallingBlock"; 30 | import { 31 | _ENABLE_UP_KEY, 32 | _IS_PRINTING_STATE, 33 | BOARD_CELL_COLOR, 34 | BOARD_COLOR, 35 | BOARD_COLS, 36 | BOARD_ROWS, 37 | boardCellFallDurationMillisecondsRate, 38 | CELL_SIZE, 39 | countdownTotalSecs, 40 | EMPTY, 41 | EMPTY_CELL_COLOR, 42 | ENABLE_INSTANT_DROP, 43 | ENABLE_SMOOTH_FALL, 44 | frameStep, 45 | groundExitPenaltyRate, 46 | interp, 47 | interpKeydownMult, 48 | interpMax, 49 | interpRate, 50 | LARGE_TEXT_SIZE, 51 | lockMax, 52 | matchAnimLength, 53 | MIN_WORD_LENGTH, 54 | PLAYER_COLOR, 55 | playerCellFallDurationMillisecondsRate, 56 | UNIVERSAL_BORDER_RADIUS, 57 | } from "./setup"; 58 | import { UserCell } from "./UserCell"; 59 | import { Header } from "./components/Header"; 60 | import { Prompt } from "./components/Prompt"; 61 | import { getLeaders } from "./util/webUtil"; 62 | import { GameSidePanel } from "./components/GameSidePanel"; 63 | import { PersonalHighScore } from "./components/PersonalHighScore"; 64 | 65 | // Terminology: https://tetris.fandom.com/wiki/Glossary 66 | // Declaration of game states. 67 | const stateMachine = createMachine({ 68 | initial: "startingGame", 69 | states: { 70 | startingGame: { on: { START: "countdown" } }, 71 | countdown: { on: { DONE: "spawningBlock" } }, 72 | spawningBlock: { on: { SPAWN: "placingBlock" } }, 73 | placingBlock: { 74 | on: { 75 | TOUCHING_BLOCK: "lockDelay", 76 | BLOCKED: "gameOver", 77 | DO_INSTANT_DROP_ANIM: "playerInstantDropAnim", 78 | }, 79 | }, 80 | playerInstantDropAnim: { 81 | on: { TOUCHING_BLOCK: "lockDelay" }, 82 | }, 83 | lockDelay: { on: { LOCK: "fallingLetters", UNLOCK: "placingBlock" } }, 84 | fallingLetters: { on: { DO_ANIM: "fallingLettersAnim" } }, 85 | fallingLettersAnim: { on: { GROUNDED: "checkingMatches" } }, 86 | checkingMatches: { 87 | on: { 88 | PLAYING_ANIM: "playMatchAnimation", 89 | SKIP_ANIM: "postMatchAnimation", 90 | }, 91 | }, 92 | playMatchAnimation: { 93 | on: { 94 | CHECK_FOR_CHAIN: "fallingLetters", 95 | SKIP_ANIM: "postMatchAnimation", 96 | }, 97 | }, 98 | postMatchAnimation: { 99 | on: { DONE: "spawningBlock" }, 100 | }, 101 | gameOver: { on: { RESTART: "startingGame" } }, 102 | }, 103 | predictableActionArguments: true, 104 | }); 105 | 106 | // Handle states. 107 | const stateHandler = interpret(stateMachine).onTransition((state) => { 108 | if (_IS_PRINTING_STATE) console.log(" STATE:", state.value); 109 | }); 110 | stateHandler.start(); 111 | 112 | const timestamps = { 113 | matchAnimStart: 0, 114 | lockStart: 0, 115 | countdownStartTime: 0, 116 | accumFrameTime: 0, 117 | prevFrameTime: performance.now(), 118 | countdownMillisecondsElapsed: 0, 119 | fallingLettersAnimStartMilliseconds: 0, 120 | fallingLettersAnimDurationMilliseconds: 0, 121 | playerInstantDropAnimStart: 0, 122 | playerInstantDropAnimDurationMilliseconds: 0, 123 | }; 124 | 125 | type PlayerState = { 126 | pos: [number, number]; 127 | cells: UserCell[]; 128 | adjustedCells: UserCell[]; 129 | }; 130 | 131 | type PlayerAction = 132 | | { type: "resetPlayer" } 133 | | { type: "setCells"; newCells: UserCell[]; newAdjustedCells: UserCell[] } 134 | | { type: "movePlayer"; posUpdate: [number, number] } 135 | | { type: "groundPlayer"; playerRowPos: number }; 136 | 137 | export function GameLoop() { 138 | const [player, dispatchPlayer] = useReducer( 139 | (state: PlayerState, action: PlayerAction): PlayerState => { 140 | let newPos; 141 | switch (action.type) { 142 | case "resetPlayer": { 143 | newPos = [...spawnPos] as const; 144 | const initCells = generateUserCells(); 145 | return { 146 | ...state, 147 | pos: newPos.slice() as [number, number], 148 | cells: initCells, 149 | adjustedCells: convertCellsToAdjusted( 150 | initCells, 151 | newPos, 152 | ), 153 | }; 154 | } 155 | case "setCells": { 156 | return { 157 | ...state, 158 | cells: action.newCells, 159 | adjustedCells: action.newAdjustedCells, 160 | }; 161 | } 162 | case "movePlayer": { 163 | newPos = [ 164 | state.pos[0] + action.posUpdate[0], 165 | state.pos[1] + action.posUpdate[1], 166 | ] as [number, number]; 167 | return { 168 | ...state, 169 | pos: newPos, 170 | adjustedCells: convertCellsToAdjusted( 171 | state.cells, 172 | newPos, 173 | ), 174 | }; 175 | } 176 | case "groundPlayer": { 177 | newPos = [action.playerRowPos, state.pos[1]] as [ 178 | number, 179 | number, 180 | ]; 181 | return { 182 | ...state, 183 | pos: newPos, 184 | adjustedCells: convertCellsToAdjusted( 185 | state.cells, 186 | newPos, 187 | ), 188 | }; 189 | } 190 | } 191 | }, 192 | { 193 | pos: [...spawnPos], 194 | cells: [], 195 | adjustedCells: [], 196 | }, 197 | ); 198 | 199 | const [validWords, setValidWords] = useState(new Set()); 200 | 201 | const [leaders, setLeaders] = React.useState( 202 | [] as Array<{ name: string; score: number }>, 203 | ); 204 | 205 | useEffect(() => { 206 | dispatchPlayer({ type: "resetPlayer" }); 207 | // Fetch validWords during countdown. 208 | fetch( 209 | "https://raw.githubusercontent.com/khivy/wordtris/main/wordtris/lexicons/Scrabble80K.txt", 210 | ) 211 | .then((res) => res.text()) 212 | .then((res) => res.split("\n")) 213 | .then((data) => setValidWords(new Set(data))); 214 | 215 | updateLeaders(); 216 | }, []); 217 | 218 | function updateLeaders() { 219 | getLeaders() 220 | .then((response) => response.json()) 221 | .then((data) => { 222 | setLeaders(data); 223 | }); 224 | } 225 | 226 | const [boardCellMatrix, setBoardCellMatrix] = useState( 227 | createBoard(BOARD_ROWS, BOARD_COLS), 228 | ); 229 | 230 | const [isPlayerVisible, setPlayerVisibility] = useState(false); 231 | const [isPlayerMovementEnabled, setIsPlayerMovementEnabled] = useState( 232 | false, 233 | ); 234 | 235 | /* Block cell coordinates that were placed/dropped.. */ 236 | const [placedCells, setPlacedCells] = useState( 237 | new Set() as Set<[number, number]>, 238 | ); 239 | 240 | // Variables for valid word matches. 241 | const [matchedWords, setMatchedWords] = useState([] as string[]); 242 | /* matchedCells stores string coordinates, rather than [number, number], 243 | to allow for `.has()` to find equivalent coordinates. */ 244 | const [matchedCells, setMatchedCells] = useState(new Set() as Set); 245 | 246 | // Variables for `` 247 | const [isCountdownVisible, setCountdownVisibility] = useState(false); 248 | const [countdownSec, setcountdownSec] = useState(0); 249 | 250 | // Variable(s) to prevent infinite stalling. 251 | const [isGameOverVisible, setGameOverVisibility] = useState(false); 252 | const [groundExitPenalty, setGroundExitPenalty] = useState(0); 253 | 254 | const [didInstantDrop, setDidInstantDrop] = useState(false); 255 | const [localHighScore, setLocalHighScore] = useState(0); 256 | 257 | const [ 258 | fallingBoardLettersBeforeAndAfter, 259 | setFallingBoardLettersBeforeAndAfter, 260 | ] = useState([]); 261 | const [ 262 | fallingPlayerLettersBeforeAndAfter, 263 | setFallingPlayerLettersBeforeAndAfter, 264 | ] = useState([]); 265 | 266 | useEffect(() => { 267 | globalThis.addEventListener("keydown", handleKeydown); 268 | return () => { 269 | globalThis.removeEventListener("keydown", handleKeydown); 270 | }; 271 | }); 272 | 273 | useInterval(() => { 274 | loop(); 275 | }, 10); 276 | 277 | function rotatePlayerBlock(isClockwise: boolean, board: BoardCell[][]) { 278 | const rotatedCells = rotateCells( 279 | player.cells, 280 | isClockwise, 281 | ); 282 | 283 | let rotatedCellsAdjusted = rotatedCells.map((cell) => 284 | getAdjustedUserCell(cell, player.pos) 285 | ); 286 | 287 | // Get the overlapping cell's respective index in non-adjusted array. 288 | const overlappingCellIndex = rotatedCellsAdjusted.findIndex((cell) => ( 289 | !isInCBounds(cell.c) || 290 | !isInRBounds(cell.r) || 291 | board[cell.r][cell.c].char !== EMPTY 292 | )); 293 | // If there's no overlap, place it. Otherwise, shift it in the opposite direction of the overlapping cell. 294 | if (overlappingCellIndex === -1) { 295 | // If rotation puts a block right underneath a placed block, set interp to 0. 296 | const isAdjacentToGround = rotatedCellsAdjusted.some((cell) => { 297 | return !isInRBounds(cell.r + 1) || 298 | board[cell.r + 1][cell.c].char !== EMPTY; 299 | }); 300 | if (isAdjacentToGround) { 301 | interp.val = 0; 302 | } 303 | dispatchPlayer({ 304 | type: "setCells", 305 | newCells: rotatedCells, 306 | newAdjustedCells: rotatedCellsAdjusted, 307 | }); 308 | } else { 309 | console.assert(player.adjustedCells.length === 2); 310 | // Get direction of overlapping cell. 311 | const dr = Math.floor(layout.length / 2) - 312 | rotatedCells[overlappingCellIndex].r; 313 | const dc = Math.floor(layout[0].length / 2) - 314 | rotatedCells[overlappingCellIndex].c; 315 | // Shift it. 316 | for (const cell of rotatedCells) { 317 | cell.r += dr; 318 | cell.c += dc; 319 | } 320 | rotatedCellsAdjusted = rotatedCells.map((cell) => 321 | getAdjustedUserCell(cell, player.pos) 322 | ); 323 | // Check for overlaps with shifted cells. 324 | const isOverlapping = rotatedCellsAdjusted.some((cell) => 325 | !isInCBounds(cell.c) || 326 | !isInRBounds(cell.r) || 327 | board[cell.r][cell.c].char !== EMPTY 328 | ); 329 | if (!isOverlapping) { 330 | dispatchPlayer({ 331 | type: "setCells", 332 | newCells: rotatedCells, 333 | newAdjustedCells: rotatedCellsAdjusted, 334 | }); 335 | } 336 | } 337 | } 338 | 339 | function handleKeydown( 340 | { code }: { code: string }, 341 | ): void { 342 | if (!isPlayerMovementEnabled) { 343 | return; 344 | } 345 | const board = boardCellMatrix; 346 | const areTargetSpacesEmpty = ( 347 | dr: -1 | 0 | 1 | number, 348 | dc: -1 | 0 | 1, 349 | ) => player.adjustedCells.every((cell) => { 350 | return board[cell.r + dr][cell.c + dc].char === EMPTY; 351 | }); 352 | if ("ArrowLeft" === code) { 353 | // Move left. 354 | if ( 355 | isInCBounds( 356 | getAdjustedLeftmostC(player.adjustedCells) - 1, 357 | ) && 358 | // Ensure blocks don't cross over to ground higher than it, regarding interpolation. 359 | (!ENABLE_SMOOTH_FALL || 360 | isInRBounds( 361 | getAdjustedBottomR(player.adjustedCells) + 362 | Math.ceil(interp.val / interpMax), 363 | )) && 364 | areTargetSpacesEmpty( 365 | Math.ceil(ENABLE_SMOOTH_FALL ? interp.val / interpMax : 0), 366 | -1, 367 | ) 368 | ) { 369 | dispatchPlayer({ type: "movePlayer", posUpdate: [0, -1] }); 370 | } 371 | } else if ("ArrowRight" === code) { 372 | // Move right. 373 | if ( 374 | isInCBounds( 375 | getAdjustedRightmostC(player.adjustedCells) + 1, 376 | ) && 377 | // Ensure blocks don't cross over to ground higher than it, regarding interpolation. 378 | (!ENABLE_SMOOTH_FALL || 379 | isInRBounds( 380 | getAdjustedBottomR(player.adjustedCells) + 381 | Math.ceil(interp.val / interpMax), 382 | )) && 383 | areTargetSpacesEmpty( 384 | Math.ceil(ENABLE_SMOOTH_FALL ? interp.val / interpMax : 0), 385 | 1, 386 | ) 387 | ) { 388 | dispatchPlayer({ type: "movePlayer", posUpdate: [0, 1] }); 389 | } 390 | } else if ("ArrowDown" === code) { 391 | // Move down faster. 392 | if ( 393 | getAdjustedBottomR(player.adjustedCells) + 1 < BOARD_ROWS && 394 | areTargetSpacesEmpty(1, 0) 395 | ) { 396 | if (ENABLE_SMOOTH_FALL) { 397 | interp.val += interpRate * interpKeydownMult; 398 | } else { 399 | dispatchPlayer({ type: "movePlayer", posUpdate: [1, 0] }); 400 | // Reset interp. 401 | interp.val = 0; 402 | } 403 | } 404 | } else if ("KeyZ" === code) { 405 | // Rotate left. 406 | rotatePlayerBlock(false, board); 407 | } else if ("ArrowUp" === code || "KeyX" === code) { 408 | // Rotate right. 409 | rotatePlayerBlock(true, board); 410 | } else if ("Space" === code) { 411 | // Instant drop. 412 | if (ENABLE_INSTANT_DROP) { 413 | setDidInstantDrop(true); 414 | } else if ( 415 | _ENABLE_UP_KEY && 416 | 0 <= getAdjustedTopR(player.adjustedCells) - 1 && 417 | areTargetSpacesEmpty(-1, 0) 418 | ) { 419 | dispatchPlayer({ type: "movePlayer", posUpdate: [-1, 0] }); 420 | } 421 | } 422 | } 423 | 424 | const loop = () => { 425 | const curTime = performance.now(); 426 | timestamps.accumFrameTime += curTime - timestamps.prevFrameTime; 427 | timestamps.prevFrameTime = curTime; 428 | 429 | // Update physics. 430 | while (timestamps.accumFrameTime >= frameStep) { 431 | timestamps.accumFrameTime -= frameStep; 432 | handleStates(); 433 | } 434 | }; 435 | 436 | function findWords(arr: BoardCell[], reversed: boolean): number[] { 437 | // Given the array of a row or column, returns the left and right indices (inclusive) of the longest word. 438 | const contents = reversed 439 | ? arr.map((cell) => cell.char === EMPTY ? "-" : cell.char).reverse() 440 | .join("") 441 | : arr.map((cell) => cell.char === EMPTY ? "-" : cell.char).join(""); 442 | // Look for words in row 443 | let resLeft = -1; 444 | let resRight = -1; 445 | for (let left = 0; left < contents.length; ++left) { 446 | for ( 447 | let right = left + MIN_WORD_LENGTH - 1; 448 | right < contents.length; 449 | ++right 450 | ) { 451 | const cand = contents.slice(left, right + 1); 452 | if (validWords.has(cand)) { 453 | if (right - left > resRight - resLeft) { 454 | resRight = right; 455 | resLeft = left; 456 | } 457 | } 458 | } 459 | } 460 | return reversed 461 | ? [ 462 | contents.length - resRight - 1, 463 | resRight - (resLeft) + (contents.length - resRight - 1), 464 | ] 465 | : [resLeft, resRight]; 466 | } 467 | 468 | function handleStates() { 469 | if ("startingGame" === stateHandler.state.value) { 470 | // Takes care of multiple enqueued state changes. 471 | setBoardCellMatrix(createBoard(BOARD_ROWS, BOARD_COLS)); 472 | 473 | // Reset Word List. 474 | setMatchedWords([]); 475 | setMatchedCells(new Set()); 476 | 477 | setFallingBoardLettersBeforeAndAfter([]); 478 | setFallingPlayerLettersBeforeAndAfter([]); 479 | 480 | setGameOverVisibility(false); 481 | 482 | // Temporary fix for lingering hasMatched cells. See Github issue #55. 483 | setBoardCellMatrix((matrix) => 484 | matrix.map((row) => { 485 | return row.map((cell) => { 486 | cell.hasMatched = false; 487 | return cell; 488 | }); 489 | }) 490 | ); 491 | 492 | setCountdownVisibility(true); 493 | timestamps.countdownStartTime = performance.now(); 494 | stateHandler.send("START"); 495 | } else if ("countdown" === stateHandler.state.value) { 496 | timestamps.countdownMillisecondsElapsed = performance.now() - 497 | timestamps.countdownStartTime; 498 | const currCountdownSec = countdownTotalSecs - 499 | Math.floor(timestamps.countdownMillisecondsElapsed / 1000); 500 | if (currCountdownSec !== 0) { 501 | setcountdownSec(currCountdownSec); 502 | } else { 503 | stateHandler.send("DONE"); 504 | } 505 | } else if ("spawningBlock" === stateHandler.state.value) { 506 | // Wait while validWords fetches data. 507 | if (validWords.size === 0) { 508 | return; 509 | } 510 | // Hide countdown. 511 | setCountdownVisibility(false); 512 | 513 | // Reset player. 514 | dispatchPlayer({ type: "resetPlayer" }); 515 | setIsPlayerMovementEnabled(true); 516 | setPlayerVisibility(true); 517 | setMatchedCells(new Set()); 518 | 519 | // Reset penalty. 520 | setGroundExitPenalty(0); 521 | 522 | // Empty placedCells. 523 | setPlacedCells((prev) => { 524 | prev.clear(); 525 | return prev; 526 | }); 527 | stateHandler.send("SPAWN"); 528 | } else if ("placingBlock" === stateHandler.state.value) { 529 | // Reset if spawn point is blocked. 530 | if (boardCellMatrix[spawnPos[0]][spawnPos[1]].char !== EMPTY) { 531 | // Pause player movement. 532 | setPlayerVisibility(false); 533 | setIsPlayerMovementEnabled(false); 534 | // Signal Game Over. 535 | setGameOverVisibility(true); 536 | setLocalHighScore((prev) => 537 | prev < matchedWords.length ? matchedWords.length : prev 538 | ); 539 | stateHandler.send("BLOCKED"); 540 | } 541 | 542 | // Handle gradual fall. 543 | if (isPlayerMovementEnabled) { 544 | const dr = doGradualFall( 545 | boardCellMatrix, 546 | player.adjustedCells, 547 | ); 548 | dispatchPlayer({ type: "movePlayer", posUpdate: [dr, 0] }); 549 | } 550 | 551 | // Check if player is touching ground. 552 | if (isPlayerTouchingGround(player.adjustedCells, boardCellMatrix)) { 553 | timestamps.lockStart = performance.now(); 554 | stateHandler.send("TOUCHING_BLOCK"); 555 | } 556 | 557 | if (didInstantDrop) { 558 | setPlayerVisibility(false); 559 | const closestPlayerCellToGround = player.adjustedCells.reduce(( 560 | prev, 561 | cur, 562 | ) => getGroundHeight(prev.c, prev.r, boardCellMatrix) - prev.r < 563 | getGroundHeight(cur.c, cur.r, boardCellMatrix) - cur.r 564 | ? prev 565 | : cur 566 | ); 567 | const closestGround = getGroundHeight( 568 | closestPlayerCellToGround.c, 569 | closestPlayerCellToGround.r, 570 | boardCellMatrix, 571 | ); 572 | const minDist = closestGround - closestPlayerCellToGround.r; 573 | timestamps.playerInstantDropAnimStart = performance.now(); 574 | timestamps.playerInstantDropAnimDurationMilliseconds = 575 | playerCellFallDurationMillisecondsRate * minDist; 576 | setFallingPlayerLettersBeforeAndAfter( 577 | player.adjustedCells.map((cell) => [ 578 | { ...cell }, 579 | { ...cell, r: closestGround }, 580 | ]), 581 | ); 582 | setIsPlayerMovementEnabled(false); 583 | stateHandler.send("DO_INSTANT_DROP_ANIM"); 584 | } 585 | } else if ("playerInstantDropAnim" === stateHandler.state.value) { 586 | if ( 587 | timestamps.playerInstantDropAnimDurationMilliseconds < 588 | performance.now() - timestamps.playerInstantDropAnimStart 589 | ) { 590 | setPlayerVisibility(true); 591 | let ground_row = BOARD_ROWS; 592 | player.adjustedCells.forEach((cell) => 593 | ground_row = Math.min( 594 | ground_row, 595 | getGroundHeight(cell.c, cell.r, boardCellMatrix), 596 | ) 597 | ); 598 | const mid = Math.floor(layout.length / 2); 599 | // Offset with the lowest cell, centered around layout's midpoint. 600 | let dy = 0; 601 | player.cells.forEach((cell) => dy = Math.max(dy, cell.r - mid)); 602 | dispatchPlayer({ 603 | type: "groundPlayer", 604 | playerRowPos: ground_row - dy, 605 | }); 606 | stateHandler.send("TOUCHING_BLOCK"); 607 | } 608 | } else if ("lockDelay" === stateHandler.state.value) { 609 | const lockTime = performance.now() - timestamps.lockStart + 610 | groundExitPenalty; 611 | if ( 612 | !isPlayerTouchingGround(player.adjustedCells, boardCellMatrix) 613 | ) { 614 | // Player has moved off of ground. 615 | setGroundExitPenalty((prev) => prev + groundExitPenaltyRate); 616 | stateHandler.send("UNLOCK"); 617 | } else if (lockMax <= lockTime || didInstantDrop) { 618 | // Lock in block. 619 | setFallingPlayerLettersBeforeAndAfter([]); 620 | const newBoard = boardCellMatrix.slice(); 621 | setPlacedCells((prev) => { 622 | player.adjustedCells.forEach((cell) => { 623 | prev.add([cell.r, cell.c]); 624 | // Give player cells to board. 625 | newBoard[cell.r][cell.c].char = cell.char; 626 | }); 627 | return prev; 628 | }); 629 | setBoardCellMatrix(newBoard); 630 | interp.val = 0; 631 | setDidInstantDrop(false); 632 | 633 | // Disable player block features. 634 | setIsPlayerMovementEnabled(false); 635 | setPlayerVisibility(false); 636 | stateHandler.send("LOCK"); 637 | } 638 | } else if ("fallingLetters" === stateHandler.state.value) { 639 | // For each floating block, move it 1 + the ground. 640 | const { boardWithoutFallCells, postFallCells, preFallCells } = 641 | dropFloatingCells( 642 | boardCellMatrix, 643 | ); 644 | 645 | // Update falling letters & animation information. 646 | const newFallingBoardLettersBeforeAndAfter = preFallCells.map(( 647 | k, 648 | i, 649 | ) => [k, postFallCells[i]]); 650 | // Handle animation duration. 651 | let animDuration = 0; 652 | if (postFallCells.length !== 0) { 653 | const [maxFallBeforeCell, maxFallAfterCell] = 654 | newFallingBoardLettersBeforeAndAfter.reduce((prev, cur) => 655 | prev[1].r - prev[0].r > cur[1].r - cur[0].r ? prev : cur 656 | ); 657 | animDuration = boardCellFallDurationMillisecondsRate * 658 | (maxFallAfterCell.r - maxFallBeforeCell.r); 659 | } 660 | setFallingBoardLettersBeforeAndAfter( 661 | newFallingBoardLettersBeforeAndAfter, 662 | ); 663 | timestamps.fallingLettersAnimDurationMilliseconds = animDuration; 664 | timestamps.fallingLettersAnimStartMilliseconds = performance.now(); 665 | 666 | setBoardCellMatrix(boardWithoutFallCells); 667 | 668 | setPlacedCells((prev) => { 669 | postFallCells.forEach((boardCell) => 670 | prev.add([boardCell.r, boardCell.c]) 671 | ); 672 | return prev; 673 | }); 674 | 675 | stateHandler.send("DO_ANIM"); 676 | } else if ("fallingLettersAnim" === stateHandler.state.value) { 677 | if ( 678 | timestamps.fallingLettersAnimDurationMilliseconds < 679 | performance.now() - 680 | timestamps.fallingLettersAnimStartMilliseconds 681 | ) { 682 | // Drops floating cells again in-case 683 | const { boardWithoutFallCells, _postFallCells, _preFallCells } = 684 | dropFloatingCells( 685 | boardCellMatrix, 686 | ); 687 | setBoardCellMatrix(boardWithoutFallCells); 688 | 689 | const newBoard = boardCellMatrix.slice(); 690 | fallingBoardLettersBeforeAndAfter.forEach((beforeAndAfter) => { 691 | const [before, after] = beforeAndAfter; 692 | newBoard[before.r][before.c].char = EMPTY; 693 | newBoard[after.r][after.c].char = after.char; 694 | }); 695 | setFallingBoardLettersBeforeAndAfter([]); 696 | setBoardCellMatrix(newBoard); 697 | stateHandler.send("GROUNDED"); 698 | } 699 | } else if ("checkingMatches" === stateHandler.state.value) { 700 | // Allocate a newBoard to avoid desync between render and board (React, pls). 701 | const newBoard = boardCellMatrix.slice(); 702 | // TODO: Remove repeated checks when placedCells occupy same row or col. 703 | let hasRemovedWord = false; 704 | const affectedRows = new Set( 705 | [...placedCells].map((cell) => cell[0]), 706 | ); 707 | const affectedCols = new Set( 708 | [...placedCells].map((cell) => cell[1]), 709 | ); 710 | const newMatchedCells = [] as string[]; 711 | affectedRows.forEach((r) => { 712 | // Row words 713 | const [row_left, row_right] = findWords(newBoard[r], false); 714 | // Remove word, but ignore when a candidate isn't found. 715 | if (row_left !== -1) { 716 | matchedWords.push( 717 | newBoard[r].slice(row_left, row_right + 1).map((cell) => 718 | cell.char 719 | ).join(""), 720 | ); 721 | for (let i = row_left; i < row_right + 1; ++i) { 722 | newMatchedCells.push([r, i].toString()); 723 | } 724 | hasRemovedWord = true; 725 | } 726 | }); 727 | const newMatchedWords = [] as string[]; 728 | affectedCols.forEach((c) => { 729 | // Column words 730 | let [col_top, col_bot] = findWords( 731 | boardCellMatrix.map((row) => row[c]), 732 | false, 733 | ); 734 | const [col_topR, col_botR] = findWords( 735 | boardCellMatrix.map((row) => row[c]), 736 | true, 737 | ); 738 | // Use reversed word if longer. 739 | let isColReversed = false; 740 | if (col_botR - col_topR > col_bot - col_top) { 741 | col_top = col_topR; 742 | col_bot = col_botR; 743 | isColReversed = true; 744 | } 745 | // Remove word, but ignore when a candidate isn't found. 746 | if (col_top !== -1) { 747 | newMatchedWords.push( 748 | isColReversed 749 | ? boardCellMatrix.map((row) => row[c]) 750 | .slice(col_top, col_bot + 1).map((cell) => 751 | cell.char 752 | ).reverse().join("") 753 | : boardCellMatrix.map((row) => row[c]) 754 | .slice(col_top, col_bot + 1).map((cell) => 755 | cell.char 756 | ).join(""), 757 | ); 758 | for (let i = col_top; i < col_bot + 1; ++i) { 759 | newMatchedCells.push([i, c].toString()); 760 | } 761 | hasRemovedWord = true; 762 | } 763 | }); 764 | 765 | setMatchedWords((matchedWords) => 766 | matchedWords.concat(newMatchedWords) 767 | ); 768 | setMatchedCells((prev) => { 769 | newMatchedCells.forEach((word) => prev.add(word)); 770 | // Signal characters to remove. 771 | newBoard.forEach((row, r) => { 772 | row.forEach((cell, c) => { 773 | if (matchedCells.has([r, c].toString())) { 774 | cell.hasMatched = true; 775 | } 776 | }); 777 | }); 778 | return prev; 779 | }); 780 | 781 | setBoardCellMatrix(newBoard); 782 | 783 | if (hasRemovedWord) { 784 | timestamps.matchAnimStart = performance.now(); 785 | } 786 | stateHandler.send("PLAYING_ANIM"); 787 | } else if ("playMatchAnimation" === stateHandler.state.value) { 788 | const animTime = performance.now() - timestamps.matchAnimStart; 789 | if (matchAnimLength <= animTime) { 790 | // Also remove characters. (hasMatched) 791 | const newBoard = boardCellMatrix.slice(); 792 | newBoard.forEach((row, r) => { 793 | row.forEach((cell, c) => { 794 | if (matchedCells.has([r, c].toString())) { 795 | cell.char = EMPTY; 796 | cell.hasMatched = false; 797 | } 798 | }); 799 | }); 800 | setBoardCellMatrix(newBoard); 801 | 802 | setPlacedCells((prev) => { 803 | return new Set(prev); 804 | }); 805 | 806 | if (matchedCells.size !== 0) { 807 | setMatchedCells(new Set()); 808 | stateHandler.send("CHECK_FOR_CHAIN"); 809 | } 810 | stateHandler.send("SKIP_ANIM"); 811 | } 812 | } else if ("postMatchAnimation" === stateHandler.state.value) { 813 | // Remove matched characters again. 814 | const newBoard = boardCellMatrix.slice(); 815 | newBoard.forEach((row, r) => { 816 | row.forEach((cell, c) => { 817 | if (matchedCells.has([r, c].toString())) { 818 | cell.char = EMPTY; 819 | cell.hasMatched = false; 820 | } 821 | }); 822 | }); 823 | setBoardCellMatrix(newBoard); 824 | 825 | setPlacedCells((prev) => { 826 | prev.clear(); 827 | return prev; 828 | }); 829 | stateHandler.send("DONE"); 830 | } 831 | } 832 | const pageStyle = { 833 | background: BOARD_COLOR, 834 | height: "100%", 835 | width: "100%", 836 | position: "absolute", 837 | // Allow `containerStyle` div to grow downwards, filling the page. 838 | display: "flex", 839 | flexDirection: "column", 840 | } as const; 841 | 842 | const containerStyle = { 843 | display: "flex", 844 | alignItems: "center", 845 | justifyContent: "center", 846 | height: "100%", 847 | width: "100%", 848 | // Prevents `
` from pushing game downwards. 849 | position: "absolute", 850 | } as const; 851 | 852 | const appStyle = { 853 | display: "flex", 854 | flexWrap: "wrap", 855 | flexDirection: "row", 856 | border: `1vmin solid ${EMPTY_CELL_COLOR}`, 857 | padding: "0.4vmin", 858 | top: 0, 859 | borderRadius: UNIVERSAL_BORDER_RADIUS, 860 | } as const; 861 | 862 | // Style of encompassing board. 863 | const boardStyle = { 864 | display: "inline-grid", 865 | gridTemplateColumns: `repeat(${BOARD_COLS}, ${CELL_SIZE})`, 866 | gridTemplateRows: `repeat(${BOARD_ROWS}, ${CELL_SIZE})`, 867 | position: "relative", 868 | background: BOARD_COLOR, 869 | } as const; 870 | 871 | const gameOverTextStyle = { 872 | color: "white", 873 | fontSize: LARGE_TEXT_SIZE, 874 | textAlign: "center", 875 | // WebkitTextStroke: "0.2vmin", 876 | // WebkitTextStrokeColor: BOARD_CELL_COLOR, 877 | } as const; 878 | 879 | return ( 880 |
881 |
882 |
883 | 884 |
885 |
886 | 890 | 894 | 895 | 900 | 901 | 906 | 907 | 910 | 911 |
Game Over
912 | 915 | 919 |
920 |
921 | 922 |
923 |
924 |
925 |
926 | ); 927 | } 928 | --------------------------------------------------------------------------------