1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-12-19 10:22:27 +00:00
emacs/lisp/play/bubbles.el
2009-01-05 03:18:22 +00:00

1454 lines
45 KiB
EmacsLisp
Raw Blame History

;;; bubbles.el --- Puzzle game for Emacs.
;; Copyright (C) 2007, 2008, 2009 Free Software Foundation, Inc.
;; Author: Ulf Jasper <ulf.jasper@web.de>
;; URL: http://ulf.epplejasper.de/
;; Created: 5. Feb. 2007
;; Keywords: games
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Bubbles is a puzzle game. Its goal is to remove as many bubbles as
;; possible in as few moves as possible.
;; Bubbles is an implementation of the "Same Game", similar to "Same
;; GNOME" and many others, see <http://en.wikipedia.org/wiki/SameGame>.
;; Installation
;; ------------
;; Add the following lines to your Emacs startup file (`~/.emacs').
;; (add-to-list 'load-path "/path/to/bubbles/")
;; (autoload 'bubbles "bubbles" "Play Bubbles" t)
;; ======================================================================
;;; History:
;; 0.5 (2007-09-14)
;; - Minor bugfixes.
;; 0.4 (2007-08-27)
;; - Allow for undoing last move.
;; - Bonus for removing all bubbles.
;; - Speed improvements.
;; - Animation enhancements.
;; - Added `bubbles-mode-hook'.
;; - Fixes: Don't move point.
;; - New URL.
;; 0.3 (2007-03-11)
;; - Renamed shift modes and thus names of score files. All
;; highscores are lost, unless you rename the score files from
;; bubbles-shift-... to bubbles-...!
;; - Bugfixes: Check for successful image creation.
;; Disable menus and counter when game is over.
;; Tested with GNU Emacs 22.0.93
;; 0.2 (2007-02-24)
;; - Introduced game themes.
;; - Introduced graphics themes (changeable while playing).
;; - Added menu.
;; - Customization: grid size, colors, chars, shift mode.
;; - More keybindings.
;; - Changed shift direction from to-right to to-left.
;; - Bugfixes: Don't remove single-bubble regions;
;; Animation glitches fixed.
;; Tested with GNU Emacs 22.0.93 and 21.4.1.
;; 0.1 (2007-02-11)
;; Initial release. Tested with GNU Emacs 22.0.93 and 21.4.1.
;; ======================================================================
;;; Code:
(defconst bubbles-version "0.5" "Version number of bubbles.el.")
(require 'gamegrid)
(require 'cl)
;; User options
;; Careful with that axe, Eugene! Order does matter in the custom
;; section below.
(defcustom bubbles-game-theme
'easy
"Overall game theme.
The overall game theme specifies a grid size, a set of colors,
and a shift mode."
:type '(radio (const :tag "Easy" easy)
(const :tag "Medium" medium)
(const :tag "Difficult" difficult)
(const :tag "Hard" hard)
(const :tag "User defined" user-defined))
:group 'bubbles)
(defun bubbles-set-game-easy ()
"Set game theme to 'easy'."
(interactive)
(setq bubbles-game-theme 'easy)
(bubbles))
(defun bubbles-set-game-medium ()
"Set game theme to 'medium'."
(interactive)
(setq bubbles-game-theme 'medium)
(bubbles))
(defun bubbles-set-game-difficult ()
"Set game theme to 'difficult'."
(interactive)
(setq bubbles-game-theme 'difficult)
(bubbles))
(defun bubbles-set-game-hard ()
"Set game theme to 'hard'."
(interactive)
(setq bubbles-game-theme 'hard)
(bubbles))
(defun bubbles-set-game-userdefined ()
"Set game theme to 'user-defined'."
(interactive)
(setq bubbles-game-theme 'user-defined)
(bubbles))
(defgroup bubbles nil
"Bubbles, a puzzle game."
:group 'games)
(defcustom bubbles-graphics-theme
'circles
"Graphics theme.
It is safe to choose a graphical theme. If Emacs cannot display
images the `ascii' theme will be used."
:type '(radio (const :tag "Circles" circles)
(const :tag "Squares" squares)
(const :tag "Diamonds" diamonds)
(const :tag "Balls" balls)
(const :tag "Emacs" emacs)
(const :tag "ASCII (no images)" ascii))
:group 'bubbles)
(defconst bubbles--grid-small '(10 . 10)
"Predefined small bubbles grid.")
(defconst bubbles--grid-medium '(15 . 10)
"Predefined medium bubbles grid.")
(defconst bubbles--grid-large '(20 . 15)
"Predefined large bubbles grid.")
(defconst bubbles--grid-huge '(30 . 20)
"Predefined huge bubbles grid.")
(defcustom bubbles-grid-size
bubbles--grid-medium
"Size of bubbles grid."
:type `(radio (const :tag "Small" ,bubbles--grid-small)
(const :tag "Medium" ,bubbles--grid-medium)
(const :tag "Large" ,bubbles--grid-large)
(const :tag "Huge" ,bubbles--grid-huge)
(cons :tag "User defined"
(integer :tag "Width")
(integer :tag "Height")))
:group 'bubbles)
(defconst bubbles--colors-2 '("orange" "violet")
"Predefined bubbles color list with two colors.")
(defconst bubbles--colors-3 '("lightblue" "palegreen" "pink")
"Predefined bubbles color list with three colors.")
(defconst bubbles--colors-4 '("firebrick" "sea green" "steel blue" "chocolate")
"Predefined bubbles color list with four colors.")
(defconst bubbles--colors-5 '("firebrick" "sea green" "steel blue"
"sandy brown" "bisque3")
"Predefined bubbles color list with five colors.")
(defcustom bubbles-colors
bubbles--colors-3
"List of bubble colors.
The length of this list determines how many different bubble
types are present."
:type `(radio (const :tag "Red, darkgreen" ,bubbles--colors-2)
(const :tag "Red, darkgreen, blue" ,bubbles--colors-3)
(const :tag "Red, darkgreen, blue, orange" ,bubbles--colors-4)
(const :tag "Red, darkgreen, blue, orange, violet"
,bubbles--colors-5)
(repeat :tag "User defined" color))
:group 'bubbles)
(defcustom bubbles-chars
'(?+ ?O ?# ?X ?. ?* ?& ?<3F>)
"Characters used for bubbles.
Note that the actual number of different bubbles is determined by
the number of colors, see `bubbles-colors'."
:type '(repeat character)
:group 'bubbles)
(defcustom bubbles-shift-mode
'default
"Shift mode.
Available modes are `shift-default' and `shift-always'."
:type '(radio (const :tag "Default" default)
(const :tag "Shifter" always)
;;(const :tag "Mega Shifter" 'mega)
)
:group 'bubbles)
(defcustom bubbles-mode-hook nil
"Hook run by Bubbles mode."
:group 'bubbles
:type 'hook)
(defun bubbles-customize ()
"Open customization buffer for bubbles."
(interactive)
(customize-group 'bubbles))
;; ======================================================================
;; internal variables
(defvar bubbles--score 0
"Current Bubbles score.")
(defvar bubbles--neighbourhood-score 0
"Score of active bubbles neighborhood.")
(defvar bubbles--faces nil
"List of currently used faces.")
(defvar bubbles--playing nil
"Play status indicator.")
(defvar bubbles--empty-image nil
"Image used for removed bubbles (empty grid cells).")
(defvar bubbles--images nil
"List of images for bubbles.")
(defvar bubbles--images-ok nil
"Indicate whether images have been created successfully.")
(defvar bubbles--col-offset 0
"Horizontal offset for centering the bubbles grid.")
(defvar bubbles--row-offset 0
"Vertical offset for centering the bubbles grid.")
(defvar bubbles--save-data nil
"List containing bubbles save data (SCORE BUFFERCONTENTS).")
(defconst bubbles--image-template-circle
"/* XPM */
static char * dot_xpm[] = {
\"20 20 2 1\",
\" c None\",
\". c #FFFFFF\",
\" ...... \",
\" .......... \",
\" .............. \",
\" ................ \",
\" ................ \",
\" .................. \",
\" .................. \",
\"....................\",
\"....................\",
\"....................\",
\"....................\",
\"....................\",
\"....................\",
\" .................. \",
\" .................. \",
\" ................ \",
\" ................ \",
\" .............. \",
\" .......... \",
\" ...... \"};")
(defconst bubbles--image-template-square
"/* XPM */
static char * dot_xpm[] = {
\"20 20 2 1\",
\"0 c None\",
\"1 c #FFFFFF\",
\"00000000000000000000\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"01111111111111111110\",
\"00000000000000000000\"};")
(defconst bubbles--image-template-diamond
"/* XPM */
static char * dot_xpm[] = {
\"20 20 2 1\",
\"0 c None\",
\"1 c #FFFFFF\",
\"00000000011000000000\",
\"00000000111100000000\",
\"00000001111110000000\",
\"00000011111111000000\",
\"00000111111111100000\",
\"00001111111111110000\",
\"00011111111111111000\",
\"00111111111111111100\",
\"01111111111111111110\",
\"11111111111111111111\",
\"01111111111111111110\",
\"00111111111111111100\",
\"00011111111111111000\",
\"00001111111111110000\",
\"00000111111111100000\",
\"00000011111111000000\",
\"00000001111110000000\",
\"00000000111100000000\",
\"00000000011000000000\",
\"00000000000000000000\"};")
(defconst bubbles--image-template-emacs
"/* XPM */
static char * emacs_24_xpm[] = {
\"24 24 129 2\",
\" c None\",
\". c #837DA4\",
\"+ c #807AA0\",
\"@ c #9894B2\",
\"# c #CCCAD9\",
\"$ c #C2C0D2\",
\"% c #B6B3C9\",
\"& c #A19DB9\",
\"* c #8681A5\",
\"= c #7D779B\",
\"- c #B6B3C7\",
\"; c #ABA7BE\",
\"> c #9792AF\",
\", c #AAA6BD\",
\"' c #CBC9D7\",
\") c #AAA7BE\",
\"! c #908BAA\",
\"~ c #797397\",
\"{ c #948FAC\",
\"] c #9A95B1\",
\"^ c #EBEAEF\",
\"/ c #F1F1F5\",
\"( c #BCB9CB\",
\"_ c #A9A5BD\",
\": c #757093\",
\"< c #918DA9\",
\"[ c #DDDBE4\",
\"} c #FFFFFF\",
\"| c #EAE9EF\",
\"1 c #A7A4BA\",
\"2 c #716C8F\",
\"3 c #8D89A5\",
\"4 c #9C98B1\",
\"5 c #DBDAE3\",
\"6 c #A4A1B7\",
\"7 c #6E698A\",
\"8 c #8B87A1\",
\"9 c #928EA7\",
\"0 c #C5C3D1\",
\"a c #F8F8F9\",
\"b c #CCCAD6\",
\"c c #A29FB4\",
\"d c #6A6585\",
\"e c #88849D\",
\"f c #B5B2C2\",
\"g c #F0F0F3\",
\"h c #E1E0E6\",
\"i c #A5A2B5\",
\"j c #A09DB1\",
\"k c #676281\",
\"l c #85819A\",
\"m c #9591A7\",
\"n c #E1E0E5\",
\"o c #F0EFF2\",
\"p c #B3B0C0\",
\"q c #9D9AAE\",
\"r c #635F7C\",
\"s c #827F96\",
\"t c #9997AA\",
\"u c #F7F7F9\",
\"v c #C8C7D1\",
\"w c #89869D\",
\"x c #9B99AB\",
\"y c #5F5B78\",
\"z c #7F7C93\",
\"A c #CFCDD6\",
\"B c #B7B5C2\",
\"C c #9996A9\",
\"D c #5C5873\",
\"E c #7A778D\",
\"F c #F5F5F6\",
\"G c #8E8C9E\",
\"H c #7D798F\",
\"I c #58546F\",
\"J c #6C6981\",
\"K c #D5D4DB\",
\"L c #F5F4F6\",
\"M c #9794A5\",
\"N c #625F78\",
\"O c #79768C\",
\"P c #55516A\",
\"Q c #605C73\",
\"R c #CAC9D1\",
\"S c #EAE9EC\",
\"T c #B4B3BE\",
\"U c #777488\",
\"V c #514E66\",
\"W c #DEDEE2\",
\"X c #F4F4F5\",
\"Y c #9D9BA9\",
\"Z c #747185\",
\"` c #4E4B62\",
\" . c #DEDDE1\",
\".. c #A6A5B0\",
\"+. c #716F81\",
\"@. c #4A475D\",
\"#. c #A4A3AE\",
\"$. c #F4F3F5\",
\"%. c #777586\",
\"&. c #6E6C7D\",
\"*. c #464358\",
\"=. c #514E62\",
\"-. c #B9B8C0\",
\";. c #D1D0D5\",
\">. c #747282\",
\",. c #6B6979\",
\"'. c #434054\",
\"). c #5A5769\",
\"!. c #D0CFD4\",
\"~. c #5B5869\",
\"{. c #696676\",
\"]. c #403D50\",
\"^. c #DBDADE\",
\"/. c #F3F3F4\",
\"(. c #646271\",
\"_. c #666473\",
\":. c #3D3A4C\",
\"<. c #555362\",
\"[. c #9E9DA6\",
\"}. c #9E9CA5\",
\"|. c #646170\",
\"1. c #393647\",
\"2. c #514E5D\",
\"3. c #83818C\",
\"4. c #A8A7AE\",
\"5. c #E6E6E8\",
\"6. c #DAD9DC\",
\"7. c #353343\",
\"8. c #32303E\",
\" . . . . . . . . . . . . . . . . . . \",
\" + @ # $ % % % % % % % % % % % % % % & * + + \",
\" = - ; > > > > > > > > , ' ) > > > > > > ! = \",
\"~ ~ { { { { { { { { { { { ] ^ / ( { { { { _ ~ ~ \",
\": : < < < < < < < < < < < < [ } } | < < < 1 : : \",
\"2 2 3 3 3 3 3 3 3 3 3 3 4 5 } } } 5 3 3 3 6 2 2 \",
\"7 7 8 8 8 8 8 8 8 8 9 0 a } } } b 8 8 8 8 c 7 7 \",
\"d d e e e e e e e f g } } } h i e e e e e j d d \",
\"k k l l l l l m n } } } o p l l l l l l l q k k \",
\"r r s s s s t u } } } v w s s s s s s s s x r r \",
\"y y z z z z A } } } B z z z z z z z z z z C y y \",
\"D D D D D D E F } } G D D D D D D D D D D H D D \",
\"I I I I I I I J K } L M N I I I I I I I I O I I \",
\"P P P P P P Q R } } } S T P P P P P P P P U P P \",
\"V V V V V V W } } X Y V V V V V V V V V V Z V V \",
\"` ` ` ` ` ` .} } ..` ` ` ` ` ` ` ` ` ` ` +.` ` \",
\"@.@.@.@.@.@.@.#.$.$.%.@.@.@.@.@.@.@.@.@.@.&.@.@.\",
\"*.*.*.*.*.*.*.*.=.-.} ;.>.*.*.*.*.*.*.*.*.,.*.*.\",
\"'.'.'.'.'.'.'.'.'.'.).!.} !.~.'.'.'.'.'.'.{.'.'.\",
\"].].].].].].].].].].].].^.} /.(.].].].].]._.].].\",
\":.:.:.:.:.:.:.:.:.:.<.[./.} } }.:.:.:.:.:.|.:.:.\",
\" 1.1.1.1.1.1.1.1.2.3.4.5.6.3.1.1.1.1.1.1.1.1. \",
\" 7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7. \",
\" 8.8.8.8.8.8.8.8.8.8.8.8.8.8.8.8.8.8. \"};")
(defconst bubbles--image-template-ball
"/* XPM */
static char * dot3d_xpm[] = {
\"20 20 190 2\",
\" c None\",
\". c #F9F6F6\",
\"+ c #D6D0D0\",
\"@ c #BFBBBB\",
\"# c #AAA4A4\",
\"$ c #ABAAAB\",
\"% c #A8A8A8\",
\"& c #A29D9D\",
\"* c #B5B2B2\",
\"= c #CDC9C9\",
\"- c #D7D0D0\",
\"; c #B3AFAF\",
\"> c #B5B5B5\",
\", c #B7B7B7\",
\"' c #B8B8B8\",
\") c #B6B6B6\",
\"! c #B3B3B3\",
\"~ c #AFAFAF\",
\"{ c #A9A9A9\",
\"] c #A2A2A2\",
\"^ c #9C9A9A\",
\"/ c #C9C5C5\",
\"( c #FDFBFB\",
\"_ c #C3BCBC\",
\": c #BBBBBB\",
\"< c #C0C0C0\",
\"[ c #C3C2C2\",
\"} c #C3C3C3\",
\"| c #C2C2C2\",
\"1 c #BEBEBE\",
\"2 c #B9B9B9\",
\"3 c #B2B2B2\",
\"4 c #ABAAAA\",
\"5 c #999999\",
\"6 c #ACA7A7\",
\"7 c #C2BBBB\",
\"8 c #C5C5C5\",
\"9 c #CACBCB\",
\"0 c #CECECE\",
\"a c #CFCFCF\",
\"b c #CDCDCD\",
\"c c #C8C9C9\",
\"d c #9F9F9F\",
\"e c #959595\",
\"f c #A9A5A5\",
\"g c #D5CFCE\",
\"h c #BDBDBD\",
\"i c #C6C6C6\",
\"j c #D5D5D5\",
\"k c #D9D9D9\",
\"l c #DADADA\",
\"m c #D8D8D8\",
\"n c #D2D2D2\",
\"o c #CBCBCB\",
\"p c #A4A4A5\",
\"q c #9A9A9A\",
\"r c #8F8F8F\",
\"s c #C3BFBF\",
\"t c #AFACAB\",
\"u c #CCCCCC\",
\"v c #D6D6D6\",
\"w c #DEDEDE\",
\"x c #E4E4E4\",
\"y c #E5E5E5\",
\"z c #E2E2E2\",
\"A c #DBDBDB\",
\"B c #C9C8C8\",
\"C c #A8A9A8\",
\"D c #9D9E9D\",
\"E c #929292\",
\"F c #8A8888\",
\"G c #D3CECE\",
\"H c #B0B0B0\",
\"I c #D1D1D1\",
\"J c #DCDCDC\",
\"K c #E6E6E6\",
\"L c #EEEEEE\",
\"M c #F1F1F0\",
\"N c #EBEBEB\",
\"O c #D7D7D8\",
\"P c #ABABAB\",
\"Q c #A0A0A0\",
\"R c #949494\",
\"S c #898989\",
\"T c #C0BDBD\",
\"U c #B9B6B6\",
\"V c #B1B1B1\",
\"W c #BCBCBC\",
\"X c #C8C8C8\",
\"Y c #D3D3D3\",
\"Z c #DFDFDE\",
\"` c #EAEAEA\",
\" . c #F5F5F5\",
\".. c #FAFAFA\",
\"+. c #F1F1F1\",
\"@. c #CECFCF\",
\"#. c #ACACAC\",
\"$. c #A1A1A1\",
\"%. c #8A8A8A\",
\"&. c #9B9999\",
\"*. c #C7C7C7\",
\"=. c #DDDDDD\",
\"-. c #E8E8E8\",
\";. c #F2F2F2\",
\">. c #898A89\",
\",. c #7A7878\",
\"'. c #AEAEAE\",
\"). c #C4C4C4\",
\"!. c #CBCBCA\",
\"~. c #AAAAAA\",
\"{. c #939393\",
\"]. c #888888\",
\"^. c #7C7C7C\",
\"/. c #AAAAAB\",
\"(. c #BFBFBF\",
\"_. c #C9C9C9\",
\":. c #DFDEDF\",
\"<. c #A6A6A6\",
\"[. c #9B9B9B\",
\"}. c #909191\",
\"|. c #858586\",
\"1. c #797979\",
\"2. c #989494\",
\"3. c #A5A6A5\",
\"4. c #B9B9B8\",
\"5. c #C1C1C1\",
\"6. c #CFCFCE\",
\"7. c #979797\",
\"8. c #8D8D8D\",
\"9. c #828282\",
\"0. c #747171\",
\"a. c #ADAAAA\",
\"b. c #A9A8A9\",
\"c. c #B8B9B9\",
\"d. c #A5A5A5\",
\"e. c #9C9C9C\",
\"f. c #7E7E7D\",
\"g. c #929191\",
\"h. c #C9C4C4\",
\"i. c #989898\",
\"j. c #ADADAD\",
\"k. c #9D9D9D\",
\"l. c #8C8C8C\",
\"m. c #787878\",
\"n. c #B8B6B6\",
\"o. c #939191\",
\"p. c #A5A5A6\",
\"q. c #ABABAA\",
\"r. c #A8A8A9\",
\"s. c #A3A3A3\",
\"t. c #858585\",
\"u. c #757474\",
\"v. c #C5C1C1\",
\"w. c #969696\",
\"x. c #9B9B9C\",
\"y. c #A4A4A4\",
\"z. c #9E9E9E\",
\"A. c #939394\",
\"B. c #7D7D7D\",
\"C. c #747474\",
\"D. c #B7B5B5\",
\"E. c #A5A1A1\",
\"F. c #919191\",
\"G. c #9A9999\",
\"H. c #838383\",
\"I. c #757575\",
\"J. c #939090\",
\"K. c #A29E9E\",
\"L. c #868686\",
\"M. c #8D8D8C\",
\"N. c #8E8E8E\",
\"O. c #8D8D8E\",
\"P. c #8B8C8C\",
\"Q. c #848485\",
\"R. c #7F7F80\",
\"S. c #7A7A7A\",
\"T. c #737373\",
\"U. c #929090\",
\"V. c #828080\",
\"W. c #818181\",
\"X. c #808080\",
\"Y. c #7E7E7E\",
\"Z. c #737272\",
\"`. c #B7B4B4\",
\" + c #BCBABA\",
\".+ c #959494\",
\"++ c #747172\",
\"@+ c #767676\",
\"#+ c #6F6D6D\",
\"$+ c #8F8E8E\",
\" . + @ # $ % & * = . \",
\" - ; > , ' ) ! ~ { ] ^ / \",
\" ( _ > : < [ } | 1 2 3 4 ] 5 6 ( \",
\" 7 ) 1 8 9 0 a b c | : 3 { d e f \",
\" g ! h i 0 j k l m n o | 2 ~ p q r s \",
\". t ' | u v w x y z A n B 1 ! C D E F . \",
\"G H : i I J K L M N z O b | ) P Q R S T \",
\"U V W X Y Z ` ...+.y l @.} ' #.$.e %.&.\",
\"& H W *.n =.-.;. .L x k 0 [ , #.Q e >.,.\",
\"] '.2 ).a k z -.` K w j !.< > ~.d {.].^.\",
\"d /.> (._.I k =.:.J v 0 8 : V <.[.}.|.1.\",
\"2.3.~ 4.5._.6.n Y I u i 1 > P $.7.8.9.0.\",
\"a.d b.V c.(.).*.X i | h ) '.d.e.E ].f.g.\",
\"h.i.$.C ~ > 2 W W : ' ! j.d.k.e l.9.m.n.\",
\". o.i.d p.q.'.H V H j.r.s.k.e 8.t.^.u.. \",
\" v.r w.x.Q s.d.d.y.] z.5 A.8.t.B.C.D. \",
\" E.l.F.e i.G.q 5 7.{.r %.H.^.I.J. \",
\" ( K.L.%.M.N.N.O.P.S Q.R.S.T.U.( \",
\" @ V.W.H.H.9.X.Y.S.I.Z.`. \",
\" . +.+++@+C.#+$+D.. \"};")
;; ======================================================================
;; Functions
(defsubst bubbles--grid-width ()
"Return the grid width for the current game theme."
(car (case bubbles-game-theme
('easy
bubbles--grid-small)
('medium
bubbles--grid-medium)
('difficult
bubbles--grid-large)
('hard
bubbles--grid-huge)
('user-defined
bubbles-grid-size))))
(defsubst bubbles--grid-height ()
"Return the grid height for the current game theme."
(cdr (case bubbles-game-theme
('easy
bubbles--grid-small)
('medium
bubbles--grid-medium)
('difficult
bubbles--grid-large)
('hard
bubbles--grid-huge)
('user-defined
bubbles-grid-size))))
(defsubst bubbles--colors ()
"Return the color list for the current game theme."
(case bubbles-game-theme
('easy
bubbles--colors-2)
('medium
bubbles--colors-3)
('difficult
bubbles--colors-4)
('hard
bubbles--colors-5)
('user-defined
bubbles-colors)))
(defsubst bubbles--shift-mode ()
"Return the shift mode for the current game theme."
(case bubbles-game-theme
('easy
'default)
('medium
'default)
('difficult
'always)
('hard
'always)
('user-defined
bubbles-shift-mode)))
(defun bubbles-save-settings ()
"Save current customization settings."
(interactive)
(custom-set-variables
(list 'bubbles-game-theme `(quote ,bubbles-game-theme) t)
(list 'bubbles-graphics-theme `(quote ,bubbles-graphics-theme) t))
(customize-save-customized))
(defsubst bubbles--empty-char ()
"The character used for removed bubbles (empty grid cells)."
?\s)
(defun bubbles-set-graphics-theme-ascii ()
"Set graphics theme to `ascii'."
(interactive)
(setq bubbles-graphics-theme 'ascii)
(bubbles--update-faces-or-images))
(defun bubbles-set-graphics-theme-circles ()
"Set graphics theme to `circles'."
(interactive)
(setq bubbles-graphics-theme 'circles)
(bubbles--initialize-images)
(bubbles--update-faces-or-images))
(defun bubbles-set-graphics-theme-squares ()
"Set graphics theme to `squares'."
(interactive)
(setq bubbles-graphics-theme 'squares)
(bubbles--initialize-images)
(bubbles--update-faces-or-images))
(defun bubbles-set-graphics-theme-diamonds ()
"Set graphics theme to `diamonds'."
(interactive)
(setq bubbles-graphics-theme 'diamonds)
(bubbles--initialize-images)
(bubbles--update-faces-or-images))
(defun bubbles-set-graphics-theme-balls ()
"Set graphics theme to `balls'."
(interactive)
(setq bubbles-graphics-theme 'balls)
(bubbles--initialize-images)
(bubbles--update-faces-or-images))
(defun bubbles-set-graphics-theme-emacs ()
"Set graphics theme to `emacs'."
(interactive)
(setq bubbles-graphics-theme 'emacs)
(bubbles--initialize-images)
(bubbles--update-faces-or-images))
;; game theme menu
(defvar bubbles-game-theme-menu
(let ((menu (make-sparse-keymap "Game Theme")))
(define-key menu [bubbles-set-game-userdefined]
(list 'menu-item "User defined" 'bubbles-set-game-userdefined
:button '(:radio . (eq bubbles-game-theme 'user-defined))))
(define-key menu [bubbles-set-game-hard]
(list 'menu-item "Hard" 'bubbles-set-game-hard
:button '(:radio . (eq bubbles-game-theme 'hard))))
(define-key menu [bubbles-set-game-difficult]
(list 'menu-item "Difficult" 'bubbles-set-game-difficult
:button '(:radio . (eq bubbles-game-theme 'difficult))))
(define-key menu [bubbles-set-game-medium]
(list 'menu-item "Medium" 'bubbles-set-game-medium
:button '(:radio . (eq bubbles-game-theme 'medium))))
(define-key menu [bubbles-set-game-easy]
(list 'menu-item "Easy" 'bubbles-set-game-easy
:button '(:radio . (eq bubbles-game-theme 'easy))))
menu)
"Map for bubbles game theme menu.")
;; graphics theme menu
(defvar bubbles-graphics-theme-menu
(let ((menu (make-sparse-keymap "Graphics Theme")))
(define-key menu [bubbles-set-graphics-theme-ascii]
(list 'menu-item "ASCII" 'bubbles-set-graphics-theme-ascii
:button '(:radio . (eq bubbles-graphics-theme 'ascii))))
(define-key menu [bubbles-set-graphics-theme-emacs]
(list 'menu-item "Emacs" 'bubbles-set-graphics-theme-emacs
:button '(:radio . (eq bubbles-graphics-theme 'emacs))))
(define-key menu [bubbles-set-graphics-theme-balls]
(list 'menu-item "Balls" 'bubbles-set-graphics-theme-balls
:button '(:radio . (eq bubbles-graphics-theme 'balls))))
(define-key menu [bubbles-set-graphics-theme-diamonds]
(list 'menu-item "Diamonds" 'bubbles-set-graphics-theme-diamonds
:button '(:radio . (eq bubbles-graphics-theme 'diamonds))))
(define-key menu [bubbles-set-graphics-theme-squares]
(list 'menu-item "Squares" 'bubbles-set-graphics-theme-squares
:button '(:radio . (eq bubbles-graphics-theme 'squares))))
(define-key menu [bubbles-set-graphics-theme-circles]
(list 'menu-item "Circles" 'bubbles-set-graphics-theme-circles
:button '(:radio . (eq bubbles-graphics-theme 'circles))))
menu)
"Map for bubbles graphics theme menu.")
;; menu
(defvar bubbles-menu
(let ((menu (make-sparse-keymap "Bubbles")))
(define-key menu [bubbles-quit]
(list 'menu-item "Quit" 'bubbles-quit))
(define-key menu [bubbles]
(list 'menu-item "New game" 'bubbles))
(define-key menu [bubbles-separator-1]
'("--"))
(define-key menu [bubbles-save-settings]
(list 'menu-item "Save all settings" 'bubbles-save-settings))
(define-key menu [bubbles-customize]
(list 'menu-item "Edit all settings" 'bubbles-customize))
(define-key menu [bubbles-game-theme-menu]
(list 'menu-item "Game Theme" bubbles-game-theme-menu))
(define-key menu [bubbles-graphics-theme-menu]
(list 'menu-item "Graphics Theme" bubbles-graphics-theme-menu
:enable 'bubbles--playing))
(define-key menu [bubbles-separator-2]
'("--"))
(define-key menu [bubbles-undo]
(list 'menu-item "Undo last move" 'bubbles-undo
:enable '(and bubbles--playing (listp buffer-undo-list))))
menu)
"Map for bubbles menu.")
;; bubbles mode map
(defvar bubbles-mode-map
(let ((map (make-sparse-keymap 'bubbles-mode-map)))
;; (suppress-keymap map t)
(define-key map "q" 'bubbles-quit)
(define-key map "\n" 'bubbles-plop)
(define-key map " " 'bubbles-plop)
(define-key map [double-down-mouse-1] 'bubbles-plop)
(define-key map [mouse-2] 'bubbles-plop)
(define-key map "\C-m" 'bubbles-plop)
(define-key map "u" 'bubbles-undo)
(define-key map "p" 'previous-line)
(define-key map "n" 'next-line)
(define-key map "f" 'forward-char)
(define-key map "b" 'backward-char)
;; bind menu to mouse
(define-key map [down-mouse-3] bubbles-menu)
;; Put menu in menu-bar
(define-key map [menu-bar Bubbles] (cons "Bubbles" bubbles-menu))
map)
"Mode map for bubbles.")
(define-derived-mode bubbles-mode nil "Bubbles"
"Major mode for playing bubbles.
\\{bubbles-mode-map}"
(setq buffer-read-only t)
(buffer-disable-undo)
(force-mode-line-update)
(redisplay)
(add-hook 'post-command-hook 'bubbles--mark-neighbourhood t t))
;;;###autoload
(defun bubbles ()
"Play Bubbles game."
(interactive)
(switch-to-buffer (get-buffer-create "*bubbles*"))
(when (or (not bubbles--playing)
(y-or-n-p "Start new game? "))
(setq bubbles--save-data nil)
(setq bubbles--playing t)
(bubbles--initialize)))
(defun bubbles-quit ()
"Quit Bubbles."
(interactive)
(message "bubbles-quit")
(bury-buffer))
(declare-function image-size "image.c" (spec &optional pixels frame))
(defun bubbles--compute-offsets ()
"Update horizontal and vertical offsets for centering the bubbles grid.
Set `bubbles--col-offset' and `bubbles--row-offset'."
(cond ((and (display-images-p)
bubbles--images-ok
(not (eq bubbles-graphics-theme 'ascii))
(fboundp 'window-inside-pixel-edges))
;; compute offset in units of pixels
(let ((bubbles--image-size
(car (image-size (car bubbles--images) t))))
(setq bubbles--col-offset
(list
(max 0 (/ (- (nth 2 (window-inside-pixel-edges))
(nth 0 (window-inside-pixel-edges))
(* ( + bubbles--image-size 2) ;; margin
(bubbles--grid-width))) 2))))
(setq bubbles--row-offset
(list
(max 0 (/ (- (nth 3 (window-inside-pixel-edges))
(nth 1 (window-inside-pixel-edges))
(* (+ bubbles--image-size 1) ;; margin
(bubbles--grid-height))) 2))))))
(t
;; compute offset in units of chars
(setq bubbles--col-offset
(max 0 (/ (- (window-width)
(bubbles--grid-width)) 2)))
(setq bubbles--row-offset
(max 0 (/ (- (window-height)
(bubbles--grid-height) 2) 2))))))
(defun bubbles--remove-overlays ()
"Remove all overlays."
(if (fboundp 'remove-overlays)
(remove-overlays)))
(defun bubbles--initialize ()
"Initialize Bubbles game."
(bubbles--initialize-faces)
(bubbles--initialize-images)
(bubbles--remove-overlays)
(switch-to-buffer (get-buffer-create "*bubbles*"))
(bubbles--compute-offsets)
(let ((inhibit-read-only t))
(set-buffer-modified-p nil)
(erase-buffer)
(insert " ")
(add-text-properties
(point-min) (point) (list 'intangible t 'display
(cons 'space
(list :height bubbles--row-offset))))
(insert "\n")
(let ((max-char (length (bubbles--colors))))
(dotimes (i (bubbles--grid-height))
(let ((p (point)))
(insert " ")
(add-text-properties
p (point) (list 'intangible t
'display (cons 'space
(list :width
bubbles--col-offset)))))
(dotimes (j (bubbles--grid-width))
(let* ((index (random max-char))
(char (nth index bubbles-chars)))
(insert char)
(add-text-properties (1- (point)) (point) (list 'index index))))
(insert "\n"))
(insert "\n ")
(add-text-properties
(1- (point)) (point) (list 'intangible t 'display
(cons 'space
(list :width bubbles--col-offset)))))
(put-text-property (point-min) (point-max) 'pointer 'arrow))
(bubbles-mode)
(bubbles--reset-score)
(bubbles--update-faces-or-images)
(bubbles--goto 0 0)
(setq buffer-undo-list t)
(force-mode-line-update)
(redisplay))
(defun bubbles--initialize-faces ()
"Prepare faces for playing `bubbles'."
(copy-face 'default 'bubbles--highlight-face)
(set-face-background 'bubbles--highlight-face "#8080f4")
(when (display-color-p)
(setq bubbles--faces
(mapcar (lambda (color)
(let ((fname (intern (format "bubbles--face-%s" color))))
(unless (facep fname)
(copy-face 'default fname)
(set-face-foreground fname color))
fname))
(bubbles--colors)))))
(defsubst bubbles--row (pos)
"Return row of point POS."
(save-excursion
(goto-char pos)
(beginning-of-line)
(1- (count-lines (point-min) (point)))))
(defsubst bubbles--col (pos)
"Return column of point POS."
(save-excursion
(goto-char pos)
(1- (current-column))))
(defun bubbles--goto (row col)
"Move point to bubble at coordinates ROW and COL."
(if (or (< row 0)
(< col 0)
(>= row (bubbles--grid-height))
(>= col (bubbles--grid-width)))
;; Error! return nil
nil
;; go
(goto-char (point-min))
(forward-line (1+ row))
(forward-char (1+ col))
(point)))
(defun bubbles--char-at (row col)
"Return character at bubble ROW and COL."
(save-excursion
(if (bubbles--goto row col)
(char-after (point))
nil)))
(defun bubbles--mark-direct-neighbours (row col char)
"Mark direct neighbors of bubble at ROW COL with same CHAR."
(save-excursion
(let ((count 0))
(when (and (bubbles--goto row col)
(eq char (char-after (point)))
(not (get-text-property (point) 'active)))
(add-text-properties (point) (1+ (point))
'(active t face 'bubbles--highlight-face))
(setq count (+ 1
(bubbles--mark-direct-neighbours row (1+ col) char)
(bubbles--mark-direct-neighbours row (1- col) char)
(bubbles--mark-direct-neighbours (1+ row) col char)
(bubbles--mark-direct-neighbours (1- row) col char))))
count)))
(defun bubbles--mark-neighbourhood (&optional pos)
"Mark neighborhood of point.
Use optional parameter POS instead of point if given."
(when bubbles--playing
(unless pos (setq pos (point)))
(condition-case err
(let ((char (char-after pos))
(inhibit-read-only t)
(row (bubbles--row (point)))
(col (bubbles--col (point))))
(add-text-properties (point-min) (point-max)
'(face default active nil))
(let ((count 0))
(when (and row col (not (eq char (bubbles--empty-char))))
(setq count (bubbles--mark-direct-neighbours row col char))
(unless (> count 1)
(add-text-properties (point-min) (point-max)
'(face default active nil))
(setq count 0)))
(bubbles--update-neighbourhood-score count))
(put-text-property (point-min) (point-max) 'pointer 'arrow)
(bubbles--update-faces-or-images)
(sit-for 0))
(error (message "Bubbles: Internal error %s" err)))))
(defun bubbles--neighbourhood-available ()
"Return t if another valid neighborhood is available."
(catch 'found
(save-excursion
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(let ((c (bubbles--char-at i j)))
(if (and (not (eq c (bubbles--empty-char)))
(or (eq c (bubbles--char-at (1+ i) j))
(eq c (bubbles--char-at i (1+ j)))))
(throw 'found t)))))
nil)))
(defun bubbles--count ()
"Count remaining bubbles."
(let ((count 0))
(save-excursion
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(let ((c (bubbles--char-at i j)))
(if (not (eq c (bubbles--empty-char)))
(setq count (1+ count)))))))
count))
(defun bubbles--reset-score ()
"Reset bubbles score."
(setq bubbles--neighbourhood-score 0
bubbles--score 0)
(bubbles--update-score))
(defun bubbles--update-score ()
"Calculate and display new bubbles score."
(setq bubbles--score (+ bubbles--score bubbles--neighbourhood-score))
(bubbles--show-scores))
(defun bubbles--update-neighbourhood-score (size)
"Calculate and display score of active neighborhood from its SIZE."
(if (> size 1)
(setq bubbles--neighbourhood-score (expt (- size 1) 2))
(setq bubbles--neighbourhood-score 0))
(bubbles--show-scores))
(defun bubbles--show-scores ()
"Display current scores."
(save-excursion
(goto-char (or (next-single-property-change (point-min) 'status)
(point-max)))
(let ((inhibit-read-only t)
(pos (point)))
(delete-region (point) (point-max))
(insert (format "Selected: %4d\n" bubbles--neighbourhood-score))
(insert " ")
(add-text-properties (1- (point)) (point)
(list 'intangible t 'display
(cons 'space
(list :width bubbles--col-offset))))
(insert (format "Score: %4d" bubbles--score))
(put-text-property pos (point) 'status t))))
(defun bubbles--game-over ()
"Finish bubbles game."
(bubbles--update-faces-or-images)
(setq bubbles--playing nil
bubbles--save-data nil)
;; add bonus if all bubbles were removed
(when (= 0 (bubbles--count))
(setq bubbles--score (+ bubbles--score (* (bubbles--grid-height)
(bubbles--grid-width))))
(bubbles--show-scores))
;; Game over message
(goto-char (point-max))
(let* ((inhibit-read-only t))
(insert "\n ")
(add-text-properties (1- (point)) (point)
(list 'intangible t 'display
(cons 'space
(list :width bubbles--col-offset))))
(insert "Game Over!"))
;; save score
(gamegrid-add-score (format "bubbles-%s-%d-%d-%d-scores"
(symbol-name (bubbles--shift-mode))
(length (bubbles--colors))
(bubbles--grid-width) (bubbles--grid-height))
bubbles--score))
(defun bubbles-plop ()
"Remove active bubbles region."
(interactive)
(when (and bubbles--playing
(> bubbles--neighbourhood-score 0))
(setq bubbles--save-data (list bubbles--score (buffer-string)))
(let ((inhibit-read-only t))
;; blank out current neighbourhood
(let ((row (bubbles--row (point)))
(col (bubbles--col (point))))
(goto-char (point-max))
(while (not (bobp))
(backward-char)
(while (get-text-property (point) 'active)
(delete-char 1)
(insert (bubbles--empty-char))
(add-text-properties (1- (point)) (point) (list 'removed t
'index -1))))
(bubbles--goto row col))
;; show new score
(bubbles--update-score)
;; update display and wait
(bubbles--update-faces-or-images)
(sit-for 0)
(sleep-for 0.2)
(discard-input)
;; drop down
(let ((something-dropped nil))
(save-excursion
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(bubbles--goto i j)
(while (get-text-property (point) 'removed)
(setq something-dropped (or (bubbles--shift 'top i j)
something-dropped))))))
;; update display and wait
(bubbles--update-faces-or-images)
(when something-dropped
(sit-for 0)))
(discard-input)
;; shift to left
(put-text-property (point-min) (point-max) 'removed nil)
(save-excursion
(goto-char (point-min))
(let ((removed-string (format "%c" (bubbles--empty-char))))
(while (search-forward removed-string nil t)
(put-text-property (1- (point)) (point) 'removed t))))
(let ((shifted nil))
(cond ((eq (bubbles--shift-mode) 'always)
(save-excursion
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(bubbles--goto i j)
(while (get-text-property (point) 'removed)
(setq shifted (or (bubbles--shift 'right i j)
shifted))))))
(bubbles--update-faces-or-images)
(sleep-for 0.5))
(t ;; default shift-mode
(save-excursion
(dotimes (j (bubbles--grid-width))
(bubbles--goto (1- (bubbles--grid-height)) j)
(let ((shifted-cols 0))
(while (get-text-property (point) 'removed)
(setq shifted-cols (1+ shifted-cols))
(bubbles--shift 'right (1- (bubbles--grid-height)) j))
(dotimes (k shifted-cols)
(let ((i (- (bubbles--grid-height) 2)))
(while (>= i 0)
(setq shifted (or (bubbles--shift 'right i j)
shifted))
(setq i (1- i))))))))))
(when shifted
;;(sleep-for 0.5)
(bubbles--update-faces-or-images)
(sit-for 0)))
(put-text-property (point-min) (point-max) 'removed nil)
(unless (bubbles--neighbourhood-available)
(bubbles--game-over)))
;; undo
(setq buffer-undo-list '((apply bubbles-undo . nil)))
(force-mode-line-update)
(redisplay)))
(defun bubbles-undo ()
"Undo last move."
(interactive)
(when bubbles--save-data
(let ((inhibit-read-only t)
(pos (point)))
(erase-buffer)
(insert (cadr bubbles--save-data))
(bubbles--update-faces-or-images)
(setq bubbles--score (car bubbles--save-data))
(goto-char pos))
(setq buffer-undo-list t)
(force-mode-line-update)
(redisplay)))
(defun bubbles--shift (from row col)
"Move bubbles FROM one side to position ROW COL.
Return t if new char is non-empty."
(save-excursion
(when (bubbles--goto row col)
(let ((char-org (char-after (point)))
(char-new (bubbles--empty-char))
(removed nil)
(trow row)
(tcol col)
(index -1))
(cond ((eq from 'top)
(setq trow (1- row)))
((eq from 'left)
(setq tcol (1- col)))
((eq from 'right)
(setq tcol (1+ col))))
(save-excursion
(when (bubbles--goto trow tcol)
(setq char-new (char-after (point)))
(setq removed (get-text-property (point) 'removed))
(setq index (get-text-property (point) 'index))
(bubbles--shift from trow tcol)))
(insert char-new)
(delete-char 1)
(add-text-properties (1- (point)) (point) (list 'index index
'removed removed))
(not (eq char-new (bubbles--empty-char)))))))
(defun bubbles--initialize-images ()
"Prepare images for playing `bubbles'."
(when (and (display-images-p)
(not (eq bubbles-graphics-theme 'ascii)))
(let ((template (case bubbles-graphics-theme
('circles bubbles--image-template-circle)
('balls bubbles--image-template-ball)
('squares bubbles--image-template-square)
('diamonds bubbles--image-template-diamond)
('emacs bubbles--image-template-emacs))))
(setq bubbles--empty-image
(create-image (replace-regexp-in-string
"^\"\\(.*\\)\t.*c .*\",$"
"\"\\1\tc None\"," template)
'xpm t
;;:mask 'heuristic
:margin '(2 . 1)))
(setq bubbles--images
(mapcar (lambda (color)
(let* ((rgb (color-values color))
(red (nth 0 rgb))
(green (nth 1 rgb))
(blue (nth 2 rgb)))
(with-temp-buffer
(insert template)
(goto-char (point-min))
(re-search-forward
"^\"[0-9]+ [0-9]+ \\(.*?\\) .*\",$" nil t)
(goto-char (point-min))
(while (re-search-forward
"^\"\\(.*\\)\t.*c \\(#.*\\)\",$" nil t)
(let* ((crgb (color-values (match-string 2)))
(r (nth 0 crgb))
(g (nth 1 crgb))
(b (nth 2 crgb))
(brightness (/ (+ r g b) 3.0 256 256))
(val (sin (* brightness (/ pi 2))))
(rr (* red val))
(gg (* green val))
(bb (* blue val))
;;(rr (/ (+ red r) 2))
;;(gg (/ (+ green g) 2))
;;(bb (/ (+ blue b) 2))
(color (format "#%02x%02x%02x"
(/ rr 256) (/ gg 256)
(/ bb 256))))
(replace-match (format "\"\\1\tc %s\","
(upcase color)))))
(create-image (buffer-string) 'xpm t
:margin '(2 . 1)
;;:mask 'heuristic
))))
(bubbles--colors))))
;; check images
(setq bubbles--images-ok bubbles--empty-image)
(mapc (lambda (elt)
(setq bubbles--images-ok (and bubbles--images-ok elt)))
bubbles--images)))
(defun bubbles--update-faces-or-images ()
"Update faces and/or images, depending on graphics mode."
(bubbles--set-faces)
(bubbles--show-images))
(defun bubbles--set-faces ()
"Update faces in the bubbles buffer."
(unless (and (display-images-p)
bubbles--images-ok
(not (eq bubbles-graphics-theme 'ascii)))
(when (display-color-p)
(save-excursion
(let ((inhibit-read-only t))
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(bubbles--goto i j)
(let* ((index (get-text-property (point) 'index))
(face (nth index bubbles--faces))
(fg-col (face-foreground face)))
(when (get-text-property (point) 'active)
(set-face-foreground 'bubbles--highlight-face "#ff0000")
(setq face 'bubbles--highlight-face))
(put-text-property (point) (1+ (point))
'face face)))))))))
(defun bubbles--show-images ()
"Update images in the bubbles buffer."
(bubbles--remove-overlays)
(if (and (display-images-p)
bubbles--images-ok
(not (eq bubbles-graphics-theme 'ascii)))
(save-excursion
(goto-char (point-min))
(forward-line 1)
(let ((inhibit-read-only t)
char)
(dotimes (i (bubbles--grid-height))
(dotimes (j (bubbles--grid-width))
(forward-char 1)
(let ((index (or (get-text-property (point) 'index) -1)))
(let ((img bubbles--empty-image))
(if (>= index 0)
(setq img (nth index bubbles--images)))
(put-text-property (point) (1+ (point))
'display (cons img nil)))))
(forward-line 1))))
(save-excursion
(let ((inhibit-read-only t))
(goto-char (point-min))
(while (not (eobp))
(let ((disp-prop (get-text-property (point) 'display)))
(if (and (listp disp-prop)
(listp (car disp-prop))
(eq (caar disp-prop) 'image))
(put-text-property (point) (1+ (point)) 'display nil))
(forward-char 1)))
(put-text-property (point-min) (point-max) 'pointer 'arrow)))))
(provide 'bubbles)
;; arch-tag: 2cd7237a-b0ad-400d-a7fd-75f676dceb70
;;; bubbles.el ends here