Hand History

Dette kan allerede gjøres standard i PokerMavens, men er litt kronglete og om du forlater bordet så mister du også hand history. En lettere måte å gjøre dette på er gjennom et userscript som husker hendene selv om du forlater bordet men om du refresher vinduet så tror jeg du mister alt du ikke har lagret. Videre så fungerer dette bare i Chrome/Chromium nettlesere som støtter Extensions.

  1. Install TamperMonkey i Chrome
    Trykk på de 3 prikkene øverst til høyre i Chrome og velg Extensions -> Visit Chrome Web Store. I søkefeltet øverst, skriv inn tampermonkey og klikk på TamperMonkey i resultatlista. Der finner du en knapp får å installere.
  2. Installer scriptet
    (For de som er litt mer erfarne med TamperMonkey kan installere scriptet her og så editere linjen som definerer serveren, som i skrivende stund er linje 7 i versjon 4 av scriptet.)
    Klikk på TamperMonkey ikonet som du finner mellom adressefeltet og Chrome profilbildet ditt.

I menyen du får opp, velg Create a new script… Du får nå opp et vindu med noe standard kodetekst. Merk alt og slett all tekst som er i teksteditoren fra før. Paste deretter inn koden som du finner helt nederst på siden her.

Velg deretter File fra menyen over teksteditoren og Save, deretter File menyen igjen og Close. Du blir da sendt til Installed Userscripts i TamperMonkey og der skal du se Mavens Hand History Saver. Det kan hende den bare står som ‘new userscript’ e.l. frem til du restarter nettleseren, det er ikke så nøye.

Restart nettleseren din og logg inn på Atlantis. Du skal nå se et rødt 1-tall i TamperMonkey-ikonet. Dette illustrerer at TamperMonkey kjører et script på den siden du er på.

I Lobby, under Account vil du nå se to nye meny-valg.

Du kan nå laste ned alle hender du har observert siden du logget inn (denne gangen). Dersom du har spilt noen hender vil dine kort bli lagret i HH, det samme gjelder kort som gikk til showdown. Du må selv velge når/om du vil laste ned hand history. Dersom du ofte mister forbindelsen vil jeg anbefale at du lagrer ofte og tar Clear hand histories… etter å ha lagret.

Det er et par quirks her.

  • Det er ikke jeg som har laget user scriptet
  • Alle hender på et bord blir én tekstfil
  • Om du har sett/spilt 100 hender på et bord og lagrer så laster du ned en zip-fil med alle 100 hendene på det bordet
  • Om du spiller 2 hender til og laster ned igjen, så får du en ny zip-fil med 102 hender
  • Jeg tror du mister alle hender om du refresher nettsiden. Zip-fila/ene med allerede lastet ned HH vil uansett ikke bli rørt
  • Filene lagres der du vanligvis lagrer filer du laster ned fra Chrome, typisk Download-folderen din om du ikke har endret det
  • Jeg aner ikke om de filene funker i PT4/HM3/H2N osv. Da må dere i så fall kontakte support hos de for å få fiksa det.

Scriptet

// ==UserScript==
// @name         Mavens Hand History Saver
// @namespace    strobe878
// @version      4
// @description  Saves poker hand histories and allows downloading them as ZIP files
// @author       strobe878
// @match        *://atlantis.pokernites.com:8087/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @run-at       document-idle
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/522948/Mavens%20Hand%20History%20Saver.user.js
// @updateURL https://update.greasyfork.org/scripts/522948/Mavens%20Hand%20History%20Saver.meta.js
// ==/UserScript==
(function() {
  'use strict';
  const dbVersion = 2;
  // Track tables and their current hand histories
  const tables = new Map(); // tableId -> TableHand
  // Global variables
  let username = null;
  let menuItem = null;
  let previousUsername = null;
  const initializedDatabases = new Set();
  // Constants
  const DB_PREFIX = 'PokerHandsDB_';
  const STORE_NAME = 'hands';
  // Check if we're running in a mobile context
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  // Add error handling for script initialization
  window.addEventListener('error', function(e) {
    console.error('[PHHS] Global error:', e.message, 'at', e.filename, ':', e.lineno);
  });
  // Define the debounce helper before using it
  function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }
  // Main observer for detecting tables and username changes
  const mainObserver = new MutationObserver(
    debounce(async (mutations) => {
      // Try both desktop and mobile selectors for lobby
      let lobbyDiv = isMobile ?
      document.getElementById('SiteMobile') :
      document.querySelector('div#Lobby > div.header > div.title');
      if (!lobbyDiv) {
        return;
      }
      const lobbyTitle = lobbyDiv.textContent;
      // Get username from lobby
      let newUsername = null;
      if (isMobile) {
        // two cases: lobby view or table view
        newUsername = (lobbyTitle.match(/.+ - Logged in as (.+)/) || [])[1]?.trim() ||
        (lobbyTitle.match(/.+ - (.+)/) || [])[1]?.trim() ||
        null;
      } else {
        newUsername = (lobbyTitle.match(/Lobby - (.+) logged in/) || [])[1]?.trim() ||
        null;
      }
      // Update UI if username changed
      if (newUsername !== username) {
        updateUI(newUsername);
        username = newUsername;
      }
      if (!username) {
        for (const [tableId, table] of tables) {
          await saveAndRemoveTable(tableId);
        }
        return;
      }
      // Initialize database if needed
      try {
        await initializeDB(username);
      } catch (error) {
        console.error('[PHHS] Failed to initialize database:', error);
        return;
      }
      // Find active tables
      const tableDivs = Array.from(document.querySelectorAll('div[class="dialog"]:has(> div[class="tablecontent"])'));
      const activeTables = tableDivs
      .map(tableDiv => {
        let currentDiv = tableDiv.nextElementSibling;
        while (currentDiv) {
          if (currentDiv.classList.contains('dialog')) {
            const infotabs = currentDiv.querySelector('div.infotabs');
            if (infotabs) return infotabs;
          }
          currentDiv = currentDiv.nextElementSibling;
        }
        return null;
      })
      .filter(Boolean);
      // Remove closed tables
      for (const [tableId, table] of tables) {
        if (!activeTables.includes(table.div)) {
          await saveAndRemoveTable(tableId);
        }
      }
      // Process active tables
      activeTables.forEach(tableDiv => {
        const infoDiv = tableDiv.querySelector('div.generalinfo > div.memo > pre');
        if (!infoDiv?.textContent) return;
        const tableMatch = infoDiv.textContent.match(/^Table name:\s+(.+)/) ||
              infoDiv.textContent.match(/^Tournament name:\s+(.*)/);
        const typeMatch = infoDiv.textContent.match(/Type:\s+(.+)/);
        if (!tableMatch || !typeMatch) return;
        const tableName = tableMatch[1].trim();
        const tableType = typeMatch[1].trim();
        const tableId = `${tableName} [${tableType}]`;
        // Create new table tracking if it doesn't exist
        if (!tables.has(tableId)) {
          const tableHand = new TableHand(tableId);
          tables.set(tableId, tableHand);
          tableHand.attachTo(tableDiv);
        }
      });
    }, 500)
  );
  // Helper function to save and remove a table
  async function saveAndRemoveTable(tableId) {
    const table = tables.get(tableId);
    if (table) {
      await table.save();
      table.detach();
      tables.delete(tableId);
    }
  }
  // Function to start observing
  const startObserving = () => {
    // Try both desktop and mobile selectors
    const clientDiv = document.getElementById('client_div') ||
    document.getElementById('SiteMobile') ||
    document.querySelector('.site-mobile');
    if (clientDiv) {
      mainObserver.observe(clientDiv, {
        childList: true,
        subtree: true
      });
    } else {
      setTimeout(startObserving, 2000);
    }
  };
  // Start immediately but also retry if needed
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', startObserving);
  } else {
    startObserving();
  }
  // Get database name for current user
  const getDBName = (username) => `${DB_PREFIX}${username}`;
  // Structure for tracking a table's current hand
  class TableHand {
    constructor(tableId) {
      this.tableId = tableId;
      this.handNumber = null;
      this.lines = [];
      this.div = null;
      this.observer = null;
    }
    async addLines(text) {
      const handMatch = text.match(/Hand #(\d+-\d+)/);
      if (!handMatch) {
        return;
      }
      const handNumber = handMatch[1];
      if (this.handNumber !== handNumber) {
        await this.save();
        this.handNumber = handNumber;
      }
      this.lines = text
        .split('<br>')
        .map(line => line.trim())
        .filter(line => line);
    }
    async save() {
      if (!username || !this.handNumber) {
        return;
      }
      const filename = `${this.tableId}-${this.handNumber}.txt`;
      try {
        await withDB(username, async (db) => {
          const transaction = db.transaction([STORE_NAME], 'readwrite');
          const store = transaction.objectStore(STORE_NAME);
          await Promise.all([
            new Promise((resolve, reject) => {
              const request = store.put({
                tableId: this.tableId,
                handNumber: this.handNumber,
                content: this.lines.join('\n'),
                contentType: 'text/plain'
              });
              request.onerror = () => reject(request.error);
              request.onsuccess = () => resolve();
            }),
            new Promise((resolve, reject) => {
              transaction.oncomplete = () => resolve();
              transaction.onerror = () => reject(transaction.error);
            })
          ]);
        });
      } catch (error) {
        console.error(`[PHHS] Error saving hand ${filename}:`, error);
      }
      this.handNumber = null;
      this.lines = [];
    }
    attachTo(tableDiv) {
      this.div = tableDiv;
      this.observer = createTableObserver(tableDiv, this.tableId, username);
    }
    detach() {
      if (this.observer) {
        this.observer.disconnect();
        this.observer = null;
      }
      this.div = null;
    }
  }
  // Create observer for a table's history
  function createTableObserver(tableDiv, tableId, username) {
    const historyDiv = tableDiv.querySelector('div.historyinfo > div.memo');
    if (!historyDiv) {
      console.error(`[PHHS] Could not find history div for table ${tableId}`);
      return null;
    }
    const observer = new MutationObserver(async (mutations) => {
      const historyText = historyDiv.innerHTML;
      const tableHand = tables.get(tableId);
      await tableHand.addLines(historyText);
    });
    observer.observe(historyDiv, {
      childList: true,
      characterData: true,
      subtree: true
    });
    return observer;
  }
  // Helper for creating menu items
  const createMenuItem = (id, text, handler) => {
    const item = document.createElement('li');
    item.id = id;
    item.textContent = text;
    $(item).on("touchstart mousedown", function(e) {
      if (!menuItem) {
        menuItem = this;
        return false;
      }
    });
    $(item).on("touchend mouseup", function(e) {
      if (!menuItem) return;
      const wasMenuItem = (this === menuItem);
      $(this).parent().hide();
      menuItem = null;
      if (wasMenuItem) {
        handler();
      }
      return false;
    });
    return item;
  };
  // Create UI elements
  const createUI = () => {
    const accountSpan = document.querySelector('span#AccountMenu');
    if (!accountSpan) {
      console.error('[PHHS] Account menu span not found');
      return null;
    }
    const accountMenu = accountSpan.nextElementSibling;
    if (!accountMenu || accountMenu.tagName !== 'UL') {
      console.error('[PHHS] Account menu ul not found');
      return null;
    }
    const computedStyle = window.getComputedStyle(accountMenu);
    const defaultColor = computedStyle.color;
    const defaultBgColor = computedStyle.backgroundColor;
    // Create Download Hands menu item
    const downloadItem = createMenuItem('AccountDownloadHands', 'Download hand histories...', handleDownload);
    const clearItem = createMenuItem('AccountClearHands', 'Clear hand histories...', handleClear);
    // Set initial styles and add hover effects
    [downloadItem, clearItem].forEach(item => {
      item.style.color = defaultColor;
      item.style.backgroundColor = defaultBgColor;
      // Add hover effects
      item.addEventListener('mouseenter', () => {
        item.style.color = defaultBgColor;
        item.style.backgroundColor = defaultColor;
      });
      item.addEventListener('mouseleave', () => {
        item.style.color = defaultColor;
        item.style.backgroundColor = defaultBgColor;
      });
    });
    // Add items to menu
    accountMenu.appendChild(downloadItem);
    accountMenu.appendChild(clearItem);
    return { downloadButton: downloadItem, clearButton: clearItem };
  };
  // Remove UI elements
  const removeUI = () => {
    const downloadItem = document.getElementById('AccountDownloadHands');
    const clearItem = document.getElementById('AccountClearHands');
    if (downloadItem) downloadItem.remove();
    if (clearItem) clearItem.remove();
  };
  // Handle download click
  const handleDownload = async () => {
    if (!username) {
      console.error('[PHHS] No username available for download');
      return;
    }
    let db = null;
    try {
      const dbName = getDBName(username);
      // Open database
      db = await new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, dbVersion);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
      });
      // Check if store exists
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        alert('No hand histories available.');
        return;
      }
      // Get all hands
      const transaction = db.transaction([STORE_NAME], 'readonly');
      const store = transaction.objectStore(STORE_NAME);
      const hands = await new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
      });
      if (hands.length === 0) {
        alert('No hand histories available.');
        return;
      }
      // Create a file for each table and session.
      // The hand number, e.g., 1234-5, has two parts:
      //   First part: absolute hand number across all tables
      //   Second part: hand number for the current session on a table, starting from 1.
      try {
        const zip = new JSZip();
        // Group hands by table and session
        const sessions = new Map(); // Map<tableId, Map<sessionStart, hands[]>>
        hands.forEach(hand => {
          const { tableId, handNumber } = hand;
          const [absoluteHandNumber, sessionHandNumber] = handNumber.split('-').map(Number);
          if (sessionHandNumber === 1) {
            if (!sessions.has(tableId)) {
              sessions.set(tableId, new Map());
            }
            sessions.get(tableId).set(absoluteHandNumber, []);
          }
          const tableMap = sessions.get(tableId);
          const sessionStart = Math.max(...[...tableMap.keys()].filter(start => start <= absoluteHandNumber));
          tableMap.get(sessionStart).push(hand);
        });
        for (const [tableId, tableSessions] of sessions) {
          for (const [sessionStart, sessionHands] of tableSessions) {
            // Sort hands by absolute hand number
            sessionHands.sort((a, b) => {
              const [aAbs] = a.handNumber.split('-').map(Number);
              const [bAbs] = b.handNumber.split('-').map(Number);
              return aAbs - bAbs;
            });
            // Concatenate hands in a session
            const sessionContent = sessionHands.map(hand => hand.content).join('\n\n');
            const filename = `${tableId} - Session starting ${sessionStart}.txt`;
            zip.file(filename, sessionContent);
          }
        }
        const blob = await zip.generateAsync({
          type: "blob",
          compression: "DEFLATE"
        });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `poker_hands_${username}_${new Date().toISOString().split('T')[0]}.zip`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      } catch (e) {
        console.error('Error creating zip:', e);
      }
    } catch (error) {
      console.error('[PHHS] Error during download:', error);
    } finally {
      if (db) {
        db.close();
      }
    }
  };
  // Handle clear click
  const handleClear = async () => {
    if (!username) {
      return;
    }
    let db = null;
    try {
      const dbName = getDBName(username);
      // Open database
      db = await new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, dbVersion);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
      });
      // Check if store exists
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        alert('No hand histories available.');
        return;
      }
      // Get count and size before confirming
      const transaction = db.transaction([STORE_NAME], 'readonly');
      const store = transaction.objectStore(STORE_NAME);
      const hands = await new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
      });
      if (hands.length === 0) {
        alert('No hand histories available.');
        return;
      }
      const totalBytes = hands.reduce((sum, hand) => sum + hand.content.length, 0);
      const size = formatSize(totalBytes);
      if (!confirm(`Are you sure you want to clear all stored hands?\n\nThis will delete ${hands.length} hands (${size}).`)) {
        return;
      }
      // Clear all hands
      const clearTransaction = db.transaction([STORE_NAME], 'readwrite');
      const clearStore = clearTransaction.objectStore(STORE_NAME);
      await new Promise((resolve, reject) => {
        const request = clearStore.clear();
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve();
      });
    } catch (error) {
      console.error('Error clearing hands:', error);
    } finally {
      if (db) {
        db.close();
      }
    }
  };
  // Database helper
  const withDB = async (username, callback) => {
    if (!username) {
      console.error('[PHHS] DB operation attempted without username');
      return null;
    }
    let db = null;
    try {
      const dbName = getDBName(username);
      db = await new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, dbVersion);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
      });
      return await callback(db);
    } catch (error) {
      console.error('[PHHS] Database error:', error);
      throw error;
    } finally {
      if (db) {
        db.close();
      }
    }
  };
  // Initialize database for a user
  const initializeDB = (username) => {
    return new Promise((resolve, reject) => {
      const dbName = getDBName(username);
      const request = indexedDB.open(dbName, dbVersion);
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (event.oldVersion < 1) {
          db.createObjectStore(STORE_NAME, { keyPath: ['tableId', 'handNumber'] });
          return;
        }
        if (event.oldVersion < 2) {
          const transaction = event.target.transaction;
          const oldStore = transaction.objectStore(STORE_NAME);
          oldStore.getAll().onsuccess = (event) => {
            const records = event.target.result;
            // Delete old store
            db.deleteObjectStore(STORE_NAME);
            // Create new store with composite key
            const newStore = db.createObjectStore(STORE_NAME, {
              keyPath: ['tableId', 'handNumber']
            });
            // Migrate each record
            records.forEach(record => {
              // Split out the tableId and handNumber from the filename
              const match = record.filename.match(/(.*)-([^-]+-[^-]+)\.txt$/);
              if (match) {
                const [_, tableId, handNumber] = match;
                newStore.add({
                  tableId,
                  handNumber,
                  content: record.content,
                  contentType: record.contentType
                });
              }
            });
          };
        }
      };
      request.onsuccess = () => {
        const db = request.result;
        db.close();
        resolve();
      };
      request.onerror = (event) => {
        console.error('Database initialization error:', event.target.error);
        reject(event.target.error);
      };
    });
  };
  // Update UI based on username changes
  const updateUI = async (newUsername) => {
    if (previousUsername !== newUsername) {
      initializedDatabases.clear();
    }
    // Username changed from null to non-null
    if (!previousUsername && newUsername) {
      const elements = createUI();
      if (elements) {
        const { downloadButton, clearButton } = elements;
        await initializeDB(newUsername);
      }
    }
    // Username changed from non-null to null
    else if (previousUsername && !newUsername) {
      removeUI();
    }
    // Username changed to different user
    else if (previousUsername && newUsername && previousUsername !== newUsername) {
      await initializeDB(newUsername);
    }
    previousUsername = newUsername;
  };
  // Helper function to format size
  const formatSize = (bytes) => {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
    if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
  };
})();

Sist oppdatert 31. januar 2025