Poker hand evaluator
-This project can be built using wasm-pack.\
+This project can be built using cargo and wasm-bindgen-cli (only some functions are available for wasm).\
An example is available [here](https://poker-eval.beaudet.xyz).
+
+To build the web version locally, you should run
+
+```sh
+cargo build --lib --release --target=wasm32-unknown-unknown
+wasm-bindgen --target web --no-typescript --out-dir static target/wasm32-unknown-unknown/release/poker_eval.wasm
+miniserve static/ # or any other webserver you use...
+```
+
+The released binary is light so I included it in the `static folder`.
+
+A deployment script is available in the `static` folder. You should use it as a post-receive git hook script on the server and set it as an executable.
+
Based on Kevin L. Suffecool [poker hand evaluator](http://suffe.cool/poker/evaluator.html)
--- /dev/null
+#!/bin/sh
+
+GIT_REPO="/srv/git/poker-eval.git"
+TMP_GIT_CLONE="/tmp/poker-eval"
+PROD_WWW="/srv/www/poker-eval"
+STAGING_WWW="/srv/www/poker-eval-staging"
+
+while read -r _ _ refname
+do
+ branch=$(git rev-parse --symbolic --abbrev-ref "${refname}")
+
+ if [ "$branch" = "main" ]; then
+ git clone -b main "$GIT_REPO" "$TMP_GIT_CLONE"
+ cp -r "$TMP_GIT_CLONE/static/." "$PROD_WWW"
+ cargo build --manifest-path="$TMP_GIT_CLONE/Cargo.toml" --release --target=wasm32-unknown-unknown
+ wasm-bindgen --target web --no-typescript --out-dir "$PROD_WWW" "$TMP_GIT_CLONE/target/wasm32-unknown-unknown/release/poker_eval.wasm"
+ elif [ "$branch" = "staging" ]; then
+ git clone -b staging "$GIT_REPO" "$TMP_GIT_CLONE"
+ cp -r "$TMP_GIT_CLONE/static/." "$STAGING_WWW"
+ cargo build --manifest-path="$TMP_GIT_CLONE/Cargo.toml" --release --target=wasm32-unknown-unknown
+ wasm-bindgen --target web --no-typescript --out-dir "$STAGING_WWW" "$TMP_GIT_CLONE/target/wasm32-unknown-unknown/release/poker_eval.wasm"
+ fi
+
+ rm -rf "$TMP_GIT_CLONE"
+done
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Poker hand evaluator</title>
+ <link rel="stylesheet" href="styles.css" />
+ <link rel="icon" href="favicon.ico" type="image/x-icon">
+ </head>
+ <body>
+ <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
+ <h1>Poker hand evaluator</h1>
+ <p>Based on Kevin L. Suffecool <a href="http://suffe.cool/poker/evaluator.html">poker hand evaluator</a>.</p>
+ <p>Returns the hand's equivalence value.
+ The evaluator orders hands from 1 to 7462.
+ The lower the score is, the better it is.
+ </p>
+ <p>You should pass five-hand or seven-hand poker cards.<br />
+ A card contains one of '23456789TJQKA' followed by one of 'cdhs'. This is case-sensitive.</p>
+
+ <label for="eval-input">Cards</label>
+ <input class="input" id="eval-input" placeholder="2s 3d Td Kc 2d" type="text" />
+ <button class="input" id="eval-button" type="submit">Evaluate</button>
+ <div id="result">Score:<br />Hand value:</div>
+ <footer>Code available on my <a href="https://git.beaudet.xyz/?p=poker-eval.git">git repository</a>
+ <script src="index.js" type="module" defer></script>
+ </body>
+</html>
--- /dev/null
+import init, { eval_from_str, hand_rank } from './poker_eval.js';
+async function run() {
+ await init();
+}
+run();
+
+const evalButton = document.getElementById("eval-button");
+evalButton.addEventListener("click", _ => {
+ const inputText = document.getElementById("eval-input").value;
+ const resultDiv = document.getElementById("result");
+
+ const result = eval_from_str(inputText);
+ const rank = hand_rank(result);
+ resultDiv.innerHTML = "Score: " + result + "<br />";
+ resultDiv.innerHTML += "Hand value: " + VALUE_STR[rank];
+});
+
+const VALUE_STR = [
+ "",
+ "Straight Flush",
+ "Four of a Kind",
+ "Full House",
+ "Flush",
+ "Staight",
+ "Three of a Kind",
+ "Two Pair",
+ "One Pair",
+ "High Card",
+];
+
+// import { eval_from_str, hand_rank } from "poker-eval";
+
+// const evalButton = document.getElementById("eval-button");
+// // const evalForm = document.getElementById("eval-form");
+// // evalForm.addEventListener("click", event => {
+// // event.preventDefault();
+// // })
+
+// evalButton.addEventListener("click", _ => {
+// const inputText = document.getElementById("eval-input").value;
+// const resultDiv = document.getElementById("result");
+
+// const result = eval_from_str(inputText);
+// const rank = hand_rank(result);
+// resultDiv.innerHTML = "Score: " + result + "<br />";
+// resultDiv.innerHTML += "Hand value: " + VALUE_STR[rank];
+// });
+
+// const VALUE_STR = [
+// "",
+// "Straight Flush",
+// "Four of a Kind",
+// "Full House",
+// "Flush",
+// "Staight",
+// "Three of a Kind",
+// "Two Pair",
+// "One Pair",
+// "High Card",
+// ];
+
--- /dev/null
+
+let wasm;
+
+let WASM_VECTOR_LEN = 0;
+
+let cachegetUint8Memory0 = null;
+function getUint8Memory0() {
+ if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
+ cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachegetUint8Memory0;
+}
+
+let cachedTextEncoder = new TextEncoder('utf-8');
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+ ? function (arg, view) {
+ return cachedTextEncoder.encodeInto(arg, view);
+}
+ : function (arg, view) {
+ const buf = cachedTextEncoder.encode(arg);
+ view.set(buf);
+ return {
+ read: arg.length,
+ written: buf.length
+ };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+ if (realloc === undefined) {
+ const buf = cachedTextEncoder.encode(arg);
+ const ptr = malloc(buf.length);
+ getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+ WASM_VECTOR_LEN = buf.length;
+ return ptr;
+ }
+
+ let len = arg.length;
+ let ptr = malloc(len);
+
+ const mem = getUint8Memory0();
+
+ let offset = 0;
+
+ for (; offset < len; offset++) {
+ const code = arg.charCodeAt(offset);
+ if (code > 0x7F) break;
+ mem[ptr + offset] = code;
+ }
+
+ if (offset !== len) {
+ if (offset !== 0) {
+ arg = arg.slice(offset);
+ }
+ ptr = realloc(ptr, len, len = offset + arg.length * 3);
+ const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+ const ret = encodeString(arg, view);
+
+ offset += ret.written;
+ }
+
+ WASM_VECTOR_LEN = offset;
+ return ptr;
+}
+/**
+* Wrapper over both five-card and seven-card hand evaluator from string.
+*
+* The evaluator orders hands from 1 to 7462.
+*
+* # Example
+*
+* ```
+* use poker_eval::{eval_from_str};
+*
+* assert_eq!(eval_from_str("4s 4d 4h 4c 5d"), 140);
+* assert_eq!(eval_from_str("Ts 9s 8s 7s 6s 7h 6d"), 5);
+* ```
+* @param {string} s
+* @returns {number}
+*/
+export function eval_from_str(s) {
+ var ptr0 = passStringToWasm0(s, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ var len0 = WASM_VECTOR_LEN;
+ var ret = wasm.eval_from_str(ptr0, len0);
+ return ret >>> 0;
+}
+
+/**
+* Returns the hand category based on the hand evaluation.
+*
+* # Example
+*
+* ```
+* use poker_eval::{eval_from_str, hand_rank, constants::FLUSH};
+*
+* let eval = eval_from_str("Js 9s 7s 3s 2s");
+* assert_eq!(eval, 1433);
+* assert_eq!(hand_rank(eval), FLUSH);
+* ```
+* @param {number} i
+* @returns {number}
+*/
+export function hand_rank(i) {
+ var ret = wasm.hand_rank(i);
+ return ret >>> 0;
+}
+
+async function load(module, imports) {
+ if (typeof Response === 'function' && module instanceof Response) {
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
+ try {
+ return await WebAssembly.instantiateStreaming(module, imports);
+
+ } catch (e) {
+ if (module.headers.get('Content-Type') != 'application/wasm') {
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ const bytes = await module.arrayBuffer();
+ return await WebAssembly.instantiate(bytes, imports);
+
+ } else {
+ const instance = await WebAssembly.instantiate(module, imports);
+
+ if (instance instanceof WebAssembly.Instance) {
+ return { instance, module };
+
+ } else {
+ return instance;
+ }
+ }
+}
+
+async function init(input) {
+ if (typeof input === 'undefined') {
+ input = new URL('poker_eval_bg.wasm', import.meta.url);
+ }
+ const imports = {};
+
+
+ if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+ input = fetch(input);
+ }
+
+
+
+ const { instance, module } = await load(await input, imports);
+
+ wasm = instance.exports;
+ init.__wbindgen_wasm_module = module;
+
+ return wasm;
+}
+
+export default init;
+
--- /dev/null
+/* Adapted from https://moderncss.dev/custom-css-styles-for-form-inputs-and-textareas/ */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+:root {
+ --input-border: #8b8a8b;
+ --input-focus-h: 245;
+ --input-focus-s: 100%;
+ --input-focus-l: 42%;
+}
+
+body {
+ min-height: 100vh;
+ display: grid;
+ place-content: center;
+ gap: 0.5rem;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ background-color: #e9f2fd;
+ padding: 1rem;
+}
+
+label {
+ font-size: 1.125rem;
+ font-weight: 500;
+ line-height: 1;
+}
+
+.input + label {
+ margin-top: 2rem;
+}
+
+.input {
+ font-size: 16px;
+ font-size: max(16px, 1em);
+ font-family: inherit;
+ padding: 0.25em 0.5em;
+ background-color: #fff;
+ border: 2px solid var(--input-border);
+ border-radius: 4px;
+ transition: 180ms box-shadow ease-in-out;
+ line-height: 1;
+ height: 2.25rem;
+}
+
+.input:focus {
+ border-color: hsl(
+ var(--input-focus-h),
+ var(--input-focus-s),
+ var(--input-focus-l)
+ );
+ box-shadow: 0 0 0 3px
+ hsla(
+ var(--input-focus-h),
+ var(--input-focus-s),
+ calc(var(--input-focus-l) + 40%),
+ 0.8
+ );
+ outline: 3px solid transparent;
+}