Sunday, March 22, 2015

Parallel Redmine Installations: Installation

Install Redmine with RVM, Git, Apache, Phusion Passenger and MySql under Ubuntu Server 14.04


Preface


As a system admin I've been responsable for managing our Redmine server in my company for nearly two years now. Over this time I've found some problems that made me search for a way to be able to install and test new Redmine versions without affecting my currently running Redmine environment.
Using RVM and dedicated gemsets requieres a little more overhead for the installation but on the long run it brings huge benefits, specially if you are a sysadmin.

The Concept

Following table shows how separated redmine installations can be organized using RVM.

In addition to the three elements in the table (redmine, ruby+gems,passenger) you can have separated databases for every Redmine version, or directly migrate the current database.
In my case, my goal is to be able to test new Redmine releases in a safe environment before I decide to upgrade to it. So for testing purposes, I dump the currently used Redmine database, create a new database and import the dump back into the new database.
Once I'm sure the new version works well, I drop the old database and the related Redmine and ruby stuff.
If for some reason the new version does not work, switching back to the older version is not a problem.

Following I will describe how to install Redmine from GitHub 2.6-stable branch with dedicated ruby, gems and passenger module. Follow the same steps to install other separated Redmine versions.

Install Dependencies

root@redmine:/home/adrian# apt-get install apache2 mysql-server git libmysqlclient-dev imagemagick libmagickwand-dev libcurl4-openssl-dev apache2-threaded-dev libapr1-dev libaprutil1-dev

 

Create Dedicated User

This user's home will live under /opt/redmine which will hold the redmine as well as ruby, ruby gems and the passenger modules.
root@redmine:/home/adrian# adduser --home /opt/redmine redmine
Add the redmine user to the sudo group
root@redmine:/home/adrian# gpasswd -a redmine sudo
This is needed since RVM will install system dependencies via apt-get when needed.

 

Install RVM

Switch to the newly created redmine user
root@redmine:/home/adrian# su - redmine
Get RVM gpg key
redmine@redmine:~$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
Install RVM stable
redmine@redmine:~$ curl -sSL https://get.rvm.io | bash -s stable
To start using RVM source the rvm script
redmine@redmine:~$ source /opt/redmine/.rvm/scripts/rvm
The RVM installer adds RVM to the user's PATH variable so there is no need to source RVM every time you switch to the redmine user.

Before we move on to install ruby and ruby gems, we will prepare the database and get Redmine from Git.

Prepare Redmine database


In this example I will create a new database named redmine_production, adjust the database name to your needs. If you are going to use the same database there is no need to create a new one but I would recommend to create a new separated database for testing since the rake:migrate commands later on will make changes to the database.

Connect to the mysql server with the root user
redmine@redmine:~$ mysql -uroot -p
Create the Redmine database and the redmine user to connect to that database.
mysql> create database redmine_production character set utf8;
mysql> create user 'redmine'@'localhost' identified by 'redmine_mysql_user_password';
mysql> grant all privileges on redmine_production.* to 'redmine'@'localhost';
mysql> flush privileges;
mysql> exit

Install Redmine from Git

Clone Redmine's repository on GitHub
redmine@redmine:~$ git clone https://github.com/redmine/redmine.git redmine-2.6-stable
Symlink the Redmine directory
redmine@redmine:~$ ln -s redmine-2.6-stable active
This symlink is needed for an easier switch between Redmine versions. The apache virtual host file will reference the DocumentRoot, PassengerAppRoot, etc using this active symlink.
Of course you can have different virtual hosts for the different Redmine paths and enable/disable as needed. I prefer the symlink method.

Cehckout the 2.6 stable branch
redmine@redmine:~/redmine-2.6.2$ git checkout origin/2.6-stable -b 2.6-stable
Configure Redmine 

The  database.yml and configuration.yml is  where you configure redmine. I will not dive into the configuration.yml file, take a look to the redmine installation documentation for it.
redmine@redmine:~/redmine-2.6.2$ cp config/database.yml.example config/database.yml
redmine@redmine:~/redmine-2.6.2$ cp config/configuration.yml.example config/configuration.yml
Edit the database.yml (production section) file to fit the mysql redmine database created in the previous section.
production:
  adapter: mysql2
  database: redmine_production
  host: localhost
  username: redmine
  password: "redmine_mysql_user_password"
  encoding: utf8
Adjust permissions
redmine@redmine:~/redmine-2.6.2$ sudo chown -R www-data files log tmp public/plugin_assets redmine@redmine:~/redmine-2.6.2$ sudo chmod -R 0775 files log tmp public/plugin_assets

Install Ruby and Gems

redmine@redmine:~$ rvm install 1.9.3
Create Gemset
redmine@redmine:~$ rvm gemset create redmine-2.6-stable
Use Gemset
This is a very important part. Every time you are going to manage gems you have to make sure the currently used gemset is the right one! Please read the RVM:Using Gemsets reference.
So after creating the redmine-2.6-stable gemset, listing the current available gemsets shows following. Note the arrow points to the currently used gemset:
redmine@redmine:~$ rvm list gemsets

rvm gemsets

=> ruby-1.9.3-p551 [ x86_64 ]
ruby-1.9.3-p551@global [ x86_64 ]
ruby-1.9.3-p551@redmine-2.6-stable [ x86_64 ]
We want to use our redmine-2.6-stable gemset:
redmine@redmine:~$ rvm use gemset ruby-1.9.3-p551@redmine-2.6-stable
Using /opt/redmine/.rvm/gems/ruby-1.9.3-p551 with gemset redmine-2.6-stable
List gemsets again to check that the redmine-2.6-stable gemset is selected:
redmine@redmine:~$ rvm list gemsets

rvm gemsets

ruby-1.9.3-p551 [ x86_64 ]
ruby-1.9.3-p551@global [ x86_64 ]
=> ruby-1.9.3-p551@redmine-2.6-stable [ x86_64 ]
Install Redmine Gems
redmine@redmine:~$ cd redmine-2.6-stable/
redmine@redmine:~/redmine-2.6-stable$ bundle install --without develop,test,postgresql,sqlite
redmine@redmine:~/redmine-2.6-stable$ gem install passenger
RMV and bundler integration
This step is needed since we will use bundler to install Redmine required gems. More detailed information about RVM and bundler integration in the RVM site.
redmine@redmine:~$ gem regenerate_binstubs

Install Phusion Passenger

redmine@redmine:~/redmine-2.6-stable$ passenger-install-apache2-module

Setup Apache

Passenger
Create a configuration file under /etc/apache2/conf-available named passenger-ruby-1.9.3-p551@redmine-2.6-stable which links to the passenger module.
Why this name? Because later on, when we want to switch between redmine installations with their separated ruby + gems installations, we will have separated passenger modules for each of them.
LoadModule passenger_module /opt/redmine/.rvm/gems/ruby-1.9.3-p551@redmine-2.6-stable/gems/passenger-5.0.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
   PassengerRoot /opt/redmine/.rvm/gems/ruby-1.9.3-p551@redmine-2.6-stable/gems/passenger-5.0.4
   PassengerDefaultRuby /opt/redmine/.rvm/gems/ruby-1.9.3-p551@redmine-2.6-stable/wrappers/ruby
</IfModule>
Virtual Host
Note that all references to the Redmine path use the active symlink instead of the actual Redmine directory, this way you only need to point the symlink to the right Redmine directoy to switch between Redmine versions and only one virtual host file is needed.
This is just a sample file. You will have to adjust it to your needs.
<VirtualHost *:80>
ServerAdmin admin@domain.sample
ServerName redmine.domain.sample

DocumentRoot /opt/redmine/active/public/
RailsBaseURI /
PassengerAppRoot /opt/redmine/active
PassengerUser www-data
PassengerGroup www-data

<Directory /opt/redmine/active/public/>
Options +Indexes +FollowSymLinks -MultiViews
AllowOverride All
Order allow,deny
Allow from all
</Directory>
LogLevel warn
ErrorLog ${APACHE_LOG_DIR}/redmine.error.log
CustomLog ${APACHE_LOG_DIR}/redmine.access.log combined
</VirtualHost>
Final Steps
Note that the rake redmine:load_default_data step is only needed when making a fresh new installation. Omit this step if using an already exiting (or imported from dump) Redmine database. The rake redmine:plugins:migrate is needed only if you are using some extra plugins.
redmine@redmine:~/redmine-2.6-stable$ rake generate_secret_token
redmine@redmine:~/redmine-2.6-stable$ RAILS_ENV=production rake db:migrate
redmine@redmine:~/redmine-2.6-stable$ RAILS_ENV=production rake redmine:load_default_data
redmine@redmine:~/redmine-2.6-stable$ RAILS_ENV=production rake redmine:plugins:migrate

Monday, April 21, 2014

Encrypted Software RAID-5 on Debian Wheezy

This post will cover the creation of a software RAID-5 with Luks encryption on top in a pure practical way. For more detailed information about Linux Software RAID check out following links:

https://raid.wiki.kernel.org/index.php/RAID_setup
https://wiki.archlinux.org/index.php/RAID

 

 Layout


Partitioning

Create a same sized partition on every disk of type Non-FS (Id=da). Its important to create identic partitions on every disk to avoid future problems if a disk has to be replaced and the RAID has to be resynced.
Fdisk output of the RAID members after partitioning:

Device Boot         Start         End      Blocks   Id  System
/dev/sda1            2048  1953525167   976761560   da  Non-FS data
/dev/sdb1            2048  1953525167   976761560   da  Non-FS data
/dev/sdc1            2048  1953525167   976761560   da  Non-FS data
/dev/sdd1            2048  1953525167   976761560   da  Non-FS data
/dev/sde1            2048  1953525167   976761560   da  Non-FS data

Create the RAID

mdadm --create --verbose --level=5 --metadata=1.2 --raid-devices=4 /dev/md0 /dev/sd[abcd]1 \
--spare-devices=1 /dev/sde1
The RAID should now start syncing. You can check the proggress with
watch cat /proc/mdstat
If for some reason the sync doesn't start automatically, you can force is with the following command
mdadm --readwrite /dev/mdX
Where /dev/mdX is your RAID device. For example /dev/md0
Depending on your RAID size this process will take a couple of hours.
Save the RAID configuration
mdadm --detail --scan >> /etc/mdadm/mdadm.conf

Encrypt RAID

Create the LUKS container on the RAID device /dev/md0. I used aes-xts-plain64, there are others like aes-cbc-essiv (which is the default at the time of this writing). What is the better cipher depends on many factors and is out of the scope of this post.


cryptsetup luksFormat --cipher aes-xts-plain64 --hash sha512 /dev/md0

This will require a confirmation in upper case to go on and enter the desired passphrase.
Open the new LUKS container:

cryptsetup luksDump /dev/md0

This will create a new link under /dev/mapper named crypt-raid To get crypt setup on the encrypted device:

cryptsetup luksDump /dev/md0

This shows, among other data, the used algorithm, key hash and used key slots

Create File System

Before you can mount the encrypted RAID device you have to create a file system on it. In this case I will go eith ext4:

mkfs.ext4 /dev/mapper/crypt-raid

Mount Device

Before the crypt device can be mounted it has to be unlocked:

cryptsetup luksOpen /dev/md0 crypt-raid

This maps the crypt device under /dev/mapper/crypt-raid
Now the device can be mounted as usual:

mount /dev/mapper/crypt-raid [mount point]

 

On Boot Mount

In order to get the encrypted device unlocked and mounted on boot, it has to be added to both the /etc/crypttab and the /etc/fstab files as follows:

/etc/crypttab:
crypt-raid /dev/md0 none luks
 
/etc/fstab:
/dev/mapper/crypt-raid /mnt/raid ext4 defaults 0 0

This will block the boot process until you provide the required passphrase which can be a problem if you are running a headless server. One solution is to provide a key file on an external USB stick or only use the fstab entry (no crypttab entry) with the noauto option and manually mount the device via ssh.

Sunday, April 28, 2013

Thunar Custom Actions: Pictures Batch Resize with Zenity Progress Bar and Convert

This is e better version of my old picture resize script. It uses Zenity to ask for the resizing percentage and to show a progress bar while the script is working.

Screenshots

Resize Script Enter Percentage

Resize Script Progress Bar
Resize Script Work Completed

Bash Script


picresize.sh
 1 #!/bin/bash
 2 
 3 
 4 # paths, constants, etc.
 5 PICPATH="$1"
 6 TOTALPICS="$((($# - 1)))"
 7 STEP="$(((100 / $TOTALPICS)))"
 8 PROGR=0
 9 MINSIZE=1
10 MAXSIZE=500
11 
12 # get the new size percentage, check it and set output directory path
13 SIZE=""
14 while [ -z "$SIZE" ] || [[ $SIZE<$MINSIZE ]] || [[ $SIZE>$MAXSIZE ]]; do
15     SIZE="$(zenity --entry --title="Picture Resize" --text="Enter percentage...")"
16 
17     # exit if cancel pressed
18     [[ $? != 0 ]] && exit 0
19 
20     # check range
21     if [[ $SIZE<$MINSIZE ]] || [[ $SIZE>$MAXSIZE ]]; then
22         zenity --error --text="$SIZE% is outside the accepted range: 1% - 500%!"
23     fi
24 done
25 
26 OUTPATH="resize_$SIZE%"
27 
28 # check if output directory exists
29 [ -d "$PICPATH/$OUTPATH" ] || mkdir "$PICPATH/$OUTPATH"
30 shift
31 
32 # convert pics, show progress bar
33 while (( $# )); do
34     convert "$PICPATH/$1" -resize "$SIZE%" "$PICPATH/$OUTPATH/resized_$SIZE%-$1"
35     ((PROGR+=$STEP))
36     echo $PROGR
37     shift
38 done | zenity --progress --title="Picture Resize" --text="working..." --percentage=0 -
39                                  -auto-close
40 
41 # check return value and print message depending on it
42 [[ $? == 0 ]] && zenity --info --title="Picture Resize" --text="All done!" || zenity -
43        -error --text="Canceled!"

Add Thunar Custom Action

Please refer to this article for adding a custom action to Thunar.
Here is a screenshot of my custom action for this resizing script.

Note that the bash script needs two "thunar arguments" :
  1. Path to the pictures to be resized -> %d
  2. Pictures list -> %N


Saturday, April 27, 2013

Volume OSD with Openbox and Riitek Micro Keyboard

With this small bash script you can show a nice progress bar on the screen when changing the volume in a TV-like fashion.
The script uses osd_cat provided by the xosd package under Arch Linux (Please refer to your system specific package manager for install this package under other distributions).

In order to make this script work with Openbox you will have to edit the Openbox configuration file rc.xml file normally placed under /home/<user>/.config/openbox/rc.xml. I will show how I did it later on this post.

OSD Bash Script

A few function definitions make the main part simpler. They are mainly responsible of increasing/decreasing the volume and showing the on-screen message.
There are also some constants responsible for placing the message on a certain screen position, setting the font, etc.
test.sh
 1 #!/bin/bash
 2 
 3 
 4 
 5 # constants
 6 FONT='-*-fixed-*-*-*-*-100-*-*-*-*-*-*-*'
 7 COLOR="green"
 8 DELAY=4
 9 POS="bottom"
10 ALIGN="center"
11 BARMOD="percentage"
12 VOLTXT="Volume"
13 VOLMUTEDTXT="Muted"
14 VOLSTEP=2
15 
16 
17 
18 # kills an existing osd_cat process
19 # needed when holding down a key to force repaint of the onscreen message
20 preKill() {
21     killall osd_cat
22 }
23 
24 # gets the actual volume value
25 getVol() {
26     VOL="$(amixer sget Master | grep "Mono:" | awk '{ print $4 }' | sed -e 's/\[//' -
           e 's/\]//' -e 's/\%//')"
27 }
28 
29 # gets the actual volume value and prints is on the screen
30 # with a percent bar + a percent number
31 showVol() {
32     getVol
33     if [[ $VOL == 0 ]]; then
34         osd_cat --pos="$POS" --align="$ALIGN" --delay="$DELAY" --colour="$COLOR" --
                         font="$FONT" --barmode="$BARMOD" --text="$VOLMUTEDTXT $VOL%" --
                         percentage="$VOL"
35     else
36         osd_cat --pos="$POS" --align="$ALIGN" --delay="$DELAY" --colour="$COLOR" --
                         font="$FONT" --barmode="$BARMOD" --text="$VOLTXT $VOL%" --
                         percentage="$VOL"
37     fi
38 }
39 
40 # reises the master channel by "VOLSTEP"
41 volUp() {
42     amixer sset Master "$VOLSTEP+"
43 }
44 
45 # decreases the master channel by "VOLSTEP"
46 volDown() {
47     amixer sset Master "$VOLSTEP-"
48 }
49 
50 # mutes the master channel
51 volMute() {
52     amixer sset Master 0
53 }
54 
55 
56 # main part
57 preKill
58 
59 case "$1" in
60     "volup")
61             volUp
62             showVol
63             ;;
64     "voldown")
65             volDown
66             showVol
67             ;;
68     "mute")
69             volMute
70             showVol
71             ;;
72     *)
73             ;;
74 esac

Openbox rc.xml File

The rc.xml file has a keyboard section where custom keyboard shortcuts can be defined.

x.xml
 1     <keybind key="W-KP_Add">
 2       <action name="execute">
 3         <execute>/home/adrian/scripts/osd.sh volup</execute>
 4       </action>
 5     </keybind>
 6     <keybind key="W-KP_Subtract">
 7       <action name="execute">
 8         <execute>/home/adrian/scripts/osd.sh voldown</execute>
 9       </action>
10     </keybind>
11     <keybind key="W-m">
12       <action name="execute">
13         <execute>/home/adrian/scripts/osd.sh mute</execute>
14       </action>
15     </keybind>
Riitek Micro Keyboard

Besides the keyboard shortcuts, I wanted to use my Riitek Micro Keyboard RT-MWK03. When pressing the Vulume Up/Down/Mute key combination on the keyboard an X event is generated on the X server.
These X events can be used in the rc.xml file just the same way as any other keyboard shortcut. I used Xev to find out what events are generated when pressing the volume key combinations.
y.xml
 1     <keybind key="XF86AudioRaiseVolume">
 2       <action name="execute">
 3         <execute>/home/adrian/scripts/osd.sh volup</execute>
 4       </action>
 5     </keybind>
 6     <keybind key="XF86AudioLowerVolume">
 7       <action name="execute">
 8         <execute>/home/adrian/scripts/osd.sh voldown</execute>
 9       </action>
10     </keybind>
11     <keybind key="XF86AudioMute">
12       <action name="execute">
13         <execute>/home/adrian/scripts/osd.sh mute</execute>
14       </action>
15     </keybind>


Screenshots


Openbox OSD Script Volume 59%

Openbox OSD Script Muted Volume


Thursday, September 27, 2012

Adjust pdf quality with Ghostscript and Bash

Sometimes pdf files are too big to attach them to an email. Here is a simple bash script witch uses Ghostscript to adjust a pdf file quality (and size) to three different levels: low, mid and high.
To see the script usage just type ./pdf-quality.sh on the terminal. Don't forget to give it execution permissions: chmod u+x  pdf-quality.sh

 

Code

.pdf-quality.sh.tmp
  1 #/bin/bash
  2 
  3 
  4 # function to print proper usage
  5 function usage() {
  6   echo "Usage: pdf-quality.sh <quality> <input_file>"
  7   echo "Where <quality> is low, mid or high and <input_file> the pdf file to be adjusted."
  8 }
  9 
 10 
 11 # check parameter number
 12 if [[ $# != 2 ]]; then usage; exit 1; fi
 13 
 14 # set variables
 15 QUALITY="$1"
 16 INFILE="$2"
 17 OUTFILE="$(echo "$2" | sed 's/.pdf//')-$QUALITY.pdf"
 18 
 19 # perform action depending on the given parameter
 20 case "$QUALITY" in
 21   "low")
 22     gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/screen -
                                      sOutputFile="$OUTFILE" "$INFILE";;
 23   "mid")
 24     gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -
                                      sOutputFile="$OUTFILE" "$INFILE";;
 25   "high")
 26     gs -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/printer -
                                      sOutputFile="$OUTFILE" "$INFILE";;
 27   *)  echo "$1 is not a valid option."
 28     usage
 29     exit 1;;
 30 esac
 31 
 32 echo -e "\nall done!"
 33 exit 0

Wednesday, August 22, 2012

python: yat (yet another tetris)


Who doesn't want to write its own version of one of the most popular games in the video game history, huh?

Here is YAT, my tetris version written  in python using the popular game developing module pygame.

Source code is available here.

Screenshots


About surfaces:

I think, besides the game logic, the most interesting thing here is the use of two different surfaces to show the blocks and the game information on the screen (including the next block).
This is managed in the main game file following. Basically I'm updating the game information surface with the "updateInfo" function and the blocks screen with the table class method "show". Both functions update the game surface "screen" with their own surfaces by themselves.

.tempfile
  1 #======================================================================#
  2 # This file is part of YAT (Yet Another Tetris).                       #
  3 #                                                                      #
  4 # YAT is free software: you can redistribute it and/or modify          #
  5 # it under the terms of the GNU General Public License as published by #
  6 # the Free Software Foundation, either version 3 of the License, or    #
  7 # (at your option) any later version.                                  #
  8 #                                                                      #
  9 # YAT is distributed in the hope that it will be useful,               #
 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of       #
 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        #
 12 # GNU General Public License for more details.                         #
 13 #                                                                      #
 14 # You should have received a copy of the GNU General Public License    #
 15 # along with YAT.     If not, see <http://www.gnu.org/licenses/>.      #
 16 #======================================================================#
 17 
 18 #==========================================================================#
 19 # Name       : yat.py (Yet Another Tetris)                                 #
 20 # Description: Main game file for the tetris like game "yat"               #
 21 # Author     : Adrian Antonana                                             #
 22 # Date       : 17.08.2012                                                  #
 23 # Copyright  : Adrian Antonana 2012                                        #
 24 #==========================================================================#
 25 import pygame as pg
 26 import table  as tb
 27 import blocks as bk
 28 import colors as col
 29 
 30 #==========================================================================#
 31 #                     Global Variables and Constants                       #
 32 #==========================================================================#
 33 GAME_SPEED     = 500
 34 SPEED_INC_TICK = 50
 35 LINES_INC_TICK = 10
 36 LEVEL          = 1
 37 REMOVED_LINES  = 0
 38 MAX_LEVEL      = 10
 39 FPS            = 100
 40 
 41 #==========================================================================#
 42 #                           Function Definitions                           #
 43 #==========================================================================#
 44 
 45 #-------------------- Checks when blocks have to move down ----------------#
 46 def delay(ticks):
 47   return (ticks % GAME_SPEED) >= GAME_SPEED-10
 48 
 49 #------------------ Increases the Game Level and Game Speed ---------------#
 50 def incSpeed(remlines):
 51   global GAME_SPEED
 52   global LEVEL
 53 
 54   if LEVEL < MAX_LEVEL:
 55     if remlines / (LEVEL*LINES_INC_TICK) == 1:
 56       LEVEL += 1
 57       GAME_SPEED -= SPEED_INC_TICK
 58       return True
 59   return False
 60 
 61 #---------------------- Updates the information surface -------------------#
 62 def updateInfo(nb):
 63   global LEVEL_NUM_TEXT
 64   global LINES_NUM_TEXT
 65   global infosurface
 66 
 67   LEVEL_NUM_TEXT = font.render(str(LEVEL),True,col.WHITE)
 68   LINES_NUM_TEXT = font.render(str(REMOVED_LINES),True,col.WHITE)
 69   infosurface.fill(col.GREY_DARK)
 70   infosurface.blit(LEVEL_TEXT,LEVEL_TEXT_OFFSET)
 71   infosurface.blit(LEVEL_NUM_TEXT,LEVEL_NUM_TEXT_OFFSET)
 72   infosurface.blit(LINES_TEXT,LINES_TEXT_OFFSET)
 73   infosurface.blit(LINES_NUM_TEXT,LINES_NUM_TEXT_OFFSET)
 74   nb.show(infosurface,NEXT_BLOCK_OFFSET,20,INF_BLOCK_SIZE)
 75 
 76 
 77 #==========================================================================#
 78 #              Initialize pygame (display,mixer and clock)                 #
 79 #==========================================================================#
 80 pg.init()
 81 pg.mixer.init()
 82 sndblockplaced = pg.mixer.Sound("sounds/block_placed.wav")
 83 sndblockrotate = pg.mixer.Sound("sounds/block_rotate.wav")
 84 sndremovelines = pg.mixer.Sound("sounds/remove_lines.wav")
 85 sndlevelup     = pg.mixer.Sound("sounds/level_up.wav")
 86 sndgameover    = pg.mixer.Sound("sounds/game_over.wav")
 87 clock          = pg.time.Clock()
 88 pg.display.set_caption("yat - yet another tetris")
 89 pg.key.set_repeat(10,50)
 90 
 91 #==========================================================================#
 92 #                         Information surface                              #
 93 #==========================================================================#
 94 INFO_SURFACE_HEIGHT   = 105
 95 FONT_SIZE             = 30
 96 FONT_SIZE_GAME_OVER   = 60
 97 UPPER_OFFSET          = 20
 98 LEFT_OFFSET           = 10
 99 INF_BLOCK_SIZE        = 20
100 font                  = pg.font.SysFont(pg.font.get_default_font(),FONT_SIZE)
101 font_game_over        = pg.font.SysFont(pg.font.get_default_font(),
                            FONT_SIZE_GAME_OVER)
102 LEVEL_TEXT            = font.render("Level : ",True,col.WHITE)
103 LINES_TEXT            = font.render("Lines : ",True,col.WHITE)
104 LEVEL_TEXT_OFFSET     = (LEFT_OFFSET,UPPER_OFFSET)
105 LEVEL_NUM_TEXT_OFFSET = (70+LEFT_OFFSET,UPPER_OFFSET)
106 LINES_TEXT_OFFSET     = (LEFT_OFFSET,INFO_SURFACE_HEIGHT-40)
107 LINES_NUM_TEXT_OFFSET = (70+LEFT_OFFSET,INFO_SURFACE_HEIGHT-40)
108 NEXT_BLOCK_OFFSET     = tb.BLOCK_SIZE*tb.WIDTH - INF_BLOCK_SIZE * 5
109 
110 #==========================================================================#
111 #                            Game over text                                #
112 #==========================================================================#
113 GAME_OVER_TEXT        = font_game_over.render("GAME OVER",True,col.WHITE)
114 GAME_OVER_TEXT_OFFSET = ((tb.BLOCK_SIZE*tb.WIDTH/2)-120,(tb.BLOCK_SIZE*tb.
                            HEIGHT/2)-50)
115 
116 #==========================================================================#
117 #                         Initialize surfaces                              #
118 #==========================================================================#
119 screen         = pg.display.set_mode((tb.BLOCK_SIZE*tb.WIDTH,tb.
                     BLOCK_SIZE*tb.HEIGHT+INFO_SURFACE_HEIGHT))
120 tablesurface   = screen.subsurface((0,INFO_SURFACE_HEIGHT,tb.BLOCK_SIZE*tb.
                     WIDTH,tb.BLOCK_SIZE*tb.HEIGHT))
121 infosurface    = screen.subsurface((0,0,tb.BLOCK_SIZE*tb.WIDTH,
                     INFO_SURFACE_HEIGHT))
122 
123 #==========================================================================#
124 #                        Block spawn position                              #
125 #==========================================================================#
126 BLOCK_SPAWN_POS = (0,(tb.WIDTH/2)-1)
127 
128 #==========================================================================#
129 #               Create the table and an initial block                      #
130 #==========================================================================#
131 t     = tb.table(tablesurface)
132 b     = bk.block(BLOCK_SPAWN_POS)
133 nextb = bk.block(BLOCK_SPAWN_POS)
134 
135 #==========================================================================#
136 #                 Draw initial information surface                         #
137 #==========================================================================#
138 updateInfo(nextb)
139 
140 #==========================================================================#
141 #                            Main loop                                     #
142 #==========================================================================#
143 running = True
144 
145 while running:
146   clock.tick_busy_loop(FPS)
147   t.adBlock(b.getPosList(),b.getType())
148   t.show()
149 
150   # check if the fall delay has been reached. If yes, move block down.
151   if delay(pg.time.get_ticks()):
152     if b.canMovDown(t.getHeight(),t.getOcupPosList(b.getPosList())):
153       t.adBlock(b.getPosList(),bk.E)
154       b.movDown()
155     else:
156       # if the block can't move down, spawn a new block
157       t.adBlock(b.getPosList(),b.getType())
158       sndblockplaced.play()
159       retval = t.delFullLines()
160       if retval != 0:
161         sndremovelines.play()
162         REMOVED_LINES += retval
163         if incSpeed(REMOVED_LINES):
164           sndlevelup.play()
165       b.__init__(BLOCK_SPAWN_POS,nextb.getType())
166       nextb = bk.block(BLOCK_SPAWN_POS)
167       updateInfo(nextb)
168 
169       # check if the game is over
170       if t.gameOver(b.getPosList()):
171         t.adBlock(b.getPosList(),b.getType())
172         t.show()
173         running = False
174 
175   # get one event from the queue and perform action
176   event = pg.event.poll()
177   if event.type == pg.KEYDOWN:
178     key = event.key
179     if key == pg.K_ESCAPE:
180       running = False
181     elif key == pg.K_DOWN:
182       if b.canMovDown(t.getHeight(),t.getOcupPosList(b.getPosList())):
183         t.adBlock(b.getPosList(),bk.E)
184         b.movDown()
185       else:
186         # if the block can't move down, spawn a new block
187         t.adBlock(b.getPosList(),b.getType())
188         sndblockplaced.play()
189         retval = t.delFullLines()
190         if retval != 0:
191           sndremovelines.play()
192           REMOVED_LINES += retval
193           if incSpeed(REMOVED_LINES):
194             sndlevelup.play()
195         b.__init__(BLOCK_SPAWN_POS,nextb.getType())
196         nextb = bk.block(BLOCK_SPAWN_POS)
197         updateInfo(nextb)
198 
199         # check if the game is over
200         if t.gameOver(b.getPosList()):
201           t.adBlock(b.getPosList(),b.getType())
202           t.show()
203           running = False
204 
205     # move/rotate block left/right
206     elif key == pg.K_LEFT:
207       if b.canMovLeft(t.getWidth(),t.getOcupPosList(b.getPosList())):
208         t.adBlock(b.getPosList(),bk.E)
209         b.movLeft()
210     elif key == pg.K_RIGHT:
211       if b.canMovRight(t.getWidth(),t.getOcupPosList(b.getPosList())):
212         t.adBlock(b.getPosList(),bk.E)
213         b.movRight()
214     elif key == pg.K_LCTRL:
215       t.adBlock(b.getPosList(),bk.E)
216       if b.rotLeft(t.getHeight(),t.getWidth(),t.getOcupPosList(b.getPosList()
                       )):
217         sndblockrotate.play()
218     elif key == pg.K_LALT:
219       t.adBlock(b.getPosList(),bk.E)
220       if b.rotRight(t.getHeight(),t.getWidth(),t.getOcupPosList(b.getPosList(
                        ))):
221         sndblockrotate.play()
222 
223 #==========================================================================#
224 #                           The game is over                               #
225 #==========================================================================#
226 tablesurface.fill(col.BLACK)
227 t.setSurfAlpha(60)
228 t.show()
229 tablesurface.blit(GAME_OVER_TEXT,GAME_OVER_TEXT_OFFSET)
230 pg.display.flip()
231 sndgameover.play()
232 
233 quit = False
234 while not quit:
235   event = pg.event.wait()
236   if  event.type == pg.QUIT:
237     quit = True
238   if event.type == pg.KEYDOWN:
239     if event.key == pg.K_ESCAPE:
240       quit = True

Wednesday, August 1, 2012

python: snake game

Here is my second try to write a simple game in python using pygame. I'm not posting here the whole source code since I don't want the post to be too long. The complete source code can be downloaded from this link.

First lets take a look at the game...

Snake Game Screen Cast:


 

Snake Class: 

Besides pygame, im using three self made classes on the game:
  • snake.py
  • food.py 
  • colors.py
The source code has plenty of comments which should make it quite easy to understand what is going on.
snake.py
  1 #==============================================================================================#
  2 # Name        : snake.py                                                                       #
  3 # Description : The snake class definition for the snake game.                                 #
  4 # Author      : Adrian Antonana                                                                #
  5 # Date        : 29.07.2012                                                                     #
  6 #==============================================================================================#
  7 
  8 # imports
  9 import pygame as pg
 10 from colors import *
 11 
 12 # motion direction constants
 13 UP    = 0
 14 DOWN  = 1
 15 LEFT  = 2
 16 RIGHT = 3
 17 
 18 # block sizes
 19 BLOCK_SIZE       = 30
 20 BLOCK_SIZE_INNER = 20
 21 
 22 # snake class definition
 23 class snake:
 24 
 25   # constructor
 26   def __init__(self,surface,headposx=10,headposy=10):
 27     self.surface = surface
 28     self.length  = 10
 29     self.poslist = [(headposx,y) for y in reversed(range(headposy-self.length+1,headposy+1))]
 30     self.motdir  = RIGHT
 31     self.crashed = False
 32 
 33     # for drawing the snake
 34     self.snakeblock = pg.Surface((BLOCK_SIZE,BLOCK_SIZE))
 35     self.snakeblock.set_alpha(255)
 36     self.snakeblock.fill(GREEN)
 37     self.snakeblockdark = pg.Surface((BLOCK_SIZE_INNER,BLOCK_SIZE_INNER))
 38     self.snakeblockdark.set_alpha(255)
 39     self.snakeblockdark.fill(GREEN_DARK)
 40 
 41     # for removing the snake
 42     self.backblock = pg.Surface((BLOCK_SIZE,BLOCK_SIZE))
 43     self.backblock.set_alpha(255)
 44     self.backblock.fill(BLACK)
 45 
 46   # get snake's head position
 47   def getHeadPos(self):
 48     return (self.poslist[0])
 49 
 50   # get the motion direction
 51   def getMotionDir(self):
 52     return self.motdir
 53 
 54   # get the snake positions list
 55   def getPosList(self):
 56     return self.poslist
 57 
 58   # set the motion direction
 59   def setMotionDir(self,motdir):
 60     self.motdir = motdir
 61 
 62   # increase the snake length by one
 63   def incLength(self):
 64     self.length += 1
 65 
 66   # move the snake updates the positions list and checks if the snake has crashed
 67   def move(self):
 68     motdir = self.getMotionDir()
 69     headpos = self.getHeadPos()
 70 
 71     # update positions
 72     if motdir == UP:
 73       poslist = [(headpos[0]-1,headpos[1])]
 74     elif motdir == DOWN:
 75       poslist = [(headpos[0]+1,headpos[1])]
 76     elif motdir == LEFT:
 77       poslist = [(headpos[0],headpos[1]-1)]
 78     elif motdir == RIGHT:
 79       poslist = [(headpos[0],headpos[1]+1)]
 80 
 81     poslist.extend(self.poslist[:-1])
 82     self.poslist = poslist
 83 
 84     # check if crashed
 85     if self.getHeadPos() in self.getPosList()[1:]:
 86       self.crashed = True
 87 
 88   # check if the snake has crashed
 89   def chrashed(self):
 90     return self.crashed
 91 
 92   # grow the snake. add a new position at the end
 93   def grow(self):
 94     lastpos = self.getPosList()[-1]
 95     self.length += 1
 96     self.poslist.append((lastpos[0]-1,lastpos[1]))
 97 
 98   # draw the snake
 99   def draw(self):
100     skb = self.snakeblock
101     skbd = self.snakeblockdark
102     sf = self.surface
103 
104     for blockpos in self.getPosList():
105       sf.blit(skb,(blockpos[1]*BLOCK_SIZE,blockpos[0]*BLOCK_SIZE))
106       sf.blit(skbd,(blockpos[1]*BLOCK_SIZE+5,blockpos[0]*BLOCK_SIZE+5))
107 
108   # delete the snake
109   def remove(self):
110     bkb = self.backblock
111     sf = self.surface
112 
113     # draw block for every snake position
114     for blockpos in self.getPosList():
115       sf.blit(bkb,(blockpos[1]*BLOCK_SIZE,blockpos[0]*BLOCK_SIZE))