Skip to content

Instantly share code, notes, and snippets.

@s-shin
Last active December 11, 2015 09:08
Show Gist options
  • Save s-shin/4577763 to your computer and use it in GitHub Desktop.
Save s-shin/4577763 to your computer and use it in GitHub Desktop.
CodeVS2.0で書いたコード。あまりの遅さに愕然! 順位は論外なので秘密ですが、ものすごくRubyの勉強になったので良しとします。
# encoding: utf-8
# 出来る限りロジックに集中できるようにモジュール化する
# 座標系はxが→、yが↑で、左下が(0, 0)で統一する。
# (つまりフィールドでもパックでも同じ)
# 2次元配列はy, xの順とする(ex. field[y][x])。
# 依存モジュール
require 'matrix' # 回転で使用
require 'logger' # デバッグログ出力で使用
require 'thread' # 排他制御に必要
# ## 名前空間 CodeVS2
module CodeVS2
class << self
# ### エントリーポイント
# - `case_param`: ケースごとに与えられる値P
# - `io_in`, `io_out`: 適宜ファイルに変更可能。
def main(case_param, io_in=$stdin, io_out=$stdout)
# 一行目は各種情報
line = io_in.gets().chomp!
width, height, size, sum, step = line.split(" ").map { |s| s.to_i }
# 二行目以降はパック。
# NOTE: largeの時とか逐次読み込まないとダメ?
packs = Array.new step do
pack = Array.new size do
line = io_in.gets().chomp!
line.split(" ").map { |s| Cell.new s.to_i }
end
line = io_in.gets().chomp!
abort('恐らく入力ファイルが不正です') if line != 'END'
Pack.new pack.reverse # 座標系が↑→なので
end
# ゲームオブジェクトの作成
game = Game.new width, height, sum, step, packs, case_param
while !game.pack_end? and !game.dead?
# ブロックで毎ターンごとのパックの配置処理をフック
pack, x = yield game, game.current_pack
game.update(pack, x)
io_out.puts "#{x} #{pack.r}"
io_out.flush
end
end
end
# ここまで、入出力依存のコード
#----------------------------------------------
# ゲーム全体を管理するクラス
class Game
attr_accessor :field, :sum, :score, :chain_num, :current_step
protected :field=, :sum=, :chain_num=, :current_step=
# ログを利用するか
@@enable_log = true
# 点数を計算するか
@@enable_calc_score = false
# Game#fallにおけるスレッド
# 使わないなら`false`、使うならスレッド数を指定
#@@thread_in_fall = 5
@@thread_in_fall = false
# Game#deleteにおけるスレッド
# 有効にすると点数が若干狂うことを留意
# 使わないなら`false`、使うなら`:by => :direction`などを指定。
#@@thread_in_delete = { :by => :direction }
@@thread_in_delete = false
# ロガー
if @@enable_log
@@now = Time.now
@@log = Logger.new(open(File.expand_path(File.dirname(__FILE__) +
"/debug/#{@@now.to_i}.log"), "w"))
@@log.level = Logger::DEBUG # 適宜変更すること
@@log.formatter = proc { |severity, datetime, progname, msg| "#{msg}" }
end
#--------------------------------
# #### コンストラクタ
# 最後の引数は複製用
def initialize(width, height, sum, step, packs, case_param, copy=nil)
@sum, @max_step, @packs = sum, step, packs
@case_param = case_param
@chain_num = nil # 連鎖時に使用
if !copy
@score = 0
@current_step = 1
@field = Field.new width, height, packs[0].size
if @@enable_log
@@log.info <<-EOT
Ruby v#{RUBY_VERSION}
#{@@now.to_s}
=== Basic Information ===
W (width): #{width}
H (height): #{height}
T (pack size): #{@packs[0].size}
S (sum): #{@sum}
N (max step): #{@max_step}
P (case param): #{@case_param}
=========================
EOT
end
else
copy.each { |k, v| instance_variable_set(k, v) }
end
end
#--------------------------------
# 全パック終了か
def pack_end?(n=0)
@current_step + n > @max_step
end
# 現在のパックの取得
def current_pack
next_pack(0)
end
# 次以降のパックの取得
def next_pack(n=1)
pack_end?(n) ? nil : @packs[@current_step+n-1]
end
#--------------------------------
# ### 更新処理(置く+デバッグ)
# Game#forkして使うような時は、デバッグ出力が必要ないので
# 直接Game#putを呼び出すと良い。
def update(pack, x)
if @@enable_log
start = Time.now
@@log.debug <<-EOT
turn: #{@current_step}
pack:
#{current_pack.to_s}
rotate: #{pack.r * 90}
x: #{x}
EOT
end
#---
put(pack, x)
#---
if @@enable_log
dt = ((Time.now - start) * 1000).round(3)
@@log.debug <<-EOT
---
field:
#{@field.to_s}
score: #{@score}
chain num: #{@chain_num}
time: #{dt}ms
=========================
EOT
end
end
# 死亡か
def dead?
# 落ちない
# 消えない
# デッドライン超えてる
([email protected]).each do |x|
return true if @field[@field.real_height][x].block?
end
false
end
# 置く。
def put(pack, x)
abort('置けません!') if @field.pack_outside?(pack, x)
# とりあえず置き、落とすフェーズへ
y = @field.real_height
pack.each_block do |dx, dy, block|
@field[y+dy][x+dx].set(block.val)
end
@chain_num = 0 # ここでチェーン数を初期化
fall
@current_step += 1
end
def deleted_num(pack, x)
g = fork
g.fall(false)
g.delete(true)
end
# ブロックを落とす
# NOTE: あまりに遅かったのでマルチスレッド化。ただここよりも消す部分の方が
# オーバヘッドが高そう。
def fall(auto_delete=true)
# x列目についてブロックを落とす処理
fall_x = Proc.new do |x|
bottom = 0
catch :next_column do
@field.each_block_from_bottom(x) do |_, y, block|
# ない場所を探す
while @field[bottom][x].block?
bottom += 1
throw :next_column if bottom == @field.height
end
# ない場所よりも下はどうでもいい
next if bottom > y
# 浮いたブロックを発見したので落とす
@field[bottom][x].set(block.val)
block.clear
end
end
end
# 一列づつ落とせるところまで落とす。
unless @thread_in_fall
([email protected]).each { |x| fall_x.call(x) }
else
# 各処理は競合しないので排他制御しなくてよい。
dx = @field.width / @@thread_in_fall # 切り捨てられる
begin_x = 0
threads = []
last_i = @@thread_in_fall - 1 # 次のループの最後のインデックス
@@thread_in_fall.times do |i|
# ちゃんと最後になるよう微調整
end_x = (i == last_i) ? @field.width : begin_x + dx
# スレッド生成(クロージャではまずそうなので引数で渡す)
threads << Thread.new(begin_x, end_x) do |b, e|
(b...e).each { |x| fall_x.call(x) }
end
begin_x = end_x
end
threads.each { |t| t.join } # 実行と待機
end
# 消去処理へ。消去があれば再度fallが呼ばれる(相互再帰)。
delete if auto_delete
end
# 未チェックのブロックを中心に削除処理
def delete(is_test=false)
count = 0 # 消滅カウント
if !@@thread_in_delete
# ブロックで未チェックの物をチェックすれば良い
@field.each_block_not_checked do |x, y, block|
block.checked = true
# 左右方向
count += check_to_delete(is_test) { |dx| @field.get(x+dx, y) }
# 上下方向
count += check_to_delete(is_test) { |dx| @field.get(x, y+dx) }
# 右斜め上方向
count += check_to_delete(is_test) { |dx| @field.get(x+dx, y+dx) }
# 左斜め上方向
count += check_to_delete(is_test) { |dx| @field.get(x-dx, y+dx) }
end
elsif @@thread_in_delete[:by] == :direction
# 上記のcheck_to_delete部分をスレッドにする。
# 注:速くはなるが、点数が狂う可能性がある!
#(消滅カウントを正確に出すにはマルチスレッド化することができない。)
# もっとも、連鎖数には影響ないし、概算する分には問題ないだろう。
locker = Mutex.new
threads = [
proc { |x, y| check_to_delete { |dx| @field.get(x+dx, y) } },
proc { |x, y| check_to_delete { |dx| @field.get(x, y+dx) } },
proc { |x, y| check_to_delete { |dx| @field.get(x+dx, y+dx) } },
proc { |x, y| check_to_delete { |dx| @field.get(x-dx, y+dx) } }
].map do |p|
Thread.new do
@field.each_block_not_checked do |x, y, block|
tmp = p.call(x, y)
locker.synchronize { count += tmp }
end
end
end
threads.each { |t| t.join }
elsif @@thread_in_delete[:by] == :region
locker = Mutex.new
#...
end
# 削除があるか
# 無いときは点数も計算する必要はない
if count > 0
# チェックの付いたブロックを削除
@field.each_block_to_be_deleted { |x, y, block| block.clear }
# チェーンをインクリメント
@chain_num += 1
# 点数計算
update_score count if @@enable_calc_score
# 相互再帰で続行
fall
end
end
# 点数計算
def update_score(e)
return if e == 0
pn = @case_param + (@current_step-1) % 100
@score += 2**([@chain_num, pn].min - 1) * [1, @chain_num-pn+1].max * e
end
# 周囲を調べる。
# testがtrueの時は削除があるかどうかの確認だけ行い、
# フィールドの書き換えは行わない。
# 縦、横、斜め2つについて、何個の連結かに注目して処理すれば良い。
# 結合の方向がblockを基準に2方向あるので、2次元的に考えれば良い。
# ex. (左側, 右側)
# 1個 (0, 0)
# 2個 (0, 1), (1, 0)
# 3個 (0, 2), (1, 1), (2, 0)
# ...
# といのはやめて、右にずっと見れるところまで見て、見れなくなったら
# 左を見ていく方針で行く。
def check_to_delete(is_test=false, &block)
count = 0 # 削除カウント
getCell = proc(&block)
left = 0
right = 0
100000.times do # とりあえず適当な回数
# ### 範囲外、またはブロックがあるかどうかのチェック
# このアルゴリズムの場合、ブロックが無いところまで見ているということが
# 既にチェックが完了していることを意味する。
# ブロックが無いところまで見たら左を見る
if !getCell.call(right) or getCell.call(right).none?
right -= 1
left += 1
next
end
# 左もないところまで見たら終了
break if !getCell.call(-left) or getCell.call(-left).none?
# ### ブロックがある場合のチェック
# 範囲内の和を求める
sum = 0
(-left..right).each { |dx| sum += getCell.call(dx).val }
# 基準値を超えていたら次のステップ
if sum >= @sum
# sumが一致していたら削除フラグを立てる
if sum == @sum
# テストならこの時点で終了
return true if is_test
# 同時にここで点数計算のためにブロックの消滅カウントを出すが、
# 既に全て削除フラグが立っていた場合は、カウント済みなので
# 無視する
already_deleted = true
(-left..right).each do |dx|
cell = getCell.call(dx)
if !cell.to_be_deleted
already_deleted = false
cell.to_be_deleted = true # 削除フラグ立てる
end
end
count += right + left + 1 unless already_deleted
end
# 左が詰まったらさらに右を減らしてもう一回トライ
if left > 0
right -= 1
next
end
# 左を見ていく
right -= 1
left += 1
else
# 第一段階(右を見ていく)ではleftが0
if left == 0
right += 1
else
left += 1
end
end
end
# テストならここに来た時点で削除がないのでfalseを返す
return false if is_test
count
end
#--------------------------------
# ゲーム状態の複製
def fork
Game.new \
@field.width, @field.height, @sum, @max_step, @packs, @case_param, {
:@score => @score,
:@current_step => @current_step,
:@field => Marshal.load(Marshal.dump(@field))
}
end
end
# 1マスを表すセル
class Cell
# NOTE: 消去処理の時に全座標を計算するのは現実的でない。
# 消去される可能性があるのは、落下したブロックの周りだけであるので、
# そのブロック基準に消去チェックされたかをcheckedに保持する。
# checkedが変化するのは、新しいパックが落ちてきた時とブロックが消えることで
# 落ちてきた時(falseになる)、それとチェックが終わった時である(trueになる)。
attr_accessor :val, :checked, :to_be_deleted
NONE = 0
def initialize(val=NONE)
set(val)
end
def clear
set(NONE)
end
def set(val)
# Cellを渡しても上手いことする?
#val = val.val if val.methods.grep(:val).length > 0
@val = val
@checked = false
@to_be_deleted = false
end
def none?
@val == NONE
end
def block?
!none?
end
def ==(cell)
@val == cell.val
end
# NOTE: お邪魔ブロック(obs)は、おそらく通常ブロックと一緒くたにして
# 処理できるので、判定処理をここでは書いてない。
# 必要だとしても、グローバルな設定情報を必要とするので、
# 設計が厄介になりそう。
end
# 2次元セルテーブル
class CellTable
#include Enumerable
attr_accessor :cells
def width
@cells[0].length
end
def height
@cells.length
end
# CellTable#cellsでアクセスできるが、直接アクセスできるようにもする
def [](idx)
@cells[idx]
end
def outside?(x, y)
x < 0 || width <= x || y < 0 || height <= y
end
def get(x, y)
outside?(x, y) ? nil : @cells[y][x]
end
# 各列のブロック数のうち最大のもの
def max_y
counts = []
(0...width).each do |x|
c = 0
each_block_from_bottom(x) { c += 1 }
counts << c
end
counts.max
end
# 全てのセルをイテレート
def each
(0...height).each do |y|
(0...width).each do |x|
yield x, y, @cells[y][x]
end
end
end
# 各ブロックをイテレート
def each_block
each { |x, y, cell| yield x, y, cell if cell.block? }
end
# チェックされていないブロックをイテレート
def each_block_not_checked
each_block { |x, y, cell| yield x, y, cell if !cell.checked }
end
# 削除フラグの立ったブロックをイテレート
def each_block_to_be_deleted
each_block { |x, y, cell| yield x, y, cell if cell.to_be_deleted }
end
# x列目のブロックを下からイテレート
def each_block_from_bottom(x)
(0...height).each do |y|
yield x, y, @cells[y][x] if @cells[y][x].block?
end
end
# 比較
def ==(table)
# サイズの比較
return false if width != table.width or height != table.height
# 中身の比較
(0...height).each do |y|
(0...width).each do |x|
return false if @cells[y][x] != table[y][x]
end
end
true
end
# デバッグに利用
def to_s
ret = ""
(height-1).downto(0).each do |y|
(0...width).each do |x|
v = @cells[y][x].val
ret += "%2d " % v
end
ret += "\n"
end
ret
end
# 2次元配列をCellTable(Cellの2次元配列)に変換
# NOTE: mixinされるクラスには追加されない
def self.from_a(arr)
arr.map { |row| row.map { |x| CodeVS2::Cell.new x } }
end
end
# パック
class Pack < CellTable
attr_accessor :r
protected :r=
# cells[y][x]: 2次元のCellの配列を指定
def initialize(cells=nil)
@cells = cells
@r = 0 # 初期状態からの右90度回転数
end
# 縦横同じ長さなので
def size
width
end
# rは右90度回転数。
# NOTE: 配列を回すか、アクセス時にインデックスを変換するかの2通り実装が
# 考えられるが、回転のコストと、インデックス計算のオーバヘッドの
# どっちが大きいか。とりあえず回転させることにする。
def rotate(r)
r = r % 4 # 0〜3に変換。負の時ちゃんと逆回転になる(ex: -1 % 4 = 3)。
@r += r % 4
return if r == 0 # 回転なし
# TODO: 速度に影響が出るようなら最適化する
# 回転角度
rad = - Math::PI / 2 * r
# 回転行列
mat = Matrix[
[Math.cos(rad).round, -Math.sin(rad).round],
[Math.sin(rad).round, Math.cos(rad).round]
]
# 中央点
o = Matrix[[(size-1)/2.0], [(size-1)/2.0]]
# 新しいパックセルテーブル
newCells = Array.new(size) { Array.new size }
each do |x, y, cell|
a = Matrix[[x], [y]]
oa = a - o # 中央からの相対座標にする
ob = mat * oa # 回転
b = ob + o # 絶対座標に戻す
newCells[b[1, 0].round][b[0, 0].round] = cell
end
@cells = newCells # 置き換える
end
# 左端ブロックのx座標
# NOTE: cellsのセット時に計算してしまえば毎回計算する必要はない。
# よって、最適化の余地あり。
def left_side
catch :end do
# 左から探索
(0...size).each do |x|
(0...size).each do |y|
throw :end, x if @cells[y][x].block?
end
end
end
end
# 右端ブロックのx座標
def right_side
catch :end do
# 右から探索
(size-1).downto(0).each do |x|
(0...size).each do |y|
throw :end, x if @cells[y][x].block?
end
end
end
end
end
# フィールド
class Field < CellTable
attr_accessor :real_height
protected :cells=, :real_height=
def initialize(width, height, pack_size)
# パックサイズの分だけ高さを余分に確保する
@real_height = height
@cells = Array.new(height+pack_size) { Array.new(width) {Cell.new} }
end
# 端のチェック
def pack_outside?(pack, x)
0 > x + pack.left_side or x + pack.right_side >= width
end
end
end
# encoding: utf-8
require File.expand_path(File.dirname(__FILE__) + '/codevs2')
# 動かさない
class Simple
def update(game, pack)
[pack, 0]
end
end
#------------------
runner = Simple.new
param = ARGV[0] ? ARGV[0].to_i : 25
CodeVS2.main(param) do |game, pack|
runner.update(game, pack)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment